diff --git a/README.md b/README.md index ef737be3..695356eb 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ For `alt-da` clients running on Optimism, the following commitment schema is sup Both `keccak256` (i.e, S3 storage using hash of pre-image for commitment value) and `generic` (i.e, EigenDA) are supported to ensure cross-compatibility with alt-da storage backends if desired by a rollup operator. -OP Stack itself only has a conception of the first byte (`commit type`) and does no semantical interpretation of any subsequent bytes within the encoding. The `da layer type` byte for EigenDA is always `0x0`. However it is currently unused by OP Stack with name space values still being actively [discussed](https://github.com/ethereum-optimism/specs/discussions/135#discussioncomment-9271282). +OP Stack itself only has a conception of the first byte (`commit type`) and does no semantical interpretation of any subsequent bytes within the encoding. The `da layer type` byte for EigenDA is always `0x00`. However it is currently unused by OP Stack with name space values still being actively [discussed](https://github.com/ethereum-optimism/specs/discussions/135#discussioncomment-9271282). ### Simple Commitment Mode For simple clients communicating with proxy (e.g, arbitrum nitro), the following commitment schema is supported: diff --git a/commitments/mode.go b/commitments/mode.go index ef106ad5..24903c8b 100644 --- a/commitments/mode.go +++ b/commitments/mode.go @@ -1,7 +1,6 @@ package commitments import ( - "encoding/hex" "fmt" ) @@ -32,36 +31,6 @@ func StringToCommitmentMode(s string) (CommitmentMode, error) { } } -func StringToDecodedCommitment(key string, c CommitmentMode) ([]byte, error) { - offset := 0 - if key[:2] == "0x" { - offset = 2 - } - - b, err := hex.DecodeString(key[offset:]) - if err != nil { - return nil, err - } - - if len(b) < 3 { - return nil, fmt.Errorf("commitment is too short") - } - - switch c { - case OptimismKeccak: // [op_type, ...] - return b[1:], nil - - case OptimismGeneric: // [op_type, da_provider, cert_version, ...] - return b[3:], nil - - case SimpleCommitmentMode: // [cert_version, ...] - return b[1:], nil - - default: - return nil, fmt.Errorf("unknown commitment type") - } -} - func EncodeCommitment(b []byte, c CommitmentMode) ([]byte, error) { switch c { case OptimismKeccak: diff --git a/e2e/server_test.go b/e2e/server_test.go index f7a643eb..2854f65a 100644 --- a/e2e/server_test.go +++ b/e2e/server_test.go @@ -199,21 +199,19 @@ func TestProxyClientServerIntegration(t *testing.T) { require.NoError(t, err) }) - t.Run("get data edge cases", func(t *testing.T) { - testCert := []byte("") + t.Run("get data edge cases - unsupported version byte 01", func(t *testing.T) { + testCert := []byte{1} _, err := daClient.GetData(ts.Ctx, testCert) require.Error(t, err) - assert.True(t, strings.Contains(err.Error(), - "commitment is too short") && !isNilPtrDerefPanic(err.Error())) - - testCert = []byte{1} - _, err = daClient.GetData(ts.Ctx, testCert) - require.Error(t, err) - assert.True(t, strings.Contains(err.Error(), - "commitment is too short") && !isNilPtrDerefPanic(err.Error())) + assert.True(t, + strings.Contains(err.Error(), "unsupported version byte 01") && !isNilPtrDerefPanic(err.Error())) + }) - testCert = []byte(e2e.RandString(10000)) - _, err = daClient.GetData(ts.Ctx, testCert) + t.Run("get data edge cases - huge cert", func(t *testing.T) { + // TODO: we need to add the 0 version byte at the beginning. + // should this not be done automatically by the simple_commitment client? + testCert := append([]byte{0}, []byte(e2e.RandString(10000))...) + _, err := daClient.GetData(ts.Ctx, testCert) require.Error(t, err) assert.True(t, strings.Contains(err.Error(), "failed to decode DA cert to RLP format: rlp: expected input list for verify.Certificate") && diff --git a/go.mod b/go.mod index 18e7e91d..c11506e8 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/ethereum/go-ethereum v1.14.11 github.com/go-redis/redis/v8 v8.11.5 github.com/golang/mock v1.2.0 + github.com/gorilla/mux v1.8.0 github.com/joho/godotenv v1.5.1 github.com/minio/minio-go/v7 v7.0.77 github.com/prometheus/client_golang v1.20.4 diff --git a/go.sum b/go.sum index b405f832..3783f568 100644 --- a/go.sum +++ b/go.sum @@ -402,6 +402,7 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= diff --git a/server/errors.go b/server/errors.go index 31e0faf6..77ff759b 100644 --- a/server/errors.go +++ b/server/errors.go @@ -19,7 +19,6 @@ func (me MetaError) Error() string { me.Meta.CertVersion) } -// NewMetaError creates a new MetaError -func NewMetaError(err error, meta commitments.CommitmentMeta) MetaError { - return MetaError{Err: err, Meta: meta} +func (me MetaError) Unwrap() error { + return me.Err } diff --git a/server/handlers.go b/server/handlers.go new file mode 100644 index 00000000..9d408190 --- /dev/null +++ b/server/handlers.go @@ -0,0 +1,209 @@ +package server + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "io" + "net/http" + + "github.com/Layr-Labs/eigenda-proxy/commitments" + "github.com/Layr-Labs/eigenda-proxy/store" + "github.com/gorilla/mux" +) + +func (svr *Server) handleHealth(w http.ResponseWriter, _ *http.Request) error { + w.WriteHeader(http.StatusOK) + return nil +} + +// ================================================================================================= +// GET ROUTES +// ================================================================================================= + +// handleGetSimpleCommitment handles the GET request for simple commitments. +func (svr *Server) handleGetSimpleCommitment(w http.ResponseWriter, r *http.Request) error { + versionByte, err := parseVersionByte(w, r) + if err != nil { + return fmt.Errorf("error parsing version byte: %w", err) + } + commitmentMeta := commitments.CommitmentMeta{ + Mode: commitments.SimpleCommitmentMode, + CertVersion: versionByte, + } + + rawCommitmentHex, ok := mux.Vars(r)[routingVarNameRawCommitmentHex] + if !ok { + return fmt.Errorf("commitment not found in path: %s", r.URL.Path) + } + commitment, err := hex.DecodeString(rawCommitmentHex) + if err != nil { + return fmt.Errorf("failed to decode commitment %s: %w", rawCommitmentHex, err) + } + + return svr.handleGetShared(r.Context(), w, commitment, commitmentMeta) +} + +// handleGetOPKeccakCommitment handles the GET request for optimism keccak commitments. +func (svr *Server) handleGetOPKeccakCommitment(w http.ResponseWriter, r *http.Request) error { + // TODO: do we use a version byte in OPKeccak commitments? README seems to say so, but server_test didn't + // versionByte, err := parseVersionByte(r) + // if err != nil { + // err = fmt.Errorf("error parsing version byte: %w", err) + // http.Error(w, err.Error(), http.StatusBadRequest) + // return err + // } + commitmentMeta := commitments.CommitmentMeta{ + Mode: commitments.OptimismKeccak, + CertVersion: byte(commitments.CertV0), + } + + rawCommitmentHex, ok := mux.Vars(r)[routingVarNameRawCommitmentHex] + if !ok { + return fmt.Errorf("commitment not found in path: %s", r.URL.Path) + } + commitment, err := hex.DecodeString(rawCommitmentHex) + if err != nil { + return fmt.Errorf("failed to decode commitment %s: %w", rawCommitmentHex, err) + } + + return svr.handleGetShared(r.Context(), w, commitment, commitmentMeta) +} + +// handleGetOPGenericCommitment handles the GET request for optimism generic commitments. +func (svr *Server) handleGetOPGenericCommitment(w http.ResponseWriter, r *http.Request) error { + versionByte, err := parseVersionByte(w, r) + if err != nil { + return fmt.Errorf("error parsing version byte: %w", err) + } + commitmentMeta := commitments.CommitmentMeta{ + Mode: commitments.OptimismGeneric, + CertVersion: versionByte, + } + + rawCommitmentHex, ok := mux.Vars(r)[routingVarNameRawCommitmentHex] + if !ok { + return fmt.Errorf("commitment not found in path: %s", r.URL.Path) + } + commitment, err := hex.DecodeString(rawCommitmentHex) + if err != nil { + return fmt.Errorf("failed to decode commitment %s: %w", rawCommitmentHex, err) + } + + return svr.handleGetShared(r.Context(), w, commitment, commitmentMeta) +} + +func (svr *Server) handleGetShared(ctx context.Context, w http.ResponseWriter, comm []byte, meta commitments.CommitmentMeta) error { + svr.log.Info("Processing GET request", "commitment", hex.EncodeToString(comm), "commitmentMeta", meta) + input, err := svr.router.Get(ctx, comm, meta.Mode) + if err != nil { + err = MetaError{ + Err: fmt.Errorf("get request failed with commitment %v: %w", comm, err), + Meta: meta, + } + if errors.Is(err, ErrNotFound) { + http.Error(w, err.Error(), http.StatusNotFound) + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + return err + } + + svr.writeResponse(w, input) + return nil +} + +// ================================================================================================= +// POST ROUTES +// ================================================================================================= + +// handlePostSimpleCommitment handles the POST request for simple commitments. +func (svr *Server) handlePostSimpleCommitment(w http.ResponseWriter, r *http.Request) error { + commitmentMeta := commitments.CommitmentMeta{ + Mode: commitments.SimpleCommitmentMode, + CertVersion: byte(commitments.CertV0), // TODO: hardcoded for now + } + return svr.handlePostShared(w, r, nil, commitmentMeta) +} + +// handlePostOPKeccakCommitment handles the POST request for optimism keccak commitments. +func (svr *Server) handlePostOPKeccakCommitment(w http.ResponseWriter, r *http.Request) error { + // TODO: do we use a version byte in OPKeccak commitments? README seems to say so, but server_test didn't + // versionByte, err := parseVersionByte(r) + // if err != nil { + // err = fmt.Errorf("error parsing version byte: %w", err) + // http.Error(w, err.Error(), http.StatusBadRequest) + // return err + // } + commitmentMeta := commitments.CommitmentMeta{ + Mode: commitments.OptimismKeccak, + CertVersion: byte(commitments.CertV0), + } + + rawCommitmentHex, ok := mux.Vars(r)[routingVarNameRawCommitmentHex] + if !ok { + return fmt.Errorf("commitment not found in path: %s", r.URL.Path) + } + commitment, err := hex.DecodeString(rawCommitmentHex) + if err != nil { + return fmt.Errorf("failed to decode commitment %s: %w", rawCommitmentHex, err) + } + + return svr.handlePostShared(w, r, commitment, commitmentMeta) +} + +// handlePostOPGenericCommitment handles the POST request for optimism generic commitments. +func (svr *Server) handlePostOPGenericCommitment(w http.ResponseWriter, r *http.Request) error { + commitmentMeta := commitments.CommitmentMeta{ + Mode: commitments.OptimismGeneric, + CertVersion: byte(commitments.CertV0), // TODO: hardcoded for now + } + return svr.handlePostShared(w, r, nil, commitmentMeta) +} + +func (svr *Server) handlePostShared(w http.ResponseWriter, r *http.Request, comm []byte, meta commitments.CommitmentMeta) error { + svr.log.Info("Processing POST request", "commitment", hex.EncodeToString(comm), "commitmentMeta", meta) + input, err := io.ReadAll(r.Body) + if err != nil { + err = MetaError{ + Err: fmt.Errorf("failed to read request body: %w", err), + Meta: meta, + } + http.Error(w, err.Error(), http.StatusBadRequest) + return err + } + + commitment, err := svr.router.Put(r.Context(), meta.Mode, comm, input) + if err != nil { + err = MetaError{ + Err: fmt.Errorf("put request failed with commitment %v (commitment mode %v): %w", comm, meta.Mode, err), + Meta: meta, + } + if errors.Is(err, store.ErrEigenDAOversizedBlob) || errors.Is(err, store.ErrProxyOversizedBlob) { + // we add here any error that should be returned as a 400 instead of a 500. + // currently only includes oversized blob requests + http.Error(w, err.Error(), http.StatusBadRequest) + } else { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + return err + } + + responseCommit, err := commitments.EncodeCommitment(commitment, meta.Mode) + if err != nil { + err = MetaError{ + Err: fmt.Errorf("failed to encode commitment %v (commitment mode %v): %w", commitment, meta.Mode, err), + Meta: meta, + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return err + } + + svr.log.Info(fmt.Sprintf("response commitment: %x\n", responseCommit)) + // write commitment to resp body if not in OptimismKeccak mode + if meta.Mode != commitments.OptimismKeccak { + svr.writeResponse(w, responseCommit) + } + return nil +} diff --git a/server/middleware.go b/server/middleware.go new file mode 100644 index 00000000..3b3145ea --- /dev/null +++ b/server/middleware.go @@ -0,0 +1,66 @@ +package server + +import ( + "errors" + "fmt" + "net/http" + "time" + + "github.com/Layr-Labs/eigenda-proxy/commitments" + "github.com/Layr-Labs/eigenda-proxy/metrics" + "github.com/ethereum/go-ethereum/log" +) + +// withMetrics is a middleware that records metrics for the route path. +func withMetrics( + handleFn func(http.ResponseWriter, *http.Request) error, + m metrics.Metricer, + mode commitments.CommitmentMode, +) func(http.ResponseWriter, *http.Request) error { + return func(w http.ResponseWriter, r *http.Request) error { + recordDur := m.RecordRPCServerRequest(r.Method) + + err := handleFn(w, r) + if err != nil { + var metaErr MetaError + if errors.As(err, &metaErr) { + recordDur(w.Header().Get("status"), string(metaErr.Meta.Mode), string(metaErr.Meta.CertVersion)) + } else { + recordDur(w.Header().Get("status"), string("unknown"), string("unknown")) + } + return err + } + // we assume that every route will set the status header + versionByte, err := parseVersionByte(w, r) + if err != nil { + return fmt.Errorf("metrics middleware: error parsing version byte: %w", err) + } + recordDur(w.Header().Get("status"), string(mode), string(versionByte)) + return nil + } +} + +// withLogging is a middleware that logs information related to each request. +// It does not write anything to the response, that is the job of the handlers. +// Currently we cannot log the status code because go's default ResponseWriter interface does not expose it. +// TODO: implement a ResponseWriter wrapper that saves the status code: see https://github.com/golang/go/issues/18997 +func withLogging( + handleFn func(http.ResponseWriter, *http.Request) error, + log log.Logger, +) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + err := handleFn(w, r) + var metaErr MetaError + //nolint:gocritic // ifElseChain is not a good replacement with errors.As + if errors.As(err, &metaErr) { + log.Info("request", "method", r.Method, "url", r.URL, "duration", time.Since(start), + "err", err, "status", w.Header().Get("status"), + "commitment_mode", metaErr.Meta.Mode, "cert_version", metaErr.Meta.CertVersion) + } else if err != nil { + log.Info("request", "method", r.Method, "url", r.URL, "duration", time.Since(start), "err", err) + } else { + log.Info("request", "method", r.Method, "url", r.URL, "duration", time.Since(start)) + } + } +} diff --git a/server/routing.go b/server/routing.go new file mode 100644 index 00000000..ca235935 --- /dev/null +++ b/server/routing.go @@ -0,0 +1,86 @@ +package server + +import ( + "fmt" + "net/http" + + "github.com/Layr-Labs/eigenda-proxy/commitments" + "github.com/gorilla/mux" +) + +const ( + routingVarNameRawCommitmentHex = "raw_commitment_hex" + routingVarNameVersionByteHex = "version_byte_hex" + routingVarNameCommitTypeByteHex = "commit_type_byte_hex" +) + +func (svr *Server) registerRoutes(r *mux.Router) { + subrouterGET := r.Methods("GET").PathPrefix("/get").Subrouter() + // simple commitments (for nitro) + subrouterGET.HandleFunc("/"+ + "{optional_prefix:(?:0x)?}"+ // commitments can be prefixed with 0x + "{"+routingVarNameVersionByteHex+":[0-9a-fA-F]{2}}"+ // should always be 0x00 for now but we let others through to return a 404 + "{"+routingVarNameRawCommitmentHex+":[0-9a-fA-F]*}", + withLogging(withMetrics(svr.handleGetSimpleCommitment, svr.m, commitments.SimpleCommitmentMode), svr.log), + ).Queries("commitment_mode", "simple") + // op keccak256 commitments (write to S3) + subrouterGET.HandleFunc("/"+ + "{optional_prefix:(?:0x)?}"+ // commitments can be prefixed with 0x + "{"+routingVarNameCommitTypeByteHex+":00}"+ // 00 for keccak256 commitments + // we don't use version_byte for keccak commitments, because not expecting keccak commitments to change, + // but perhaps we should (in case we want a v2 to use another hash for eg?) + // "{version_byte_hex:[0-9a-fA-F]{2}}"+ // should always be 0x00 for now but we let others through to return a 404 + "{"+routingVarNameRawCommitmentHex+"}", + withLogging(withMetrics(svr.handleGetOPKeccakCommitment, svr.m, commitments.OptimismKeccak), svr.log), + ) + // op generic commitments (write to EigenDA) + subrouterGET.HandleFunc("/"+ + "{optional_prefix:(?:0x)?}"+ // commitments can be prefixed with 0x + "{"+routingVarNameCommitTypeByteHex+":01}"+ // 01 for generic commitments + "{da_layer_byte:[0-9a-fA-F]{2}}"+ // should always be 0x00 for eigenDA but we let others through to return a 404 + "{"+routingVarNameVersionByteHex+":[0-9a-fA-F]{2}}"+ // should always be 0x00 for now but we let others through to return a 404 + "{"+routingVarNameRawCommitmentHex+"}", + withLogging(withMetrics(svr.handleGetOPGenericCommitment, svr.m, commitments.OptimismGeneric), svr.log), + ) + // unrecognized op commitment type (not 00 or 01) + subrouterGET.HandleFunc("/"+ + "{optional_prefix:(?:0x)?}"+ // commitments can be prefixed with 0x + "{"+routingVarNameCommitTypeByteHex+":[0-9a-fA-F]{2}}", + func(w http.ResponseWriter, r *http.Request) { + svr.log.Info("unsupported commitment type", routingVarNameCommitTypeByteHex, mux.Vars(r)[routingVarNameCommitTypeByteHex]) + commitType := mux.Vars(r)[routingVarNameCommitTypeByteHex] + http.Error(w, fmt.Sprintf("unsupported commitment type %s", commitType), http.StatusBadRequest) + }, + ).MatcherFunc(notCommitmentModeSimple) + + subrouterPOST := r.Methods("POST").PathPrefix("/put").Subrouter() + // simple commitments (for nitro) + subrouterPOST.HandleFunc("", // commitment is calculated by the server using the body data + withLogging(withMetrics(svr.handlePostSimpleCommitment, svr.m, commitments.SimpleCommitmentMode), svr.log), + ).Queries("commitment_mode", "simple") + // op keccak256 commitments (write to S3) + subrouterPOST.HandleFunc("/"+ + "{optional_prefix:(?:0x)?}"+ // commitments can be prefixed with 0x + // TODO: do we need this 0x00 byte? keccak commitments are the only ones that have anything after /put/ + "{"+routingVarNameCommitTypeByteHex+":00}"+ // 00 for keccak256 commitments + // we don't use version_byte for keccak commitments, because not expecting keccak commitments to change, + // but perhaps we should (in case we want a v2 to use another hash for eg?) + // "{version_byte_hex:[0-9a-fA-F]{2}}"+ // should always be 0x00 for now but we let others through to return a 404 + "{"+routingVarNameRawCommitmentHex+"}", + withLogging(withMetrics(svr.handlePostOPKeccakCommitment, svr.m, commitments.OptimismKeccak), svr.log), + ) + // op generic commitments (write to EigenDA) + subrouterPOST.HandleFunc("", // commitment is calculated by the server using the body data + withLogging(withMetrics(svr.handlePostOPGenericCommitment, svr.m, commitments.OptimismGeneric), svr.log), + ) + subrouterPOST.HandleFunc("/", // commitment is calculated by the server using the body data + withLogging(withMetrics(svr.handlePostOPGenericCommitment, svr.m, commitments.OptimismGeneric), svr.log), + ) + + r.HandleFunc("/health", withLogging(svr.handleHealth, svr.log)).Methods("GET") +} + +func notCommitmentModeSimple(r *http.Request, _ *mux.RouteMatch) bool { + commitmentMode := r.URL.Query().Get("commitment_mode") + return commitmentMode == "" || commitmentMode != "simple" +} diff --git a/server/routing_test.go b/server/routing_test.go new file mode 100644 index 00000000..89a5e39a --- /dev/null +++ b/server/routing_test.go @@ -0,0 +1,105 @@ +package server + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Layr-Labs/eigenda-proxy/metrics" + "github.com/Layr-Labs/eigenda-proxy/mocks" + "github.com/ethereum/go-ethereum/log" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" +) + +// TestRouting tests that the routes were properly encoded. +// We should eventually replace this with autogenerated specmatic tests over an openapi spec. +func TestRouting(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockRouter := mocks.NewMockIRouter(ctrl) + + m := metrics.NewMetrics("default") + server := NewServer("localhost", 8080, mockRouter, log.New(), m) + err := server.Start() + require.NoError(t, err) + + tests := []struct { + name string + url string + method string + body []byte + expectedCode int + expectedBody string + }{ + { + name: "Not Found - Must have a commitment key", + url: "/get/0x", + method: http.MethodGet, + body: nil, + // originally we returned 400 for these, but now we return 404 because + // not having a commitment is not a valid route. + expectedCode: http.StatusNotFound, + expectedBody: "404 page not found\n", + }, + { + name: "Not Found - Op Mode InvalidCommitmentKey", + url: "/get/0x1", + body: nil, + // originally we returned 400 for these, but now we return 404 because + // not having a commitment is not a valid route. + expectedCode: http.StatusNotFound, + expectedBody: "404 page not found\n", + }, + { + name: "Not Found - Op Mode InvalidCommitmentKey", + url: "/get/0x999", + body: nil, + // originally we returned 400 for these, but now we return 404 because + // not having a commitment is not a valid route. + expectedCode: http.StatusNotFound, + expectedBody: "404 page not found\n", + }, + { + name: "Not Found OP Keccak256 - TooShortCommitmentKey", + url: "/put/0x", + method: http.MethodPut, + body: []byte("some data"), + // originally we returned 400 for these, but now we return 404 because + // not having a commitment is not a valid route. + expectedCode: http.StatusNotFound, + expectedBody: "404 page not found\n", + }, + { + name: "Not Found OP Keccak256 - TooShortCommitmentKey", + url: "/put/0x1", + body: []byte("some data"), + // originally we returned 400 for these, but now we return 404 because + // not having a commitment is not a valid route. + expectedCode: http.StatusNotFound, + expectedBody: "404 page not found\n", + }, + { + name: "Not Found OP Keccak256 - InvalidCommitmentPrefixBytes", + url: fmt.Sprintf("/put/0x999%s", testCommitStr), + body: []byte("some data"), + // originally we returned 400 for these, but now we return 404 because + // not having a commitment is not a valid route. + expectedCode: http.StatusNotFound, + expectedBody: "404 page not found\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(tt.method, tt.url, nil) + rec := httptest.NewRecorder() + server.httpServer.Handler.ServeHTTP(rec, req) + + require.Equal(t, tt.expectedCode, rec.Code) + require.Equal(t, tt.expectedBody, rec.Body.String()) + + }) + } +} diff --git a/server/server.go b/server/server.go index a5e3e050..3694d329 100644 --- a/server/server.go +++ b/server/server.go @@ -2,21 +2,19 @@ package server import ( "context" + "encoding/hex" "errors" "fmt" - "io" "net" "net/http" - "path" "strconv" - "strings" "time" "github.com/Layr-Labs/eigenda-proxy/commitments" "github.com/Layr-Labs/eigenda-proxy/metrics" "github.com/Layr-Labs/eigenda-proxy/store" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/log" + "github.com/gorilla/mux" ) var ( @@ -55,57 +53,10 @@ func NewServer(host string, port int, router store.IRouter, log log.Logger, } } -// WithMetrics is a middleware that records metrics for the route path. -func WithMetrics( - handleFn func(http.ResponseWriter, *http.Request) (commitments.CommitmentMeta, error), - m metrics.Metricer, -) func(http.ResponseWriter, *http.Request) error { - return func(w http.ResponseWriter, r *http.Request) error { - recordDur := m.RecordRPCServerRequest(r.Method) - - meta, err := handleFn(w, r) - if err != nil { - var metaErr MetaError - if errors.As(err, &metaErr) { - recordDur(w.Header().Get("status"), string(metaErr.Meta.Mode), string(metaErr.Meta.CertVersion)) - } else { - recordDur(w.Header().Get("status"), string("NoCommitmentMode"), string("NoCertVersion")) - } - return err - } - // we assume that every route will set the status header - recordDur(w.Header().Get("status"), string(meta.Mode), string(meta.CertVersion)) - return nil - } -} - -// WithLogging is a middleware that logs the request method and URL. -func WithLogging( - handleFn func(http.ResponseWriter, *http.Request) error, - log log.Logger, -) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - log.Info("request", "method", r.Method, "url", r.URL) - err := handleFn(w, r) - if err != nil { // #nosec G104 - w.Write([]byte(err.Error())) //nolint:errcheck // ignore error - log.Error(err.Error()) - } - } -} - func (svr *Server) Start() error { - mux := http.NewServeMux() - - mux.HandleFunc("/get/", WithLogging(WithMetrics(svr.HandleGet, svr.m), svr.log)) - // we need to handle both: see https://github.com/ethereum-optimism/optimism/pull/12081 - // /put is for generic commitments, and /put/ for keccak256 commitments - // TODO: we should probably separate their handlers? - mux.HandleFunc("/put", WithLogging(WithMetrics(svr.HandlePut, svr.m), svr.log)) - mux.HandleFunc("/put/", WithLogging(WithMetrics(svr.HandlePut, svr.m), svr.log)) - mux.HandleFunc("/health", WithLogging(svr.Health, svr.log)) - - svr.httpServer.Handler = mux + r := mux.NewRouter() + svr.registerRoutes(r) + svr.httpServer.Handler = r listener, err := net.Listen("tcp", svr.endpoint) if err != nil { @@ -148,149 +99,13 @@ func (svr *Server) Stop() error { } return nil } -func (svr *Server) Health(w http.ResponseWriter, _ *http.Request) error { - w.WriteHeader(http.StatusOK) - return nil -} - -// HandleGet handles the GET request for commitments. -// Note: even when an error is returned, the commitment meta is still returned, -// because it is needed for metrics (see the WithMetrics middleware). -// TODO: we should change this behavior and instead use a custom error that contains the commitment meta. -func (svr *Server) HandleGet(w http.ResponseWriter, r *http.Request) (commitments.CommitmentMeta, error) { - meta, err := ReadCommitmentMeta(r) - if err != nil { - err = fmt.Errorf("invalid commitment mode: %w", err) - svr.WriteBadRequest(w, err) - return commitments.CommitmentMeta{}, err - } - key := path.Base(r.URL.Path) - comm, err := commitments.StringToDecodedCommitment(key, meta.Mode) - if err != nil { - err = fmt.Errorf("failed to decode commitment from key %v (commitment mode %v): %w", key, meta.Mode, err) - svr.WriteBadRequest(w, err) - return commitments.CommitmentMeta{}, MetaError{ - Err: err, - Meta: meta, - } - } - - input, err := svr.router.Get(r.Context(), comm, meta.Mode) - if err != nil { - err = fmt.Errorf("get request failed with commitment %v (commitment mode %v): %w", comm, meta.Mode, err) - if errors.Is(err, ErrNotFound) { - svr.WriteNotFound(w, err) - } else { - svr.WriteInternalError(w, err) - } - return commitments.CommitmentMeta{}, MetaError{ - Err: err, - Meta: meta, - } - } - - svr.WriteResponse(w, input) - return meta, nil -} - -// HandlePut handles the PUT request for commitments. -// Note: even when an error is returned, the commitment meta is still returned, -// because it is needed for metrics (see the WithMetrics middleware). -// TODO: we should change this behavior and instead use a custom error that contains the commitment meta. -func (svr *Server) HandlePut(w http.ResponseWriter, r *http.Request) (commitments.CommitmentMeta, error) { - meta, err := ReadCommitmentMeta(r) - if err != nil { - err = fmt.Errorf("invalid commitment mode: %w", err) - svr.WriteBadRequest(w, err) - return commitments.CommitmentMeta{}, err - } - // ReadCommitmentMeta function invoked inside HandlePut will not return a valid certVersion - // Current simple fix is using the hardcoded default value of 0 (also the only supported value) - //TODO: smarter decode needed when there's more than one version - meta.CertVersion = byte(commitments.CertV0) - - input, err := io.ReadAll(r.Body) - if err != nil { - err = fmt.Errorf("failed to read request body: %w", err) - svr.WriteBadRequest(w, err) - return commitments.CommitmentMeta{}, MetaError{ - Err: err, - Meta: meta, - } - } - - key := path.Base(r.URL.Path) - var comm []byte - - if len(key) > 0 && key != Put { // commitment key already provided (keccak256) - comm, err = commitments.StringToDecodedCommitment(key, meta.Mode) - if err != nil { - err = fmt.Errorf("failed to decode commitment from key %v (commitment mode %v): %w", key, meta.Mode, err) - svr.WriteBadRequest(w, err) - return commitments.CommitmentMeta{}, MetaError{ - Err: err, - Meta: meta, - } - } - } - - commitment, err := svr.router.Put(r.Context(), meta.Mode, comm, input) - if err != nil { - err = fmt.Errorf("put request failed with commitment %v (commitment mode %v): %w", comm, meta.Mode, err) - - if errors.Is(err, store.ErrEigenDAOversizedBlob) || errors.Is(err, store.ErrProxyOversizedBlob) { - // we add here any error that should be returned as a 400 instead of a 500. - // currently only includes oversized blob requests - svr.WriteBadRequest(w, err) - return meta, err - } - - svr.WriteInternalError(w, err) - return commitments.CommitmentMeta{}, MetaError{ - Err: err, - Meta: meta, - } - } - - responseCommit, err := commitments.EncodeCommitment(commitment, meta.Mode) - if err != nil { - err = fmt.Errorf("failed to encode commitment %v (commitment mode %v): %w", commitment, meta.Mode, err) - svr.WriteInternalError(w, err) - return commitments.CommitmentMeta{}, MetaError{ - Err: err, - Meta: meta, - } - } - svr.log.Info(fmt.Sprintf("response commitment: %x\n", responseCommit)) - // write commitment to resp body if not in OptimismKeccak mode - if meta.Mode != commitments.OptimismKeccak { - svr.WriteResponse(w, responseCommit) - } - return meta, nil -} - -func (svr *Server) WriteResponse(w http.ResponseWriter, data []byte) { +func (svr *Server) writeResponse(w http.ResponseWriter, data []byte) { if _, err := w.Write(data); err != nil { - svr.WriteInternalError(w, err) + http.Error(w, fmt.Sprintf("failed to write response: %v", err), http.StatusInternalServerError) } } -func (svr *Server) WriteInternalError(w http.ResponseWriter, err error) { - svr.log.Error("internal server error", "err", err) - w.WriteHeader(http.StatusInternalServerError) -} - -func (svr *Server) WriteNotFound(w http.ResponseWriter, err error) { - svr.log.Info("not found", "err", err) - w.WriteHeader(http.StatusNotFound) -} - -func (svr *Server) WriteBadRequest(w http.ResponseWriter, err error) { - svr.log.Info("bad request", "err", err) - w.WriteHeader(http.StatusBadRequest) -} - func (svr *Server) Port() int { // read from listener _, portStr, _ := net.SplitHostPort(svr.listener.Addr().String()) @@ -298,85 +113,6 @@ func (svr *Server) Port() int { return port } -// Read both commitment mode and version -func ReadCommitmentMeta(r *http.Request) (commitments.CommitmentMeta, error) { - // label requests with commitment mode and version - ct, err := ReadCommitmentMode(r) - if err != nil { - return commitments.CommitmentMeta{}, err - } - if ct == "" { - return commitments.CommitmentMeta{}, fmt.Errorf("commitment mode is empty") - } - cv, err := ReadCommitmentVersion(r, ct) - if err != nil { - // default to version 0 - return commitments.CommitmentMeta{Mode: ct, CertVersion: cv}, err - } - return commitments.CommitmentMeta{Mode: ct, CertVersion: cv}, nil -} - -func ReadCommitmentMode(r *http.Request) (commitments.CommitmentMode, error) { - query := r.URL.Query() - key := query.Get(CommitmentModeKey) - if key != "" { - return commitments.StringToCommitmentMode(key) - } - - commit := path.Base(r.URL.Path) - if len(commit) > 0 && commit != Put { // provided commitment in request params (op keccak256) - if !strings.HasPrefix(commit, "0x") { - commit = "0x" + commit - } - - decodedCommit, err := hexutil.Decode(commit) - if err != nil { - return "", err - } - - if len(decodedCommit) < 3 { - return "", fmt.Errorf("commitment is too short") - } - - switch decodedCommit[0] { - case byte(commitments.GenericCommitmentType): - return commitments.OptimismGeneric, nil - - case byte(commitments.Keccak256CommitmentType): - return commitments.OptimismKeccak, nil - - default: - return commitments.SimpleCommitmentMode, fmt.Errorf("unknown commit byte prefix") - } - } - return commitments.OptimismGeneric, nil -} - -func ReadCommitmentVersion(r *http.Request, mode commitments.CommitmentMode) (byte, error) { - commit := path.Base(r.URL.Path) - if len(commit) > 0 && commit != Put { // provided commitment in request params (op keccak256) - if !strings.HasPrefix(commit, "0x") { - commit = "0x" + commit - } - - decodedCommit, err := hexutil.Decode(commit) - if err != nil { - return 0, err - } - - if len(decodedCommit) < 3 { - return 0, fmt.Errorf("commitment is too short") - } - - if mode == commitments.OptimismGeneric || mode == commitments.SimpleCommitmentMode { - return decodedCommit[2], nil - } - - return decodedCommit[0], nil - } - return 0, nil -} - func (svr *Server) GetEigenDAStats() *store.Stats { return svr.router.GetEigenDAStore().Stats() } @@ -402,3 +138,26 @@ func (svr *Server) GetStoreStats(bt store.BackendType) (*store.Stats, error) { return nil, fmt.Errorf("store not found") } + +func parseVersionByte(w http.ResponseWriter, r *http.Request) (byte, error) { + vars := mux.Vars(r) + // decode version byte + versionByteHex, ok := vars[routingVarNameVersionByteHex] + if !ok { + return 0, fmt.Errorf("version byte not found in path: %s", r.URL.Path) + } + versionByte, err := hex.DecodeString(versionByteHex) + if err != nil { + return 0, fmt.Errorf("failed to decode version byte %s: %w", versionByteHex, err) + } + if len(versionByte) != 1 { + return 0, fmt.Errorf("version byte is not a single byte: %s", versionByteHex) + } + switch versionByte[0] { + case byte(commitments.CertV0): + return versionByte[0], nil + default: + http.Error(w, fmt.Sprintf("unsupported version byte %x", versionByte), http.StatusBadRequest) + return 0, fmt.Errorf("unsupported version byte %x", versionByte) + } +} diff --git a/server/server_test.go b/server/server_test.go index 27b9a30e..f6653238 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -7,25 +7,24 @@ import ( "net/http/httptest" "testing" - "github.com/Layr-Labs/eigenda-proxy/commitments" "github.com/Layr-Labs/eigenda-proxy/metrics" "github.com/Layr-Labs/eigenda-proxy/mocks" "github.com/ethereum/go-ethereum/log" "github.com/golang/mock/gomock" + "github.com/gorilla/mux" "github.com/stretchr/testify/require" ) const ( - genericPrefix = "\x00" + simpleCommitmentPrefix = "\x00" // [alt-da, da layer, cert version] - opKeccakPrefix = "\x00" opGenericPrefixStr = "\x01\x00\x00" testCommitStr = "9a7d4f1c3e5b8a09d1c0fa4b3f8e1d7c6b29f1e6d8c4a7b3c2d4e5f6a7b8c9d0" ) -func TestGetHandler(t *testing.T) { +func TestHandleOPCommitments(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -35,57 +34,20 @@ func TestGetHandler(t *testing.T) { server := NewServer("localhost", 8080, mockRouter, log.New(), m) tests := []struct { - name string - url string - mockBehavior func() - expectedCode int - expectedBody string - expectError bool - expectedCommitmentMeta commitments.CommitmentMeta + name string + url string + mockBehavior func() + expectedCode int + expectedBody string }{ - { - name: "Failure - Op Mode InvalidCommitmentKey", - url: "/get/0x", - mockBehavior: func() { - // Error is triggered before calling the router - }, - expectedCode: http.StatusBadRequest, - expectedBody: "", - expectError: true, - expectedCommitmentMeta: commitments.CommitmentMeta{}, - }, - { - name: "Failure - Op Mode InvalidCommitmentKey", - url: "/get/0x1", - mockBehavior: func() { - // Error is triggered before calling the router - }, - expectedCode: http.StatusBadRequest, - expectedBody: "", - expectError: true, - expectedCommitmentMeta: commitments.CommitmentMeta{}, - }, - { - name: "Failure - Op Mode InvalidCommitmentKey", - url: "/get/0x999", - mockBehavior: func() { - // Error is triggered before calling the router - }, - expectedCode: http.StatusBadRequest, - expectedBody: "", - expectError: true, - expectedCommitmentMeta: commitments.CommitmentMeta{}, - }, { name: "Failure - OP Keccak256 Internal Server Error", url: fmt.Sprintf("/get/0x00%s", testCommitStr), mockBehavior: func() { mockRouter.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("internal error")) }, - expectedCode: http.StatusInternalServerError, - expectedBody: "", - expectError: true, - expectedCommitmentMeta: commitments.CommitmentMeta{}, + expectedCode: http.StatusInternalServerError, + expectedBody: "", }, { name: "Success - OP Keccak256", @@ -93,10 +55,8 @@ func TestGetHandler(t *testing.T) { mockBehavior: func() { mockRouter.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte(testCommitStr), nil) }, - expectedCode: http.StatusOK, - expectedBody: testCommitStr, - expectError: false, - expectedCommitmentMeta: commitments.CommitmentMeta{Mode: commitments.OptimismKeccak, CertVersion: 0}, + expectedCode: http.StatusOK, + expectedBody: testCommitStr, }, { name: "Failure - OP Alt-DA Internal Server Error", @@ -104,10 +64,8 @@ func TestGetHandler(t *testing.T) { mockBehavior: func() { mockRouter.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("internal error")) }, - expectedCode: http.StatusInternalServerError, - expectedBody: "", - expectError: true, - expectedCommitmentMeta: commitments.CommitmentMeta{}, + expectedCode: http.StatusInternalServerError, + expectedBody: "", }, { name: "Success - OP Alt-DA", @@ -115,10 +73,8 @@ func TestGetHandler(t *testing.T) { mockBehavior: func() { mockRouter.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte(testCommitStr), nil) }, - expectedCode: http.StatusOK, - expectedBody: testCommitStr, - expectError: false, - expectedCommitmentMeta: commitments.CommitmentMeta{Mode: commitments.OptimismGeneric, CertVersion: 0}, + expectedCode: http.StatusOK, + expectedBody: testCommitStr, }, } @@ -129,35 +85,22 @@ func TestGetHandler(t *testing.T) { req := httptest.NewRequest(http.MethodGet, tt.url, nil) rec := httptest.NewRecorder() - meta, err := server.HandleGet(rec, req) - if tt.expectError { - require.Error(t, err) - } else { - require.NoError(t, err) - } + // To add the vars to the context, + // we need to create a router through which we can pass the request. + r := mux.NewRouter() + server.registerRoutes(r) + r.ServeHTTP(rec, req) require.Equal(t, tt.expectedCode, rec.Code) - require.Equal(t, tt.expectedCommitmentMeta, meta) - require.Equal(t, tt.expectedBody, rec.Body.String()) - - }) - } - for _, tt := range tests { - t.Run(tt.name+"/CheckMiddlewaresNoPanic", func(_ *testing.T) { - tt.mockBehavior() + // We don't test for bodies because it's a specific error message + // that contains a lot of information + // require.Equal(t, tt.expectedBody, rec.Body.String()) - req := httptest.NewRequest(http.MethodGet, tt.url, nil) - rec := httptest.NewRecorder() - // we also run the request through the middlewares, to make sure no panic occurs - // could happen if there's a problem with the metrics. For eg, in the past we saw - // panic: inconsistent label cardinality: expected 3 label values but got 1 in []string{"GET"} - handler := WithLogging(WithMetrics(server.HandleGet, server.m), server.log) - handler(rec, req) }) } } -func TestPutHandler(t *testing.T) { +func TestHandlerPut(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -165,51 +108,14 @@ func TestPutHandler(t *testing.T) { server := NewServer("localhost", 8080, mockRouter, log.New(), metrics.NoopMetrics) tests := []struct { - name string - url string - body []byte - mockBehavior func() - expectedCode int - expectedBody string - expectError bool - expectedCommitmentMeta commitments.CommitmentMeta + name string + url string + body []byte + mockBehavior func() + expectedCode int + expectedBody string + expectError bool }{ - { - name: "Failure OP Keccak256 - TooShortCommitmentKey", - url: "/put/0x", - body: []byte("some data"), - mockBehavior: func() { - // Error is triggered before calling the router - }, - expectedCode: http.StatusBadRequest, - expectedBody: "", - expectError: true, - expectedCommitmentMeta: commitments.CommitmentMeta{}, - }, - { - name: "Failure OP Keccak256 - TooShortCommitmentKey", - url: "/put/0x1", - body: []byte("some data"), - mockBehavior: func() { - // Error is triggered before calling the router - }, - expectedCode: http.StatusBadRequest, - expectedBody: "", - expectError: true, - expectedCommitmentMeta: commitments.CommitmentMeta{}, - }, - { - name: "Failure OP Keccak256 - InvalidCommitmentPrefixBytes", - url: fmt.Sprintf("/put/0x999%s", testCommitStr), - body: []byte("some data"), - mockBehavior: func() { - // Error is triggered before calling the router - }, - expectedCode: http.StatusBadRequest, - expectedBody: "", - expectError: true, - expectedCommitmentMeta: commitments.CommitmentMeta{}, - }, { name: "Failure OP Mode Alt-DA - InternalServerError", url: "/put", @@ -217,10 +123,9 @@ func TestPutHandler(t *testing.T) { mockBehavior: func() { mockRouter.EXPECT().Put(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, fmt.Errorf("internal error")) }, - expectedCode: http.StatusInternalServerError, - expectedBody: "", - expectError: true, - expectedCommitmentMeta: commitments.CommitmentMeta{}, + expectedCode: http.StatusInternalServerError, + expectedBody: "", + expectError: true, }, { name: "Success OP Mode Alt-DA", @@ -229,10 +134,9 @@ func TestPutHandler(t *testing.T) { mockBehavior: func() { mockRouter.EXPECT().Put(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte(testCommitStr), nil) }, - expectedCode: http.StatusOK, - expectedBody: opGenericPrefixStr + testCommitStr, - expectError: false, - expectedCommitmentMeta: commitments.CommitmentMeta{Mode: commitments.OptimismGeneric, CertVersion: 0}, + expectedCode: http.StatusOK, + expectedBody: opGenericPrefixStr + testCommitStr, + expectError: false, }, { name: "Success OP Mode Keccak256", @@ -241,10 +145,9 @@ func TestPutHandler(t *testing.T) { mockBehavior: func() { mockRouter.EXPECT().Put(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte(testCommitStr), nil) }, - expectedCode: http.StatusOK, - expectedBody: "", - expectError: false, - expectedCommitmentMeta: commitments.CommitmentMeta{Mode: commitments.OptimismKeccak, CertVersion: 0}, + expectedCode: http.StatusOK, + expectedBody: "", + expectError: false, }, { name: "Success Simple Commitment Mode", @@ -253,10 +156,9 @@ func TestPutHandler(t *testing.T) { mockBehavior: func() { mockRouter.EXPECT().Put(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return([]byte(testCommitStr), nil) }, - expectedCode: http.StatusOK, - expectedBody: genericPrefix + testCommitStr, - expectError: false, - expectedCommitmentMeta: commitments.CommitmentMeta{Mode: commitments.SimpleCommitmentMode, CertVersion: 0}, + expectedCode: http.StatusOK, + expectedBody: simpleCommitmentPrefix + testCommitStr, + expectError: false, }, } @@ -264,15 +166,15 @@ func TestPutHandler(t *testing.T) { t.Run(tt.name, func(t *testing.T) { tt.mockBehavior() - req := httptest.NewRequest(http.MethodPut, tt.url, bytes.NewReader(tt.body)) + req := httptest.NewRequest(http.MethodPost, tt.url, bytes.NewReader(tt.body)) rec := httptest.NewRecorder() - meta, err := server.HandlePut(rec, req) - if tt.expectError { - require.Error(t, err) - } else { - require.NoError(t, err) - } + // To add the vars to the context, + // we need to create a router through which we can pass the request. + r := mux.NewRouter() + server.registerRoutes(r) + r.ServeHTTP(rec, req) + require.Equal(t, tt.expectedCode, rec.Code) if !tt.expectError && tt.expectedBody != "" { require.Equal(t, []byte(tt.expectedBody), rec.Body.Bytes()) @@ -281,21 +183,6 @@ func TestPutHandler(t *testing.T) { if !tt.expectError && tt.expectedBody == "" { require.Equal(t, []byte(nil), rec.Body.Bytes()) } - require.Equal(t, tt.expectedCommitmentMeta, meta) - }) - } - - for _, tt := range tests { - t.Run(tt.name+"/CheckMiddlewaresNoPanic", func(_ *testing.T) { - tt.mockBehavior() - - req := httptest.NewRequest(http.MethodGet, tt.url, nil) - rec := httptest.NewRecorder() - // we also run the request through the middlewares, to make sure no panic occurs - // could happen if there's a problem with the metrics. For eg, in the past we saw - // panic: inconsistent label cardinality: expected 3 label values but got 1 in []string{"GET"} - handler := WithLogging(WithMetrics(server.HandlePut, server.m), server.log) - handler(rec, req) }) } }