Skip to content

Commit

Permalink
feat(inspect): add inspect package
Browse files Browse the repository at this point in the history
  • Loading branch information
GMKrieger committed Sep 2, 2024
1 parent c08c88a commit e495b61
Show file tree
Hide file tree
Showing 3 changed files with 391 additions and 1 deletion.
133 changes: 133 additions & 0 deletions internal/inspect/inspect.go
Original file line number Diff line number Diff line change
@@ -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)
}
253 changes: 253 additions & 0 deletions internal/inspect/inspect_test.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit e495b61

Please sign in to comment.