mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 15:37:56 -05:00
HTTP Validator API: slashing protection import and export (#13165)
* adding migration for import and export slashing protection * Update validator/rpc/handle_slashing.go Co-authored-by: Radosław Kapka <rkapka@wp.pl> * Update validator/rpc/handle_slashing.go Co-authored-by: Radosław Kapka <rkapka@wp.pl> * Update validator/rpc/handle_slashing.go Co-authored-by: Radosław Kapka <rkapka@wp.pl> * Update validator/rpc/handle_slashing.go Co-authored-by: Radosław Kapka <rkapka@wp.pl> * addressing comments * fixing unit test errors after view comments --------- Co-authored-by: Radosław Kapka <rkapka@wp.pl>
This commit is contained in:
@@ -798,7 +798,6 @@ func (c *ValidatorClient) registerRPCGatewayService(router *mux.Router) error {
|
||||
pb.RegisterHealthHandler,
|
||||
validatorpb.RegisterAccountsHandler,
|
||||
validatorpb.RegisterBeaconHandler,
|
||||
validatorpb.RegisterSlashingProtectionHandler,
|
||||
}
|
||||
gwmux := gwruntime.NewServeMux(
|
||||
gwruntime.WithMarshalerOption(gwruntime.MIMEWildcard, &gwruntime.HTTPBodyMarshaler{
|
||||
|
||||
@@ -8,10 +8,10 @@ go_library(
|
||||
"beacon.go",
|
||||
"handlers_health.go",
|
||||
"handlers_keymanager.go",
|
||||
"handlers_slashing.go",
|
||||
"intercepter.go",
|
||||
"log.go",
|
||||
"server.go",
|
||||
"slashing.go",
|
||||
"structs.go",
|
||||
"wallet.go",
|
||||
],
|
||||
@@ -93,9 +93,9 @@ go_test(
|
||||
"beacon_test.go",
|
||||
"handlers_health_test.go",
|
||||
"handlers_keymanager_test.go",
|
||||
"handlers_slashing_test.go",
|
||||
"intercepter_test.go",
|
||||
"server_test.go",
|
||||
"slashing_test.go",
|
||||
"wallet_test.go",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"github.com/prysmaticlabs/prysm/v4/consensus-types/validator"
|
||||
"github.com/prysmaticlabs/prysm/v4/encoding/bytesutil"
|
||||
eth "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1"
|
||||
validatorpb "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"
|
||||
@@ -411,12 +410,17 @@ func TestServer_DeleteKeystores(t *testing.T) {
|
||||
// JSON encode the protection JSON and save it.
|
||||
encoded, err := json.Marshal(mockJSON)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = srv.ImportSlashingProtection(ctx, &validatorpb.ImportSlashingProtectionRequest{
|
||||
request := &ImportSlashingProtectionRequest{
|
||||
SlashingProtectionJson: string(encoded),
|
||||
})
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err = json.NewEncoder(&buf).Encode(request)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v2/validator/slashing-protection/import", &buf)
|
||||
wr := httptest.NewRecorder()
|
||||
srv.ImportSlashingProtection(wr, req)
|
||||
require.Equal(t, http.StatusOK, wr.Code)
|
||||
t.Run("no slashing protection response if no keys in request even if we have a history in DB", func(t *testing.T) {
|
||||
request := &DeleteKeystoresRequest{
|
||||
Pubkeys: nil,
|
||||
|
||||
84
validator/rpc/handlers_slashing.go
Normal file
84
validator/rpc/handlers_slashing.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
httputil "github.com/prysmaticlabs/prysm/v4/network/http"
|
||||
slashing "github.com/prysmaticlabs/prysm/v4/validator/slashing-protection-history"
|
||||
"go.opencensus.io/trace"
|
||||
)
|
||||
|
||||
// ExportSlashingProtection handles the rpc call returning the json slashing history.
|
||||
// The format of the export follows the EIP-3076 standard which makes it
|
||||
// easy to migrate machines or Ethereum consensus clients.
|
||||
//
|
||||
// Steps:
|
||||
// 1. Call the function which exports the data from
|
||||
// the validator's db into an EIP standard slashing protection format.
|
||||
// 2. Format and send JSON in the response.
|
||||
func (s *Server) ExportSlashingProtection(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := trace.StartSpan(r.Context(), "validator.ExportSlashingProtection")
|
||||
defer span.End()
|
||||
|
||||
if s.valDB == nil {
|
||||
httputil.HandleError(w, "could not find validator database", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
eipJSON, err := slashing.ExportStandardProtectionJSON(ctx, s.valDB)
|
||||
if err != nil {
|
||||
httputil.HandleError(w, errors.Wrap(err, "could not export slashing protection history").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
encoded, err := json.MarshalIndent(eipJSON, "", "\t")
|
||||
if err != nil {
|
||||
httputil.HandleError(w, errors.Wrap(err, "could not JSON marshal slashing protection history").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
httputil.WriteJson(w, &ExportSlashingProtectionResponse{
|
||||
File: string(encoded),
|
||||
})
|
||||
}
|
||||
|
||||
// ImportSlashingProtection reads an input slashing protection EIP-3076
|
||||
// standard JSON string and inserts the data into validator DB.
|
||||
//
|
||||
// Read the JSON string passed through rpc, then call the func
|
||||
// which actually imports the data from the JSON file into our database. Use the Keymanager APIs if an API is required.
|
||||
func (s *Server) ImportSlashingProtection(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := trace.StartSpan(r.Context(), "validator.ImportSlashingProtection")
|
||||
defer span.End()
|
||||
|
||||
if s.valDB == nil {
|
||||
httputil.HandleError(w, "could not find validator database", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var req ImportSlashingProtectionRequest
|
||||
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.SlashingProtectionJson == "" {
|
||||
httputil.HandleError(w, "empty slashing_protection_json specified", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
enc := []byte(req.SlashingProtectionJson)
|
||||
buf := bytes.NewBuffer(enc)
|
||||
if err := slashing.ImportStandardProtectionJSON(ctx, s.valDB, buf); err != nil {
|
||||
httputil.HandleError(w, errors.Wrap(err, "could not import slashing protection history").Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Info("Slashing protection JSON successfully imported")
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
pb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1/validator-client"
|
||||
"github.com/prysmaticlabs/prysm/v4/testing/require"
|
||||
"github.com/prysmaticlabs/prysm/v4/validator/accounts"
|
||||
"github.com/prysmaticlabs/prysm/v4/validator/db/kv"
|
||||
@@ -21,16 +22,24 @@ func TestImportSlashingProtection_Preconditions(t *testing.T) {
|
||||
defaultWalletPath = localWalletDir
|
||||
|
||||
// Empty JSON.
|
||||
req := &pb.ImportSlashingProtectionRequest{
|
||||
SlashingProtectionJson: "",
|
||||
}
|
||||
s := &Server{
|
||||
walletDir: defaultWalletPath,
|
||||
}
|
||||
|
||||
request := &ImportSlashingProtectionRequest{
|
||||
SlashingProtectionJson: "",
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err := json.NewEncoder(&buf).Encode(request)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v2/validator/slashing-protection/import", &buf)
|
||||
wr := httptest.NewRecorder()
|
||||
wr.Body = &bytes.Buffer{}
|
||||
// No validator DB provided.
|
||||
_, err := s.ImportSlashingProtection(ctx, req)
|
||||
require.ErrorContains(t, "err finding validator database at path", err)
|
||||
s.ImportSlashingProtection(wr, req)
|
||||
require.Equal(t, http.StatusInternalServerError, wr.Code)
|
||||
require.StringContains(t, "could not find validator database", wr.Body.String())
|
||||
|
||||
// Create Wallet and add to server for more realistic testing.
|
||||
opts := []accounts.Option{
|
||||
@@ -63,8 +72,11 @@ func TestImportSlashingProtection_Preconditions(t *testing.T) {
|
||||
}()
|
||||
|
||||
// Test empty JSON.
|
||||
_, err = s.ImportSlashingProtection(ctx, req)
|
||||
require.ErrorContains(t, "empty slashing_protection json specified", err)
|
||||
wr = httptest.NewRecorder()
|
||||
wr.Body = &bytes.Buffer{}
|
||||
s.ImportSlashingProtection(wr, req)
|
||||
require.Equal(t, http.StatusBadRequest, wr.Code)
|
||||
require.StringContains(t, "empty slashing_protection_json specified", wr.Body.String())
|
||||
|
||||
// Generate mock slashing history.
|
||||
attestingHistory := make([][]*kv.AttestationRecord, 0)
|
||||
@@ -78,10 +90,15 @@ func TestImportSlashingProtection_Preconditions(t *testing.T) {
|
||||
// JSON encode the protection JSON and save it in rpc req.
|
||||
encoded, err := json.Marshal(mockJSON)
|
||||
require.NoError(t, err)
|
||||
req.SlashingProtectionJson = string(encoded)
|
||||
|
||||
_, err = s.ImportSlashingProtection(ctx, req)
|
||||
request.SlashingProtectionJson = string(encoded)
|
||||
err = json.NewEncoder(&buf).Encode(request)
|
||||
require.NoError(t, err)
|
||||
|
||||
req = httptest.NewRequest(http.MethodPost, "/v2/validator/slashing-protection/import", &buf)
|
||||
wr = httptest.NewRecorder()
|
||||
wr.Body = &bytes.Buffer{}
|
||||
s.ImportSlashingProtection(wr, req)
|
||||
require.Equal(t, http.StatusOK, wr.Code)
|
||||
}
|
||||
|
||||
func TestExportSlashingProtection_Preconditions(t *testing.T) {
|
||||
@@ -92,9 +109,13 @@ func TestExportSlashingProtection_Preconditions(t *testing.T) {
|
||||
s := &Server{
|
||||
walletDir: defaultWalletPath,
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodGet, "/v2/validator/slashing-protection/export", nil)
|
||||
wr := httptest.NewRecorder()
|
||||
wr.Body = &bytes.Buffer{}
|
||||
// No validator DB provided.
|
||||
_, err := s.ExportSlashingProtection(ctx, &empty.Empty{})
|
||||
require.ErrorContains(t, "err finding validator database at path", err)
|
||||
s.ExportSlashingProtection(wr, req)
|
||||
require.Equal(t, http.StatusInternalServerError, wr.Code)
|
||||
require.StringContains(t, "could not find validator database", wr.Body.String())
|
||||
|
||||
numValidators := 10
|
||||
// Create public keys for the mock validator DB.
|
||||
@@ -115,9 +136,10 @@ func TestExportSlashingProtection_Preconditions(t *testing.T) {
|
||||
genesisValidatorsRoot := [32]byte{1}
|
||||
err = validatorDB.SaveGenesisValidatorsRoot(ctx, genesisValidatorsRoot[:])
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = s.ExportSlashingProtection(ctx, &empty.Empty{})
|
||||
require.NoError(t, err)
|
||||
wr = httptest.NewRecorder()
|
||||
wr.Body = &bytes.Buffer{}
|
||||
s.ExportSlashingProtection(wr, req)
|
||||
require.Equal(t, http.StatusOK, wr.Code)
|
||||
}
|
||||
|
||||
func TestImportExportSlashingProtection_RoundTrip(t *testing.T) {
|
||||
@@ -158,18 +180,25 @@ func TestImportExportSlashingProtection_RoundTrip(t *testing.T) {
|
||||
// JSON encode the protection JSON and save it in rpc req.
|
||||
encoded, err := json.Marshal(mockJSON)
|
||||
require.NoError(t, err)
|
||||
req := &pb.ImportSlashingProtectionRequest{
|
||||
request := &ImportSlashingProtectionRequest{
|
||||
SlashingProtectionJson: string(encoded),
|
||||
}
|
||||
|
||||
_, err = s.ImportSlashingProtection(ctx, req)
|
||||
var buf bytes.Buffer
|
||||
err = json.NewEncoder(&buf).Encode(request)
|
||||
require.NoError(t, err)
|
||||
|
||||
reqE, err := s.ExportSlashingProtection(ctx, &empty.Empty{})
|
||||
require.NoError(t, err)
|
||||
req := httptest.NewRequest(http.MethodPost, "/v2/validator/slashing-protection/import", &buf)
|
||||
wr := httptest.NewRecorder()
|
||||
s.ImportSlashingProtection(wr, req)
|
||||
|
||||
req = httptest.NewRequest(http.MethodGet, "/v2/validator/slashing-protection/export", nil)
|
||||
wr = httptest.NewRecorder()
|
||||
s.ExportSlashingProtection(wr, req)
|
||||
require.Equal(t, http.StatusOK, wr.Code)
|
||||
resp := &ExportSlashingProtectionResponse{}
|
||||
require.NoError(t, json.Unmarshal(wr.Body.Bytes(), resp))
|
||||
// Attempt to read the exported data and convert from string to EIP-3076.
|
||||
enc := []byte(reqE.File)
|
||||
enc := []byte(resp.File)
|
||||
|
||||
receivedJSON := &format.EIPSlashingProtectionFormat{}
|
||||
err = json.Unmarshal(enc, receivedJSON)
|
||||
@@ -187,7 +187,6 @@ func (s *Server) Start() {
|
||||
validatorpb.RegisterWalletServer(s.grpcServer, s)
|
||||
validatorpb.RegisterBeaconServer(s.grpcServer, s)
|
||||
validatorpb.RegisterAccountsServer(s.grpcServer, s)
|
||||
validatorpb.RegisterSlashingProtectionServer(s.grpcServer, s)
|
||||
|
||||
// routes needs to be set before the server calls the server function
|
||||
go func() {
|
||||
@@ -237,6 +236,9 @@ 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)
|
||||
// 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)
|
||||
log.Info("Initialized REST API routes")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ 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/slashing-protection/export": {http.MethodGet},
|
||||
"/v2/validator/slashing-protection/import": {http.MethodPost},
|
||||
}
|
||||
gotRouteList := make(map[string][]string)
|
||||
err = s.router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error {
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
package rpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/golang/protobuf/ptypes/empty"
|
||||
"github.com/pkg/errors"
|
||||
pb "github.com/prysmaticlabs/prysm/v4/proto/prysm/v1alpha1/validator-client"
|
||||
slashing "github.com/prysmaticlabs/prysm/v4/validator/slashing-protection-history"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
// ExportSlashingProtection handles the rpc call returning the json slashing history.
|
||||
// The format of the export follows the EIP-3076 standard which makes it
|
||||
// easy to migrate machines or Ethereum consensus clients.
|
||||
//
|
||||
// Steps:
|
||||
// 1. Call the function which exports the data from
|
||||
// the validator's db into an EIP standard slashing protection format.
|
||||
// 2. Format and send JSON in the response.
|
||||
//
|
||||
// DEPRECATED: Prysm Web UI and associated endpoints will be fully removed in a future hard fork. Use the Keymanager APIs if an API is required.
|
||||
func (s *Server) ExportSlashingProtection(ctx context.Context, _ *empty.Empty) (*pb.ExportSlashingProtectionResponse, error) {
|
||||
if s.valDB == nil {
|
||||
return nil, errors.New("err finding validator database at path")
|
||||
}
|
||||
|
||||
eipJSON, err := slashing.ExportStandardProtectionJSON(ctx, s.valDB)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not export slashing protection history")
|
||||
}
|
||||
|
||||
encoded, err := json.MarshalIndent(eipJSON, "", "\t")
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not JSON marshal slashing protection history")
|
||||
}
|
||||
|
||||
return &pb.ExportSlashingProtectionResponse{
|
||||
File: string(encoded),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ImportSlashingProtection reads an input slashing protection EIP-3076
|
||||
// standard JSON string and inserts the data into validator DB.
|
||||
//
|
||||
// Read the JSON string passed through rpc, then call the func
|
||||
// which actually imports the data from the JSON file into our database. Use the Keymanager APIs if an API is required.
|
||||
// DEPRECATED: Prysm Web UI and associated endpoints will be fully removed in a future hard fork.
|
||||
func (s *Server) ImportSlashingProtection(ctx context.Context, req *pb.ImportSlashingProtectionRequest) (*emptypb.Empty, error) {
|
||||
if s.valDB == nil {
|
||||
return nil, errors.New("err finding validator database at path")
|
||||
}
|
||||
|
||||
if req.SlashingProtectionJson == "" {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "empty slashing_protection json specified")
|
||||
}
|
||||
enc := []byte(req.SlashingProtectionJson)
|
||||
|
||||
buf := bytes.NewBuffer(enc)
|
||||
if err := slashing.ImportStandardProtectionJSON(ctx, s.valDB, buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Info("Slashing protection JSON successfully imported")
|
||||
return &empty.Empty{}, nil
|
||||
}
|
||||
@@ -89,3 +89,10 @@ type GetFeeRecipientByPubkeyResponse struct {
|
||||
type SetFeeRecipientByPubkeyRequest struct {
|
||||
Ethaddress string `json:"ethaddress"`
|
||||
}
|
||||
|
||||
type ImportSlashingProtectionRequest struct {
|
||||
SlashingProtectionJson string `json:"slashing_protection_json"`
|
||||
}
|
||||
type ExportSlashingProtectionResponse struct {
|
||||
File string `json:"file"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user