From e495b6111c4df39c1d0cb45166c7db862ae28dbe Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Krieger Date: Fri, 23 Aug 2024 17:21:30 -0300 Subject: [PATCH] feat(inspect): add inspect package --- internal/inspect/inspect.go | 133 ++++++++++++++++ internal/inspect/inspect_test.go | 253 +++++++++++++++++++++++++++++++ internal/node/handlers.go | 6 +- 3 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 internal/inspect/inspect.go create mode 100644 internal/inspect/inspect_test.go diff --git a/internal/inspect/inspect.go b/internal/inspect/inspect.go new file mode 100644 index 000000000..4b4cdc6bf --- /dev/null +++ b/internal/inspect/inspect.go @@ -0,0 +1,133 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package inspect + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "net/http" + + . "github.com/cartesi/rollups-node/internal/node/model" + "github.com/cartesi/rollups-node/internal/nodemachine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +var ( + ErrInvalidMachines = errors.New("machines must not be nil") + ErrInvalidRepository = errors.New("repository must not be nil") + + ErrNoApp = errors.New("no machine for application") + ErrBadRequest = errors.New("inspect bad request") +) + +type Inspect struct { + machines Machines +} + +type InspectResponse struct { + Status string `json:"status"` + Exception string `json:"exception"` + Reports []string `json:"reports"` + InputIndex uint64 `json:"processed_input_count"` +} + +// New instantiates a new Inspect. +func New(machines Machines) (*Inspect, error) { + if machines == nil { + return nil, ErrInvalidMachines + } + + return &Inspect{machines: machines}, nil +} + +func (inspect *Inspect) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var ( + dapp Address + payload []byte + err error + reports []string + status string + ) + + dapp = common.HexToAddress(r.PathValue("dapp")) + if r.Method == "POST" { + payload, err = io.ReadAll(r.Body) + if err != nil { + slog.Info("Bad request", + "service", "inspect", + "err", err) + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + } else { + payload, err = hexutil.Decode(r.PathValue("payload")) + if err != nil { + slog.Info("Internal server error", + "service", "inspect", + "err", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + } + + result, err := inspect.process(r.Context(), dapp, payload) + if err != nil { + slog.Info("Internal server error", + "service", "inspect", + "err", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + for _, report := range result.Reports { + reports = append(reports, hexutil.Encode(report)) + } + + if result.Accepted { + status = "Accepted" + } else { + status = "Refused" + } + + response := InspectResponse{ + Status: status, + Exception: fmt.Sprintf("Error on the machine while inspecting: %s", result.Error), + Reports: reports, + InputIndex: result.InputIndex, + } + + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// process sends an inspect request to the machine +func (inspect *Inspect) process(ctx context.Context, app Address, query []byte) (*nodemachine.InspectResult, error) { + // Asserts that the app has an associated machine. + machine, ok := inspect.machines[app] + if !ok { + panic(fmt.Errorf("%w %s", ErrNoApp, app.String())) + } + + res, err := machine.Inspect(ctx, query) + if err != nil { + return nil, err + } + + return res, nil +} + +// ------------------------------------------------------------------------------------------------ + +// A map of application addresses to machines. +type Machines = map[Address]Machine + +type Machine interface { + Inspect(_ context.Context, query []byte) (*nodemachine.InspectResult, error) +} diff --git a/internal/inspect/inspect_test.go b/internal/inspect/inspect_test.go new file mode 100644 index 000000000..d52914645 --- /dev/null +++ b/internal/inspect/inspect_test.go @@ -0,0 +1,253 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package inspect + +import ( + "bytes" + "context" + crand "crypto/rand" + "encoding/json" + "fmt" + "io" + mrand "math/rand" + "net/http" + "strings" + "testing" + "time" + + . "github.com/cartesi/rollups-node/internal/node/model" + "github.com/cartesi/rollups-node/internal/nodemachine" + "github.com/cartesi/rollups-node/internal/services" + + "github.com/stretchr/testify/suite" +) + +const TestTimeout = 5 * time.Second + +func TestAdvancer(t *testing.T) { + suite.Run(t, new(InspectSuite)) +} + +type InspectSuite struct { + suite.Suite + ServicePort int + ServiceAddr string +} + +func (s *InspectSuite) SetupSuite() { + s.ServicePort = 5555 +} + +func (s *InspectSuite) SetupTest() { + s.ServicePort++ + s.ServiceAddr = fmt.Sprintf("127.0.0.1:%v", s.ServicePort) +} + +func (s *InspectSuite) TestNew() { + s.Run("Ok", func() { + require := s.Require() + var machines map[Address]Machine = Machines{randomAddress(): &MockMachine{}} + inspect, err := New(machines) + require.NotNil(inspect) + require.Nil(err) + }) + + s.Run("InvalidMachines", func() { + require := s.Require() + var machines map[Address]Machine = nil + inspect, err := New(machines) + require.Nil(inspect) + require.Error(err) + require.Equal(ErrInvalidMachines, err) + }) +} + +func (s *InspectSuite) TestGetOk() { + inspect, app, payload := s.setup() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + router := http.NewServeMux() + router.Handle("/test/{dapp}/{payload}", inspect) + service := services.HttpService{Name: "http", Address: s.ServiceAddr, Handler: router} + + result := make(chan error, 1) + ready := make(chan struct{}, 1) + go func() { + result <- service.Start(ctx, ready) + }() + + select { + case <-ready: + case <-time.After(TestTimeout): + s.FailNow("timed out waiting for HttpService to be ready") + } + + resp, err := http.Get(fmt.Sprintf("http://%v/test/%v/%v", s.ServiceAddr, app.Hex(), payload.Hex())) + if err != nil { + s.FailNow(err.Error()) + } + s.assertResponse(resp, payload.Hex()) +} + +func (s *InspectSuite) TestGetInvalidPayload() { + inspect, app, _ := s.setup() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + router := http.NewServeMux() + router.Handle("/test/{dapp}/{payload}", inspect) + service := services.HttpService{Name: "http", Address: s.ServiceAddr, Handler: router} + + result := make(chan error, 1) + ready := make(chan struct{}, 1) + go func() { + result <- service.Start(ctx, ready) + }() + + select { + case <-ready: + case <-time.After(TestTimeout): + s.FailNow("timed out waiting for HttpService to be ready") + } + + resp, _ := http.Get(fmt.Sprintf("http://%v/test/%v/%v", s.ServiceAddr, app.Hex(), "qwertyuiop")) + s.Equal(http.StatusInternalServerError, resp.StatusCode) + buf := new(strings.Builder) + io.Copy(buf, resp.Body) + s.Require().Contains(buf.String(), "hex string without 0x prefix") +} + +func (s *InspectSuite) TestPostOk() { + inspect, app, payload := s.setup() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + router := http.NewServeMux() + router.Handle("/test/{dapp}", inspect) + service := services.HttpService{Name: "http", Address: s.ServiceAddr, Handler: router} + + result := make(chan error, 1) + ready := make(chan struct{}, 1) + go func() { + result <- service.Start(ctx, ready) + }() + + select { + case <-ready: + case <-time.After(TestTimeout): + s.FailNow("timed out waiting for HttpService to be ready") + } + + resp, err := http.Post(fmt.Sprintf("http://%v/test/%v", s.ServiceAddr, app.Hex()), + "application/octet-stream", + bytes.NewBuffer(payload.Bytes())) + if err != nil { + s.FailNow(err.Error()) + } + s.assertResponse(resp, payload.Hex()) +} + +func (s *InspectSuite) setup() (*Inspect, Address, Hash) { + app := randomAddress() + machines := Machines{} + machines[app] = &MockMachine{} + inspect := &Inspect{machines} + payload := randomHash() + return inspect, app, payload +} + +func (s *InspectSuite) assertResponse(resp *http.Response, payload string) { + s.Equal(http.StatusOK, resp.StatusCode) + + defer resp.Body.Close() + + var r InspectResponse + err := json.NewDecoder(resp.Body).Decode(&r) + if err != nil { + s.FailNow("failed to read response body. ", err) + } + s.Equal(payload, r.Reports[0]) +} + +// ------------------------------------------------------------------------------------------------ + +type MockMachine struct{} + +func (mock *MockMachine) Inspect( + _ context.Context, + query []byte, +) (*nodemachine.InspectResult, error) { + var res nodemachine.InspectResult + var reports [][]byte + + reports = append(reports, query) + res.Accepted = true + res.InputIndex = 0 + res.Error = nil + res.Reports = reports + + return &res, nil +} + +// ------------------------------------------------------------------------------------------------ + +func randomAddress() Address { + address := make([]byte, 20) + _, err := crand.Read(address) + if err != nil { + panic(err) + } + return Address(address) +} + +func randomHash() Hash { + hash := make([]byte, 32) + _, err := crand.Read(hash) + if err != nil { + panic(err) + } + return Hash(hash) +} + +func randomBytes() []byte { + size := mrand.Intn(100) + 1 + bytes := make([]byte, size) + _, err := crand.Read(bytes) + if err != nil { + panic(err) + } + return bytes +} + +func randomSliceOfBytes() [][]byte { + size := mrand.Intn(10) + 1 + slice := make([][]byte, size) + for i := 0; i < size; i++ { + slice[i] = randomBytes() + } + return slice +} + +func randomInspectResult() *nodemachine.InspectResult { + res := &nodemachine.InspectResult{ + Accepted: true, + Reports: randomSliceOfBytes(), + Error: nil, + InputIndex: uint64(mrand.Intn(1000)), + } + + return res +} + +func marshal(res *nodemachine.AdvanceResult) []byte { + data, err := json.Marshal(*res) + if err != nil { + panic(err) + } + return data +} diff --git a/internal/node/handlers.go b/internal/node/handlers.go index 3fddbc8f7..6b9da9b56 100644 --- a/internal/node/handlers.go +++ b/internal/node/handlers.go @@ -10,10 +10,11 @@ import ( "net/http/httputil" "net/url" + "github.com/cartesi/rollups-node/internal/inspect" "github.com/cartesi/rollups-node/internal/node/config" ) -func newHttpServiceHandler(c config.NodeConfig) http.Handler { +func newHttpServiceHandler(c config.NodeConfig, i inspect.Inspect) http.Handler { handler := http.NewServeMux() handler.Handle("/healthz", http.HandlerFunc(healthcheckHandler)) @@ -21,6 +22,9 @@ func newHttpServiceHandler(c config.NodeConfig) http.Handler { handler.Handle("/graphql", graphqlProxy) handler.Handle("/graphiql", graphqlProxy) + handler.Handle("/inspect/{dapp}", http.Handler(&i)) + handler.Handle("/inspect/{dapp}/{payload}", http.Handler(&i)) + return handler }