package rpc import ( "context" "fmt" "net" "net/http" "path/filepath" "strings" "time" "github.com/OffchainLabs/prysm/v7/api" "github.com/OffchainLabs/prysm/v7/api/server/httprest" "github.com/OffchainLabs/prysm/v7/api/server/middleware" "github.com/OffchainLabs/prysm/v7/async/event" "github.com/OffchainLabs/prysm/v7/config/features" "github.com/OffchainLabs/prysm/v7/io/logs" ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v7/validator/accounts/wallet" "github.com/OffchainLabs/prysm/v7/validator/client" iface "github.com/OffchainLabs/prysm/v7/validator/client/iface" "github.com/OffchainLabs/prysm/v7/validator/db" "github.com/OffchainLabs/prysm/v7/validator/web" "github.com/pkg/errors" ) // Config options for the HTTP server. type Config struct { HTTPHost string HTTPPort int GRPCMaxCallRecvMsgSize int GRPCRetries uint GRPCRetryDelay time.Duration GRPCHeaders []string BeaconNodeGRPCEndpoint string BeaconApiEndpoint string BeaconAPIHeaders map[string][]string BeaconApiTimeout time.Duration BeaconNodeCert string DB db.Database Wallet *wallet.Wallet WalletDir string WalletInitializedFeed *event.Feed ValidatorService *client.ValidatorService AuthTokenPath string Middlewares []middleware.Middleware Router *http.ServeMux } // Server defining a HTTP server for the remote signer API and registering clients type Server struct { walletInitialized bool logStreamerBufferSize int grpcMaxCallRecvMsgSize int walletInitializedFeed *event.Feed beaconApiTimeout time.Duration wallet *wallet.Wallet validatorService *client.ValidatorService httpPort int cancel context.CancelFunc grpcRetries uint grpcRetryDelay time.Duration server *httprest.Server router *http.ServeMux authTokenPath string beaconNodeCert string beaconApiEndpoint string beaconApiHeaders map[string][]string beaconNodeEndpoint string healthClient ethpb.HealthClient nodeClient iface.NodeClient chainClient iface.ChainClient beaconNodeValidatorClient iface.ValidatorClient httpHost string authToken string db db.Database logStreamer logs.Streamer startFailure error ctx context.Context walletDir string jwtSecret []byte grpcHeaders []string } // NewServer instantiates a new HTTP server. func NewServer(ctx context.Context, cfg *Config) *Server { ctx, cancel := context.WithCancel(ctx) server := &Server{ ctx: ctx, cancel: cancel, logStreamer: logs.NewStreamServer(), logStreamerBufferSize: 1000, // Enough to handle most bursts of logs in the validator client. httpHost: cfg.HTTPHost, httpPort: cfg.HTTPPort, grpcMaxCallRecvMsgSize: cfg.GRPCMaxCallRecvMsgSize, grpcRetries: cfg.GRPCRetries, grpcRetryDelay: cfg.GRPCRetryDelay, grpcHeaders: cfg.GRPCHeaders, validatorService: cfg.ValidatorService, authTokenPath: cfg.AuthTokenPath, db: cfg.DB, walletDir: cfg.WalletDir, walletInitializedFeed: cfg.WalletInitializedFeed, walletInitialized: cfg.Wallet != nil, wallet: cfg.Wallet, beaconApiTimeout: cfg.BeaconApiTimeout, beaconApiEndpoint: cfg.BeaconApiEndpoint, beaconApiHeaders: cfg.BeaconAPIHeaders, beaconNodeEndpoint: cfg.BeaconNodeGRPCEndpoint, router: cfg.Router, } if server.authTokenPath == "" && server.walletDir != "" { // if a wallet dir is passed without an auth token then override the default with the wallet dir server.authTokenPath = filepath.Join(server.walletDir, api.AuthTokenFileName) } if server.authTokenPath != "" { if err := server.initializeAuthToken(); err != nil { log.WithError(err).Error("Could not initialize web auth token") } validatorWebAddr := fmt.Sprintf("%s:%d", server.httpHost, server.httpPort) logValidatorWebAuth(validatorWebAddr, server.authToken, server.authTokenPath) go server.refreshAuthTokenFromFileChanges(server.ctx, server.authTokenPath) } // Register a gRPC or HTTP client to the beacon node. // Used for proxy calls to beacon node from validator REST handlers if err := server.registerBeaconClient(); err != nil { log.WithError(err).Fatal("Could not register beacon chain gRPC or HTTP client") } // Adding AuthTokenHandler to the list of middlewares cfg.Middlewares = append(cfg.Middlewares, server.AuthTokenHandler) opts := []httprest.Option{ httprest.WithRouter(cfg.Router), httprest.WithHTTPAddr(net.JoinHostPort(server.httpHost, fmt.Sprintf("%d", server.httpPort))), httprest.WithMiddlewares(cfg.Middlewares), } if err := server.InitializeRoutesWithWebHandler(); err != nil { log.WithError(err).Fatal("Could not initialize routes with web handler") } // create and set a new http server s, err := httprest.New(server.ctx, opts...) if err != nil { log.WithError(err).Fatal("Failed to create HTTP server") } server.server = s return server } // Start the HTTP server and registers clients that can communicate via HTTP or gRPC. func (s *Server) Start() { s.server.Start() } // InitializeRoutesWithWebHandler adds a catchall wrapper for web handling func (s *Server) InitializeRoutesWithWebHandler() error { if err := s.InitializeRoutes(); err != nil { return err } s.router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/api") { r.URL.Path = strings.Replace(r.URL.Path, "/api", "", 1) // used to redirect apis to standard rest APIs s.router.ServeHTTP(w, r) return } if features.Get().EnableWeb { web.Handler(w, r) } }) return nil } // InitializeRoutes initializes pure HTTP REST endpoints for the validator client. // needs to be called before the Serve function func (s *Server) InitializeRoutes() error { if s.router == nil { return errors.New("no router found on server") } // Register all services, HandleFunc calls, etc. // ... s.router.HandleFunc("GET /eth/v1/keystores", s.ListKeystores) s.router.HandleFunc("POST /eth/v1/keystores", s.ImportKeystores) s.router.HandleFunc("DELETE /eth/v1/keystores", s.DeleteKeystores) s.router.HandleFunc("GET /eth/v1/remotekeys", s.ListRemoteKeys) s.router.HandleFunc("POST /eth/v1/remotekeys", s.ImportRemoteKeys) s.router.HandleFunc("DELETE /eth/v1/remotekeys", s.DeleteRemoteKeys) s.router.HandleFunc("GET /eth/v1/validator/{pubkey}/gas_limit", s.GetGasLimit) s.router.HandleFunc("POST /eth/v1/validator/{pubkey}/gas_limit", s.SetGasLimit) s.router.HandleFunc("DELETE /eth/v1/validator/{pubkey}/gas_limit", s.DeleteGasLimit) s.router.HandleFunc("GET /eth/v1/validator/{pubkey}/feerecipient", s.ListFeeRecipientByPubkey) s.router.HandleFunc("POST /eth/v1/validator/{pubkey}/feerecipient", s.SetFeeRecipientByPubkey) s.router.HandleFunc("DELETE /eth/v1/validator/{pubkey}/feerecipient", s.DeleteFeeRecipientByPubkey) s.router.HandleFunc("POST /eth/v1/validator/{pubkey}/voluntary_exit", s.SetVoluntaryExit) s.router.HandleFunc("GET /eth/v1/validator/{pubkey}/graffiti", s.GetGraffiti) s.router.HandleFunc("POST /eth/v1/validator/{pubkey}/graffiti", s.SetGraffiti) s.router.HandleFunc("DELETE /eth/v1/validator/{pubkey}/graffiti", s.DeleteGraffiti) // auth endpoint s.router.HandleFunc("GET "+api.WebUrlPrefix+"initialize", s.Initialize) // accounts endpoints s.router.HandleFunc("GET "+api.WebUrlPrefix+"accounts", s.ListAccounts) s.router.HandleFunc("POST "+api.WebUrlPrefix+"accounts/backup", s.BackupAccounts) s.router.HandleFunc("POST "+api.WebUrlPrefix+"accounts/voluntary-exit", s.VoluntaryExit) // web health endpoints s.router.HandleFunc("GET "+api.WebUrlPrefix+"health/version", s.GetVersion) s.router.HandleFunc("GET "+api.WebUrlPrefix+"health/logs/validator/stream", s.StreamValidatorLogs) s.router.HandleFunc("GET "+api.WebUrlPrefix+"health/logs/beacon/stream", s.StreamBeaconLogs) // Beacon calls s.router.HandleFunc("GET "+api.WebUrlPrefix+"beacon/status", s.GetBeaconStatus) s.router.HandleFunc("GET "+api.WebUrlPrefix+"beacon/summary", s.GetValidatorPerformance) s.router.HandleFunc("GET "+api.WebUrlPrefix+"beacon/validators", s.GetValidators) s.router.HandleFunc("GET "+api.WebUrlPrefix+"beacon/balances", s.GetValidatorBalances) s.router.HandleFunc("GET "+api.WebUrlPrefix+"beacon/peers", s.GetPeers) // web wallet endpoints s.router.HandleFunc("GET "+api.WebUrlPrefix+"wallet", s.WalletConfig) s.router.HandleFunc("POST "+api.WebUrlPrefix+"wallet/create", s.CreateWallet) s.router.HandleFunc("POST "+api.WebUrlPrefix+"wallet/keystores/validate", s.ValidateKeystores) s.router.HandleFunc("POST "+api.WebUrlPrefix+"wallet/recover", s.RecoverWallet) // slashing protection endpoints s.router.HandleFunc("GET "+api.WebUrlPrefix+"slashing-protection/export", s.ExportSlashingProtection) s.router.HandleFunc("POST "+api.WebUrlPrefix+"slashing-protection/import", s.ImportSlashingProtection) log.Info("Initialized REST API routes") return nil } // Stop the HTTP server. func (s *Server) Stop() error { return s.server.Stop() } // Status returns an error if the service is unhealthy. func (s *Server) Status() error { if s.startFailure != nil { return s.startFailure } return nil }