diff --git a/README.md b/README.md index ef737be..695356e 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/go.mod b/go.mod index 39d4d2a..0d2242c 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 1201bc7..6195f13 100644 --- a/go.sum +++ b/go.sum @@ -395,6 +395,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/server.go b/server/server.go index a5e3e05..d7d985c 100644 --- a/server/server.go +++ b/server/server.go @@ -2,6 +2,7 @@ package server import ( "context" + "encoding/hex" "errors" "fmt" "io" @@ -17,6 +18,7 @@ import ( "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 ( @@ -95,17 +97,50 @@ func WithLogging( } func (svr *Server) Start() error { - mux := http.NewServeMux() - - mux.HandleFunc("/get/", WithLogging(WithMetrics(svr.HandleGet, svr.m), svr.log)) + r := mux.NewRouter() + + // simple commitments (for nitro) + r.HandleFunc("/get/"+ + "{optional_prefix:(?:0x)?}"+ // commitments can be prefixed with 0x + "{version_byte_hex:[0-9a-fA-F]{2}}"+ // should always be 0x00 for now but we let others through to return a 404 + "{raw_commitment_hex}", + WithLogging(WithMetrics(svr.handleGetSimpleCommitment, svr.m), svr.log), + ).Queries("commitment_mode", "simple").Methods("GET") + // op keccak256 commitments (write to S3) + r.HandleFunc("/get/"+ + "{optional_prefix:(?:0x)?}"+ // commitments can be prefixed with 0x + "{commit_type_byte_hex:00}"+ // 00 for keccak256 commitments + "{da_layer_byte:[0-9a-fA-F]{2}}"+ // should always be 0x00 for eigenDA but we let others through to return a 404 + "{version_byte_hex:[0-9a-fA-F]{2}}"+ // should always be 0x00 for now but we let others through to return a 404 + "{raw_commitment_hex}", + WithLogging(WithMetrics(svr.handleGetOPKeccakCommitment, svr.m), svr.log), + ).Methods("GET") + // op generic commitments (write to EigenDA) + r.HandleFunc("/get/"+ + "{optional_prefix:(?:0x)?}"+ // commitments can be prefixed with 0x + "{commit_type_byte_hex: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 + "{version_byte_hex:[0-9a-fA-F]{2}}"+ // should always be 0x00 for now but we let others through to return a 404 + "{raw_commitment_hex}", + WithLogging(WithMetrics(svr.handleGetOPGenericCommitment, svr.m), svr.log), + ).Methods("GET") + // unrecognized op commitment type (not 00 or 01) + r.HandleFunc("/get/"+ + "{optional_prefix:(?:0x)?}"+ // commitments can be prefixed with 0x + "{commit_type_byte_hex:[0-9a-fA-F]{2}}", + func(w http.ResponseWriter, r *http.Request) { + commitType := mux.Vars(r)["commit_type_byte_hex"] + svr.WriteNotFound(w, fmt.Errorf("unsupported commitment type %s", commitType)) + }, + ).Methods("GET") // 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)) + r.HandleFunc("/put", WithLogging(WithMetrics(svr.handlePut, svr.m), svr.log)).Methods("POST") + r.HandleFunc("/put/", WithLogging(WithMetrics(svr.handlePut, svr.m), svr.log)).Methods("POST") + r.HandleFunc("/health", WithLogging(svr.Health, svr.log)).Methods("GET") - svr.httpServer.Handler = mux + svr.httpServer.Handler = r listener, err := net.Listen("tcp", svr.endpoint) if err != nil { @@ -153,29 +188,81 @@ func (svr *Server) Health(w http.ResponseWriter, _ *http.Request) error { return nil } -// HandleGet handles the GET request for commitments. +func (svr *Server) handleGetSimpleCommitment(w http.ResponseWriter, r *http.Request) (commitments.CommitmentMeta, error) { + versionByte, err := parseVersionByte(r) + if err != nil { + return commitments.CommitmentMeta{}, fmt.Errorf("error parsing version byte: %w", err) + } + commitmentMeta := commitments.CommitmentMeta{ + Mode: commitments.SimpleCommitmentMode, + CertVersion: versionByte, + } + + rawCommitmentHex, ok := mux.Vars(r)["raw_commitment_hex"] + if !ok { + return commitments.CommitmentMeta{}, fmt.Errorf("commitment not found in path: %s", r.URL.Path) + } + commitment, err := hex.DecodeString(rawCommitmentHex) + if err != nil { + return commitments.CommitmentMeta{}, fmt.Errorf("failed to decode commitment %s: %w", rawCommitmentHex, err) + } + + svr.log.Info("Processing simple commitment", "commitment", rawCommitmentHex, "commitmentMeta", commitmentMeta) + return commitmentMeta, svr.handleGetShared(r.Context(), w, commitment, commitmentMeta) +} + +// handleGetOPKeccakCommitment handles the GET request for optimism keccak 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) +func (svr *Server) handleGetOPKeccakCommitment(w http.ResponseWriter, r *http.Request) (commitments.CommitmentMeta, error) { + versionByte, err := parseVersionByte(r) if err != nil { - err = fmt.Errorf("invalid commitment mode: %w", err) - svr.WriteBadRequest(w, err) - return commitments.CommitmentMeta{}, err + return commitments.CommitmentMeta{}, fmt.Errorf("error parsing version byte: %w", err) } - key := path.Base(r.URL.Path) - comm, err := commitments.StringToDecodedCommitment(key, meta.Mode) + commitmentMeta := commitments.CommitmentMeta{ + Mode: commitments.OptimismKeccak, + CertVersion: versionByte, + } + + rawCommitmentHex, ok := mux.Vars(r)["raw_commitment_hex"] + if !ok { + return commitments.CommitmentMeta{}, fmt.Errorf("commitment not found in path: %s", r.URL.Path) + } + commitment, err := hex.DecodeString(rawCommitmentHex) 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, - } + return commitments.CommitmentMeta{}, fmt.Errorf("failed to decode commitment %s: %w", rawCommitmentHex, err) } - input, err := svr.router.Get(r.Context(), comm, meta.Mode) + svr.log.Info("Processing op keccak commitment", "commitment", rawCommitmentHex, "commitmentMeta", commitmentMeta) + return commitmentMeta, svr.handleGetShared(r.Context(), w, commitment, commitmentMeta) +} + +func (svr *Server) handleGetOPGenericCommitment(w http.ResponseWriter, r *http.Request) (commitments.CommitmentMeta, error) { + versionByte, err := parseVersionByte(r) + if err != nil { + return commitments.CommitmentMeta{}, fmt.Errorf("error parsing version byte: %w", err) + } + commitmentMeta := commitments.CommitmentMeta{ + Mode: commitments.OptimismGeneric, + CertVersion: versionByte, + } + + rawCommitmentHex, ok := mux.Vars(r)["raw_commitment_hex"] + if !ok { + return commitments.CommitmentMeta{}, fmt.Errorf("commitment not found in path: %s", r.URL.Path) + } + commitment, err := hex.DecodeString(rawCommitmentHex) + if err != nil { + return commitments.CommitmentMeta{}, fmt.Errorf("failed to decode commitment %s: %w", rawCommitmentHex, err) + } + + svr.log.Info("Processing op keccak commitment", "commitment", rawCommitmentHex, "commitmentMeta", commitmentMeta) + return commitmentMeta, svr.handleGetShared(r.Context(), w, commitment, commitmentMeta) +} + +func (svr *Server) handleGetShared(ctx context.Context, w http.ResponseWriter, comm []byte, meta commitments.CommitmentMeta) error { + input, err := svr.router.Get(ctx, 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) { @@ -183,22 +270,22 @@ func (svr *Server) HandleGet(w http.ResponseWriter, r *http.Request) (commitment } else { svr.WriteInternalError(w, err) } - return commitments.CommitmentMeta{}, MetaError{ + return MetaError{ Err: err, Meta: meta, } } svr.WriteResponse(w, input) - return meta, nil + return nil } -// HandlePut handles the PUT request for commitments. +// 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) +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) @@ -299,16 +386,16 @@ func (svr *Server) Port() int { } // Read both commitment mode and version -func ReadCommitmentMeta(r *http.Request) (commitments.CommitmentMeta, error) { +func readCommitmentMeta(r *http.Request) (commitments.CommitmentMeta, error) { // label requests with commitment mode and version - ct, err := ReadCommitmentMode(r) + ct, err := readCommitmentMode(r) if err != nil { - return commitments.CommitmentMeta{}, err + return commitments.CommitmentMeta{}, fmt.Errorf("failed to read commitment mode: %w", err) } if ct == "" { return commitments.CommitmentMeta{}, fmt.Errorf("commitment mode is empty") } - cv, err := ReadCommitmentVersion(r, ct) + cv, err := readCommitmentVersion(r, ct) if err != nil { // default to version 0 return commitments.CommitmentMeta{Mode: ct, CertVersion: cv}, err @@ -316,13 +403,17 @@ func ReadCommitmentMeta(r *http.Request) (commitments.CommitmentMeta, error) { return commitments.CommitmentMeta{Mode: ct, CertVersion: cv}, nil } -func ReadCommitmentMode(r *http.Request) (commitments.CommitmentMode, error) { +func readCommitmentMode(r *http.Request) (commitments.CommitmentMode, error) { query := r.URL.Query() + // if commitment mode is provided in the query params, use it + // eg. /get/0x123..?commitment_mode=simple + // TODO: should we only allow simple commitment to be set in the query params? key := query.Get(CommitmentModeKey) if key != "" { return commitments.StringToCommitmentMode(key) } + // else, we need to parse the first byte of the commitment commit := path.Base(r.URL.Path) if len(commit) > 0 && commit != Put { // provided commitment in request params (op keccak256) if !strings.HasPrefix(commit, "0x") { @@ -352,7 +443,7 @@ func ReadCommitmentMode(r *http.Request) (commitments.CommitmentMode, error) { return commitments.OptimismGeneric, nil } -func ReadCommitmentVersion(r *http.Request, mode commitments.CommitmentMode) (byte, error) { +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") { @@ -402,3 +493,25 @@ func (svr *Server) GetStoreStats(bt store.BackendType) (*store.Stats, error) { return nil, fmt.Errorf("store not found") } + +func parseVersionByte(r *http.Request) (byte, error) { + vars := mux.Vars(r) + // decode version byte + versionByteHex, ok := vars["version_byte_hex"] + 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: + return 0, fmt.Errorf("unsupported version byte %x", versionByte) + } +} diff --git a/server/server_test.go b/server/server_test.go index 27b9a30..40a37a4 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -12,6 +12,7 @@ import ( "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" ) @@ -25,7 +26,7 @@ const ( testCommitStr = "9a7d4f1c3e5b8a09d1c0fa4b3f8e1d7c6b29f1e6d8c4a7b3c2d4e5f6a7b8c9d0" ) -func TestGetHandler(t *testing.T) { +func TestGetOpKeccakCommitmentHandler(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -87,39 +88,39 @@ func TestGetHandler(t *testing.T) { expectError: true, expectedCommitmentMeta: commitments.CommitmentMeta{}, }, - { - name: "Success - OP Keccak256", - url: fmt.Sprintf("/get/0x00%s", testCommitStr), - 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}, - }, - { - name: "Failure - OP Alt-DA Internal Server Error", - url: fmt.Sprintf("/get/0x010000%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{}, - }, - { - name: "Success - OP Alt-DA", - url: fmt.Sprintf("/get/0x010000%s", testCommitStr), - 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}, - }, + // { + // name: "Success - OP Keccak256", + // url: fmt.Sprintf("/get/0x00%s", testCommitStr), + // 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}, + // }, + // { + // name: "Failure - OP Alt-DA Internal Server Error", + // url: fmt.Sprintf("/get/0x010000%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{}, + // }, + // { + // name: "Success - OP Alt-DA", + // url: fmt.Sprintf("/get/0x010000%s", testCommitStr), + // 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}, + // }, } for _, tt := range tests { @@ -129,16 +130,27 @@ 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. + router := mux.NewRouter() + testHandler := func(s string, handleFn func(w http.ResponseWriter, r *http.Request) (meta commitments.CommitmentMeta, err error)) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + meta, err := handleFn(w, r) + if tt.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + require.Equal(t, tt.expectedCode, rec.Code) + require.Equal(t, tt.expectedCommitmentMeta, meta) + require.Equal(t, tt.expectedBody, rec.Body.String()) + } } - - require.Equal(t, tt.expectedCode, rec.Code) - require.Equal(t, tt.expectedCommitmentMeta, meta) - require.Equal(t, tt.expectedBody, rec.Body.String()) + router.HandleFunc("/get/"+ + "{optional_prefix:(?:0x)?}"+ // commitments can be prefixed with 0x + "{commit_type_byte_hex:00}"+ // 00 for keccak256 commitments + "{raw_commitment_hex}", testHandler(tt.url, server.handleGetOPKeccakCommitment)) + router.ServeHTTP(rec, req) }) } @@ -151,7 +163,7 @@ func TestGetHandler(t *testing.T) { // 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 := WithLogging(WithMetrics(server.handleGetOPKeccakCommitment, server.m), server.log) handler(rec, req) }) } @@ -267,7 +279,7 @@ func TestPutHandler(t *testing.T) { req := httptest.NewRequest(http.MethodPut, tt.url, bytes.NewReader(tt.body)) rec := httptest.NewRecorder() - meta, err := server.HandlePut(rec, req) + meta, err := server.handlePut(rec, req) if tt.expectError { require.Error(t, err) } else { @@ -294,7 +306,7 @@ func TestPutHandler(t *testing.T) { // 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 := WithLogging(WithMetrics(server.handlePut, server.m), server.log) handler(rec, req) }) }