From e7dbc6957133560258aaf96657c82a78642f3255 Mon Sep 17 00:00:00 2001 From: Giulio rebuffo Date: Thu, 31 Aug 2023 01:18:12 +0200 Subject: [PATCH] Middleware for Caplin Beacon API (#8103) --- cl/beacon/handler/format.go | 64 ++++++++++++++++++++++++++++++++++++ cl/beacon/handler/genesis.go | 34 +++++++++++-------- cl/beacon/handler/handler.go | 15 ++++++--- cl/beacon/middleware.go | 19 +++++++++++ cl/beacon/router.go | 2 +- cl/clparams/version.go | 17 ++++++++++ 6 files changed, 132 insertions(+), 19 deletions(-) create mode 100644 cl/beacon/handler/format.go create mode 100644 cl/beacon/middleware.go diff --git a/cl/beacon/handler/format.go b/cl/beacon/handler/format.go new file mode 100644 index 000000000..547cd1b17 --- /dev/null +++ b/cl/beacon/handler/format.go @@ -0,0 +1,64 @@ +package handler + +import ( + "encoding/json" + "io" + "net/http" + "strings" + + "github.com/ledgerwatch/erigon-lib/types/ssz" + "github.com/ledgerwatch/erigon/cl/clparams" + "github.com/ledgerwatch/log/v3" +) + +type BeaconResponse struct { + Finalized *bool `json:"finalized,omitempty"` + Version string `json:"version,omitempty"` + ExecutionOptimistic *bool `json:"execution_optimistic,omitempty"` + Data ssz.Marshaler `json:"data,omitempty"` +} + +// In case of it being a json we need to also expose finalization, version, etc... +type beaconHandlerFn func(r *http.Request) (data ssz.Marshaler, finalized *bool, version *clparams.StateVersion, httpStatus int, err error) + +func beaconHandlerWrapper(fn beaconHandlerFn) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + accept := r.Header.Get("Accept") + isSSZ := !strings.Contains(accept, "application/json") && strings.Contains(accept, "application/stream-octect") + data, finalized, version, httpStatus, err := fn(r) + if err != nil { + w.WriteHeader(httpStatus) + io.WriteString(w, err.Error()) + log.Debug("[Beacon API] failed", "method", r.Method, "err", err, "ssz", isSSZ) + return + } + + if isSSZ { + // SSZ encoding + encoded, err := data.EncodeSSZ(nil) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + io.WriteString(w, err.Error()) + log.Debug("[Beacon API] failed", "method", r.Method, "err", err, "accepted", accept) + return + } + w.Header().Set("Content-Type", "application/octet-stream") + w.WriteHeader(httpStatus) + w.Write(encoded) + log.Debug("[Beacon API] genesis handler failed", err) + return + } + resp := &BeaconResponse{Data: data} + if version != nil { + resp.Version = clparams.ClVersionToString(*version) + } + if finalized != nil { + resp.ExecutionOptimistic = new(bool) + resp.Finalized = new(bool) + *resp.Finalized = *finalized + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(httpStatus) + json.NewEncoder(w).Encode(resp) + } +} diff --git a/cl/beacon/handler/genesis.go b/cl/beacon/handler/genesis.go index 0e4bf61de..1143610c5 100644 --- a/cl/beacon/handler/genesis.go +++ b/cl/beacon/handler/genesis.go @@ -1,14 +1,15 @@ package handler import ( - "encoding/json" - "io" + "errors" "net/http" "github.com/ledgerwatch/erigon-lib/common" + "github.com/ledgerwatch/erigon-lib/types/ssz" "github.com/ledgerwatch/erigon/cl/beacon/types" + "github.com/ledgerwatch/erigon/cl/clparams" "github.com/ledgerwatch/erigon/cl/fork" - "github.com/ledgerwatch/log/v3" + ssz2 "github.com/ledgerwatch/erigon/cl/ssz" ) type genesisReponse struct { @@ -17,26 +18,33 @@ type genesisReponse struct { GenesisForkVersion types.Bytes4 `json:"genesis_fork_version,omitempty"` } -func (a *ApiHandler) getGenesis(w http.ResponseWriter, _ *http.Request) { +func (g *genesisReponse) EncodeSSZ(buf []byte) ([]byte, error) { + return ssz2.MarshalSSZ(buf, g.GenesisTime, g.GenesisValidatorRoot[:], g.GenesisForkVersion[:]) +} + +func (g *genesisReponse) EncodingSizeSSZ() int { + return 44 +} + +func (a *ApiHandler) getGenesis(r *http.Request) (data ssz.Marshaler, finalized *bool, version *clparams.StateVersion, httpStatus int, err error) { if a.genesisCfg == nil { - w.WriteHeader(http.StatusNotFound) - io.WriteString(w, "Genesis Config is missing") + err = errors.New("Genesis Config is missing") + httpStatus = http.StatusNotFound return } digest, err := fork.ComputeForkDigest(a.beaconChainCfg, a.genesisCfg) if err != nil { - w.WriteHeader(http.StatusInternalServerError) - io.WriteString(w, "Failed to compute fork digest") - log.Error("[Beacon API] genesis handler failed", err) + err = errors.New("Failed to compute fork digest") + httpStatus = http.StatusInternalServerError return } - w.Header().Set("Content-Type", "Application/json") - w.WriteHeader(http.StatusAccepted) - json.NewEncoder(w).Encode(genesisReponse{ + data = &genesisReponse{ GenesisTime: a.genesisCfg.GenesisTime, GenesisValidatorRoot: a.genesisCfg.GenesisValidatorRoot, GenesisForkVersion: types.Bytes4(digest), - }) + } + httpStatus = http.StatusAccepted + return } diff --git a/cl/beacon/handler/handler.go b/cl/beacon/handler/handler.go index 9ad6b7526..bca67f169 100644 --- a/cl/beacon/handler/handler.go +++ b/cl/beacon/handler/handler.go @@ -35,12 +35,17 @@ func (a *ApiHandler) init() { r.Route("/v1", func(r chi.Router) { r.Get("/events", nil) r.Route("/beacon", func(r chi.Router) { - r.Get("/headers/{tag}", nil) // otterscan - r.Get("/blocks/{block_id}", a.getBlock) //otterscan - r.Get("/blocks/{block_id}/root", a.getBlockRoot) //otterscan - r.Get("/genesis", a.getGenesis) + r.Route("/headers", func(r chi.Router) { + r.Get("/", nil) + r.Get("/{block_id}", nil) + }) + r.Route("/blocks", func(r chi.Router) { + r.Post("/", nil) + r.Get("/{block_id}", a.getBlock) + r.Get("/block_id}/root", a.getBlockRoot) + }) + r.Get("/genesis", beaconHandlerWrapper(a.getGenesis)) r.Post("/binded_blocks", nil) - r.Post("/blocks", nil) r.Route("/pool", func(r chi.Router) { r.Post("/attestations", nil) r.Post("/sync_committees", nil) diff --git a/cl/beacon/middleware.go b/cl/beacon/middleware.go new file mode 100644 index 000000000..be2a1bcaf --- /dev/null +++ b/cl/beacon/middleware.go @@ -0,0 +1,19 @@ +package beacon + +import ( + "fmt" + "net/http" +) + +func newBeaconMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + contentType := r.Header.Get("Content-Type") + if contentType != "application/json" && contentType != "" { + fmt.Println(contentType) + http.Error(w, "Content-Type header must be application/json", http.StatusUnsupportedMediaType) + return + } + + next.ServeHTTP(w, r) + }) +} diff --git a/cl/beacon/router.go b/cl/beacon/router.go index 255454fb6..7514681f0 100644 --- a/cl/beacon/router.go +++ b/cl/beacon/router.go @@ -23,7 +23,7 @@ type RouterConfiguration struct { func ListenAndServe(api *handler.ApiHandler, routerCfg *RouterConfiguration) { listener, err := net.Listen(routerCfg.Protocol, routerCfg.Address) server := &http.Server{ - Handler: api, + Handler: newBeaconMiddleware(api), ReadTimeout: routerCfg.ReadTimeTimeout, IdleTimeout: routerCfg.IdleTimeout, WriteTimeout: routerCfg.IdleTimeout, diff --git a/cl/clparams/version.go b/cl/clparams/version.go index 6a1e33b74..72884406f 100644 --- a/cl/clparams/version.go +++ b/cl/clparams/version.go @@ -27,3 +27,20 @@ func StringToClVersion(s string) StateVersion { panic("unsupported fork version: " + s) } } + +func ClVersionToString(s StateVersion) string { + switch s { + case Phase0Version: + return "phase0" + case AltairVersion: + return "altair" + case BellatrixVersion: + return "bellatrix" + case CapellaVersion: + return "capella" + case DenebVersion: + return "deneb" + default: + panic("unsupported fork version") + } +}