From eccc45b5c094f419a350460e967a54e7941ee2ce 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 | 144 ++++++++++++++++++ internal/inspect/inspect_test.go | 242 +++++++++++++++++++++++++++++++ internal/node/handlers.go | 6 +- internal/node/services.go | 25 +++- 4 files changed, 413 insertions(+), 4 deletions(-) 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..d36fd3846 --- /dev/null +++ b/internal/inspect/inspect.go @@ -0,0 +1,144 @@ +// (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/advancer/machines" + . "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.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(response) + if err != nil { + slog.Info("Internal server error", + "service", "inspect", + "err", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +// 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 := inspect.machines.GetInspectMachine(app) + if machine == nil { + panic(fmt.Errorf("%w %s", ErrNoApp, app.String())) + } + + res, err := machine.Inspect(ctx, query) + if err != nil { + return nil, err + } + + return res, nil +} + +// ------------------------------------------------------------------------------------------------ + +type Machines interface { + GetInspectMachine(app Address) machines.InspectMachine +} + +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..73499a447 --- /dev/null +++ b/internal/inspect/inspect_test.go @@ -0,0 +1,242 @@ +// (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" + "net/http" + "strings" + "testing" + "time" + + "github.com/cartesi/rollups-node/internal/node/advancer/machines" + . "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() + machines := newMockMachines() + machines.Map[randomAddress()] = &MockMachine{} + inspect, err := New(machines) + require.NotNil(inspect) + require.Nil(err) + }) + + s.Run("InvalidMachines", func() { + require := s.Require() + var machines Machines = 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) //nolint: errcheck + 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()) +} + +// Note: add more tests + +func (s *InspectSuite) setup() (*Inspect, Address, Hash) { + app := randomAddress() + machines := newMockMachines() + machines.Map[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 MachinesMock struct { + Map map[Address]machines.InspectMachine +} + +func newMockMachines() *MachinesMock { + return &MachinesMock{ + Map: map[Address]machines.InspectMachine{}, + } +} + +func (mock *MachinesMock) GetInspectMachine(app Address) machines.InspectMachine { + return mock.Map[app] +} + +// ------------------------------------------------------------------------------------------------ + +type MockMachine struct{} + +func (mock *MockMachine) Inspect( + _ context.Context, + query []byte, +) (*nodemachine.InspectResult, error) { + var res nodemachine.InspectResult + var reports [][]byte + var index *uint64 = new(uint64) + *index = 0 + + reports = append(reports, query) + res.Accepted = true + res.InputIndex = index + 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) +} diff --git a/internal/node/handlers.go b/internal/node/handlers.go index 3fddbc8f7..cef2778ae 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 } diff --git a/internal/node/services.go b/internal/node/services.go index 2dd21d03b..7b6bee633 100644 --- a/internal/node/services.go +++ b/internal/node/services.go @@ -4,11 +4,14 @@ package node import ( + "context" "fmt" "log/slog" "os" evmreaderservice "github.com/cartesi/rollups-node/internal/evmreader/service" + "github.com/cartesi/rollups-node/internal/inspect" + "github.com/cartesi/rollups-node/internal/node/advancer/machines" "github.com/cartesi/rollups-node/internal/node/config" "github.com/cartesi/rollups-node/internal/repository" "github.com/cartesi/rollups-node/internal/services" @@ -99,7 +102,23 @@ func newSupervisorService( s = append(s, newAuthorityClaimer(c, workDir)) } - s = append(s, newHttpService(c)) + // initialize machines for inspect + repo := &repository.MachineRepository{Database: database} + + machines, err := machines.Load(context.Background(), repo, c.MachineServerVerbosity) + if err != nil { + slog.Error("failed to load the machines", "error", err) + os.Exit(1) + } + defer machines.Close() + + inspect, err := inspect.New(machines) + if err != nil { + slog.Error("failed to create the inspect", "error", err) + os.Exit(1) + } + + s = append(s, newHttpService(c, inspect)) s = append(s, newPostgraphileService(c, workDir)) s = append(s, newEvmReaderService(c, database)) s = append(s, newValidatorService(c, database)) @@ -111,9 +130,9 @@ func newSupervisorService( return supervisor } -func newHttpService(c config.NodeConfig) services.HttpService { +func newHttpService(c config.NodeConfig, i *inspect.Inspect) services.HttpService { addr := fmt.Sprintf("%v:%v", c.HttpAddress, getPort(c, portOffsetProxy)) - handler := newHttpServiceHandler(c) + handler := newHttpServiceHandler(c, i) return services.HttpService{ Name: "http", Address: addr,