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 Aug 28, 2024
1 parent 658078f commit 8c3a6d9
Show file tree
Hide file tree
Showing 3 changed files with 394 additions and 1 deletion.
122 changes: 122 additions & 0 deletions internal/inspect/inspect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// (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"
)

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("Inspect bad request",
"service", "inspect",
"err", err)
w.WriteHeader(http.StatusBadRequest)
return
}
} else {
payload = []byte(r.PathValue("payload"))
}

result, err := inspect.process(r.Context(), dapp, payload)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

for _, report := range result.Reports {
reports = append(reports, common.Bytes2Hex(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)
}
267 changes: 267 additions & 0 deletions internal/inspect/inspect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
// (c) Cartesi and individual authors (see AUTHORS)
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

package inspect

import (
"context"
crand "crypto/rand"
"encoding/json"
"errors"
"fmt"
mrand "math/rand"
"testing"

. "github.com/cartesi/rollups-node/internal/node/model"
"github.com/cartesi/rollups-node/internal/nodemachine"

"github.com/stretchr/testify/suite"
)

func TestAdvancer(t *testing.T) {
suite.Run(t, new(AdvancerSuite))
}

type AdvancerSuite struct{ suite.Suite }

func (s *AdvancerSuite) 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 *AdvancerSuite) TestRun() {
s.Run("Ok", func() {
require := s.Require()

machines := Machines{}
app1 := randomAddress()
machines[app1] = &MockMachine{}
app2 := randomAddress()
machines[app2] = &MockMachine{}
res1 := randomAdvanceResult()

Check failure on line 55 in internal/inspect/inspect_test.go

View workflow job for this annotation

GitHub Actions / test-go

undefined: randomAdvanceResult
res2 := randomAdvanceResult()

Check failure on line 56 in internal/inspect/inspect_test.go

View workflow job for this annotation

GitHub Actions / test-go

undefined: randomAdvanceResult
res3 := randomAdvanceResult()

Check failure on line 57 in internal/inspect/inspect_test.go

View workflow job for this annotation

GitHub Actions / test-go

undefined: randomAdvanceResult

repository := &MockRepository{

Check failure on line 59 in internal/inspect/inspect_test.go

View workflow job for this annotation

GitHub Actions / test-go

undefined: MockRepository
GetInputsReturn: map[Address][]*Input{
app1: {
{Id: 1, RawData: marshal(res1)},
{Id: 2, RawData: marshal(res2)},
},
app2: {
{Id: 5, RawData: marshal(res3)},
},
},
}

advancer, err := New(machines, repository)
require.NotNil(advancer)
require.Nil(err)

err = advancer.Step(context.Background())

Check failure on line 75 in internal/inspect/inspect_test.go

View workflow job for this annotation

GitHub Actions / test-go

advancer.Step undefined (type *Inspect has no field or method Step)
require.Nil(err)

require.Len(repository.StoredResults, 3)
})

// NOTE: missing more test cases
}

func (s *AdvancerSuite) TestProcess() {
setup := func() (Machines, *MockRepository, *Advancer, Address) {

Check failure on line 85 in internal/inspect/inspect_test.go

View workflow job for this annotation

GitHub Actions / test-go

undefined: MockRepository

Check failure on line 85 in internal/inspect/inspect_test.go

View workflow job for this annotation

GitHub Actions / test-go

undefined: Advancer
app := randomAddress()
machines := Machines{}
machines[app] = &MockMachine{}
repository := &MockRepository{}

Check failure on line 89 in internal/inspect/inspect_test.go

View workflow job for this annotation

GitHub Actions / test-go

undefined: MockRepository
advancer := &Advancer{machines, repository}
return machines, repository, advancer, app
}

s.Run("Ok", func() {
require := s.Require()

_, repository, advancer, app := setup()
inputs := []*Input{
{Id: 1, RawData: marshal(randomAdvanceResult())},
{Id: 2, RawData: marshal(randomAdvanceResult())},
{Id: 3, RawData: marshal(randomAdvanceResult())},
{Id: 4, RawData: marshal(randomAdvanceResult())},
{Id: 5, RawData: marshal(randomAdvanceResult())},
{Id: 6, RawData: marshal(randomAdvanceResult())},
{Id: 7, RawData: marshal(randomAdvanceResult())},
}

err := advancer.process(context.Background(), app, inputs)
require.Nil(err)
require.Len(repository.StoredResults, 7)
require.Equal(*inputs[6], repository.LastInput)
})

s.Run("Panic", func() {
s.Run("ErrApp", func() {
require := s.Require()

invalidApp := randomAddress()
_, _, advancer, _ := setup()
inputs := randomInputs(3)

expected := fmt.Sprintf("%v %v", ErrNoApp, invalidApp)
require.PanicsWithError(expected, func() {
_ = advancer.process(context.Background(), invalidApp, inputs)
})
})

s.Run("ErrInputs", func() {
require := s.Require()

_, _, advancer, app := setup()
inputs := []*Input{}

require.PanicsWithValue(ErrNoInputs, func() {
_ = advancer.process(context.Background(), app, inputs)
})
})
})

s.Run("Error", func() {
s.Run("Advance", func() {
require := s.Require()

_, repository, advancer, app := setup()
inputs := []*Input{
{Id: 1, RawData: marshal(randomAdvanceResult())},
{Id: 2, RawData: []byte("advance error")},
{Id: 3, RawData: []byte("unreachable")},
}

err := advancer.process(context.Background(), app, inputs)
require.Errorf(err, "advance error")
require.Len(repository.StoredResults, 1)
})

s.Run("StoreAdvance", func() {
require := s.Require()

_, repository, advancer, app := setup()
inputs := []*Input{
{Id: 1, RawData: marshal(randomAdvanceResult())},
{Id: 2, RawData: []byte("unreachable")},
}
repository.StoreAdvanceError = errors.New("store-advance error")

err := advancer.process(context.Background(), app, inputs)
require.Errorf(err, "store-advance error")
require.Len(repository.StoredResults, 1)
})

s.Run("UpdateEpochs", func() {
require := s.Require()

_, repository, advancer, app := setup()
inputs := []*Input{
{Id: 1, RawData: marshal(randomAdvanceResult())},
{Id: 2, RawData: marshal(randomAdvanceResult())},
{Id: 3, RawData: marshal(randomAdvanceResult())},
{Id: 4, RawData: marshal(randomAdvanceResult())},
}
repository.UpdateEpochsError = errors.New("update-epochs error")

err := advancer.process(context.Background(), app, inputs)
require.Errorf(err, "update-epochs error")
require.Len(repository.StoredResults, 4)
})
})

}

func (s *AdvancerSuite) TestKeysFrom() {
s.T().Skip("TODO")
}

// ------------------------------------------------------------------------------------------------

type MockMachine struct{}

func (mock *MockMachine) Inspect(
_ context.Context,
query []byte,
) (*nodemachine.InspectResult, error) {
var res nodemachine.InspectResult
err := json.Unmarshal(query, &res)
if err != nil {
return nil, errors.New(string(query))
}
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 8c3a6d9

Please sign in to comment.