From 0628b7b6390543c1baea37c322ed470f8cabb492 Mon Sep 17 00:00:00 2001 From: Renan Santos Date: Tue, 21 May 2024 18:10:25 -0300 Subject: [PATCH 1/3] feat: add rollupsmachine package --- internal/node/model/models.go | 2 + pkg/rollupsmachine/error.go | 42 +++ pkg/rollupsmachine/io.go | 123 ++++++++ pkg/rollupsmachine/machine.go | 423 +++++++++++++++++++++++++ pkg/rollupsmachine/machine_test.go | 486 +++++++++++++++++++++++++++++ pkg/rollupsmachine/server.go | 126 ++++++++ 6 files changed, 1202 insertions(+) create mode 100644 pkg/rollupsmachine/error.go create mode 100644 pkg/rollupsmachine/io.go create mode 100644 pkg/rollupsmachine/machine.go create mode 100644 pkg/rollupsmachine/machine_test.go create mode 100644 pkg/rollupsmachine/server.go diff --git a/internal/node/model/models.go b/internal/node/model/models.go index e097a5009..051aa79e5 100644 --- a/internal/node/model/models.go +++ b/internal/node/model/models.go @@ -8,6 +8,8 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" ) +const HashLength = common.HashLength + type ( Hash = common.Hash Address = common.Address diff --git a/pkg/rollupsmachine/error.go b/pkg/rollupsmachine/error.go new file mode 100644 index 000000000..b0c209cd5 --- /dev/null +++ b/pkg/rollupsmachine/error.go @@ -0,0 +1,42 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package rollupsmachine + +import ( + "errors" + "fmt" + + "github.com/cartesi/rollups-node/internal/node/model" +) + +const unreachable = "internal error: entered unreacheable code" + +var ( + ErrCartesiMachine = errors.New("cartesi machine internal error") + + // Misc. + ErrException = errors.New("last request yielded an exception") + ErrHalted = errors.New("machine halted") + ErrProgress = errors.New("machine yielded progress") + ErrSoftYield = errors.New("machine yielded softly") + ErrCycleLimitExceeded = errors.New("cycle limit exceeded") + ErrOutputsLimitExceeded = errors.New("outputs length limit exceeded") + // ErrPayloadLengthLimitExceeded = errors.New("payload length limit exceeded") + + ErrOrphanServer = errors.New("cartesi machine server was left orphan") + + // Load + ErrNotAtManualYield = errors.New("not at manual yield") + + // Advance + ErrHashLength = fmt.Errorf("hash does not have exactly %d bytes", model.HashLength) +) + +func errOrphanServerWithAddress(address string) error { + return fmt.Errorf("%w at address %s", ErrOrphanServer, address) +} + +func errCartesiMachine(err error) error { + return errors.Join(ErrCartesiMachine, err) +} diff --git a/pkg/rollupsmachine/io.go b/pkg/rollupsmachine/io.go new file mode 100644 index 000000000..17d00d9b4 --- /dev/null +++ b/pkg/rollupsmachine/io.go @@ -0,0 +1,123 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package rollupsmachine + +import ( + "fmt" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" +) + +type Input struct { + ChainId uint64 + AppContract [20]byte + Sender [20]byte + BlockNumber uint64 + BlockTimestamp uint64 + // PrevRandao uint64 + Index uint64 + Data []byte +} + +type Query struct { + Data []byte +} + +type Voucher struct { + Address [20]byte + Value *big.Int + Data []byte +} + +type Notice struct { + Data []byte +} + +func (input Input) Encode() ([]byte, error) { + chainId := new(big.Int).SetUint64(input.ChainId) + appContract := common.BytesToAddress(input.AppContract[:]) + sender := common.BytesToAddress(input.Sender[:]) + blockNumber := new(big.Int).SetUint64(input.BlockNumber) + blockTimestamp := new(big.Int).SetUint64(input.BlockTimestamp) + // prevRandao := new(big.Int).SetUint64(input.PrevRandao) + index := new(big.Int).SetUint64(input.Index) + return ioABI.Pack("EvmAdvance", chainId, appContract, sender, blockNumber, blockTimestamp, + index, input.Data) +} + +func (query Query) Encode() ([]byte, error) { + return query.Data, nil +} + +func decodeArguments(payload []byte) (arguments []any, _ error) { + method, err := ioABI.MethodById(payload) + if err != nil { + return nil, err + } + + return method.Inputs.Unpack(payload[4:]) +} + +func DecodeOutput(payload []byte) (*Voucher, *Notice, error) { + arguments, err := decodeArguments(payload) + if err != nil { + return nil, nil, err + } + + switch length := len(arguments); length { + case 1: + notice := &Notice{Data: arguments[0].([]byte)} + return nil, notice, nil + case 3: //nolint:mnd + voucher := &Voucher{ + Address: [20]byte(arguments[0].(common.Address)), + Value: arguments[1].(*big.Int), + Data: arguments[2].([]byte), + } + return voucher, nil, nil + default: + return nil, nil, fmt.Errorf("not an output: len(arguments) == %d, should be 1 or 3", length) + } +} + +var ioABI abi.ABI + +func init() { + json := `[{ + "type" : "function", + "name" : "EvmAdvance", + "inputs" : [ + { "type" : "uint256" }, + { "type" : "address" }, + { "type" : "address" }, + { "type" : "uint256" }, + { "type" : "uint256" }, + { "type" : "uint256" }, + { "type" : "bytes" } + ] + }, { + "type" : "function", + "name" : "Voucher", + "inputs" : [ + { "type" : "address" }, + { "type" : "uint256" }, + { "type" : "bytes" } + ] + }, { + "type" : "function", + "name" : "Notice", + "inputs" : [ + { "type" : "bytes" } + ] + }]` + + var err error + ioABI, err = abi.JSON(strings.NewReader(json)) + if err != nil { + panic(err) + } +} diff --git a/pkg/rollupsmachine/machine.go b/pkg/rollupsmachine/machine.go new file mode 100644 index 000000000..ff4f5649e --- /dev/null +++ b/pkg/rollupsmachine/machine.go @@ -0,0 +1,423 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package rollupsmachine + +import ( + "errors" + "fmt" + "log/slog" + + "github.com/cartesi/rollups-node/internal/node/model" + "github.com/cartesi/rollups-node/pkg/emulator" +) + +// Convenient type aliases. +type ( + Cycle = uint64 + Output = []byte + Report = []byte +) + +type requestType uint8 + +const ( + DefaultInc = Cycle(10000000) + DefaultMax = Cycle(1000000000) + + advanceStateRequest requestType = 0 + inspectStateRequest requestType = 1 + + maxOutputs = 65536 // 2^16 +) + +// A RollupsMachine wraps an emulator.Machine and provides five basic functions: +// Fork, Destroy, Hash, Advance and Inspect. +type RollupsMachine struct { + // For each request, the machine will run in increments of Inc cycles, + // for no more than Max cycles. + // + // If these fields are left undefined, + // the machine will use the DefaultInc and DefaultMax values. + Inc, Max Cycle + + address string + + inner *emulator.Machine + remote *emulator.RemoteMachineManager +} + +// Load loads the machine stored at path into the remote server from address. +// It then checks if the machine is in a valid state to receive advance and inspect requests. +func Load(path, address string, config *emulator.MachineRuntimeConfig) (*RollupsMachine, error) { + // Creates the machine with default values for Inc and Max. + machine := &RollupsMachine{Inc: DefaultInc, Max: DefaultMax, address: address} + + // Creates the remote machine manager. + remote, err := emulator.NewRemoteMachineManager(address) + if err != nil { + err = fmt.Errorf("could not create the remote machine manager: %w", err) + return nil, errCartesiMachine(err) + } + machine.remote = remote + + // Loads the machine stored at path into the server. + // Creates the inner machine reference. + inner, err := remote.LoadMachine(path, config) + if err != nil { + defer machine.remote.Delete() + err = fmt.Errorf("could not load the machine: %w", err) + return nil, errCartesiMachine(err) + } + machine.inner = inner + + // Ensures that the machine is at a manual yield. + isAtManualYield, err := machine.inner.ReadIFlagsY() + if err != nil { + defer machine.remote.Delete() + err = fmt.Errorf("could not read iflagsY: %w", err) + return nil, errors.Join(errCartesiMachine(err), machine.closeInner()) + } + if !isAtManualYield { + defer machine.remote.Delete() + return nil, errors.Join(ErrNotAtManualYield, machine.closeInner()) + } + + // Ensures that the last request the machine received did not yield and exception. + _, err = machine.lastRequestWasAccepted() + if err != nil { + defer machine.remote.Delete() + return nil, errors.Join(err, machine.closeInner()) + } + + return machine, nil +} + +// Fork forks an existing cartesi machine. +func (machine *RollupsMachine) Fork() (_ *RollupsMachine, address string, _ error) { + // Creates the new machine based on the old machine. + newMachine := &RollupsMachine{Inc: machine.Inc, Max: machine.Max} + + // Forks the remote server's process. + address, err := machine.remote.Fork() + if err != nil { + err = fmt.Errorf("could not fork the machine: %w", err) + return nil, address, errCartesiMachine(err) + } + + // Instantiates the new remote machine manager. + newMachine.remote, err = emulator.NewRemoteMachineManager(address) + if err != nil { + err = fmt.Errorf("could not create the new remote machine manager: %w", err) + errOrphanServer := errOrphanServerWithAddress(address) + return nil, address, errors.Join(errCartesiMachine(err), errOrphanServer) + } + + // Gets the inner machine reference from the remote server. + newMachine.inner, err = newMachine.remote.GetMachine() + if err != nil { + err = fmt.Errorf("could not get the machine from the server: %w", err) + return nil, address, errors.Join(errCartesiMachine(err), newMachine.closeServer()) + } + + return newMachine, address, nil +} + +// Hash returns the machine's merkle tree root hash. +func (machine RollupsMachine) Hash() (model.Hash, error) { + hash, err := machine.inner.GetRootHash() + if err != nil { + err := fmt.Errorf("could not get the machine's root hash: %w", err) + return model.Hash(hash), errCartesiMachine(err) + } + return model.Hash(hash), nil +} + +// Advance sends an input to the cartesi machine. +// It returns a boolean indicating whether or not the request was accepted. +// It also returns the corresponding outputs, reports, and the hash of the outputs. +// +// If the request was not accepted, the function does not return outputs. +func (machine *RollupsMachine) Advance(input []byte) (bool, []Output, []Report, model.Hash, error) { + var outputsHash model.Hash + + accepted, outputs, reports, err := machine.process(input, advanceStateRequest) + if err != nil { + return accepted, outputs, reports, outputsHash, err + } + + if !accepted { + return accepted, nil, reports, model.Hash{}, nil + } else { + hashBytes, err := machine.readMemory() + if err != nil { + err := fmt.Errorf("could not read the outputs' hash from the memory: %w", err) + return accepted, outputs, reports, outputsHash, errCartesiMachine(err) + } + if length := len(hashBytes); length != model.HashLength { + err := fmt.Errorf("%w (it has %d bytes)", ErrHashLength, length) + return accepted, outputs, reports, outputsHash, err + } + copy(outputsHash[:], hashBytes) + + return accepted, outputs, reports, outputsHash, nil + } +} + +// Inspect sends a query to the cartesi machine. +// It returns a boolean indicating whether or not the request was accepted +// It also returns the corresponding reports. +func (machine *RollupsMachine) Inspect(query []byte) (bool, []Report, error) { + accepted, _, reports, err := machine.process(query, inspectStateRequest) + return accepted, reports, err +} + +// Close destroys the inner cartesi machine, deletes its reference, +// shutsdown the server, and deletes the server's reference. +func (machine *RollupsMachine) Close() error { + return errors.Join(machine.closeInner(), machine.closeServer()) +} + +// ------------------------------------------------------------------------------------------------ + +// closeInner destroys the machine and deletes its reference. +func (machine *RollupsMachine) closeInner() error { + defer machine.inner.Delete() + err := machine.inner.Destroy() + if err != nil { + err = fmt.Errorf("could not destroy the machine: %w", err) + err = errCartesiMachine(err) + } + return err +} + +// closeServer shutsdown the server and deletes its reference. +func (machine *RollupsMachine) closeServer() error { + defer machine.remote.Delete() + err := machine.remote.Shutdown() + if err != nil { + err = fmt.Errorf("could not shutdown the server: %w", err) + err = errCartesiMachine(err) + err = errors.Join(err, errOrphanServerWithAddress(machine.address)) + } + return err +} + +// lastRequestWasAccepted returns true if the last request was accepted and false otherwise. +// +// The machine MUST be at a manual yield when calling this function. +func (machine *RollupsMachine) lastRequestWasAccepted() (bool, error) { + yieldReason, err := machine.readYieldReason() + if err != nil { + err := fmt.Errorf("could not read the yield reason: %w", err) + return false, errCartesiMachine(err) + } + switch yieldReason { //nolint:exhaustive + case emulator.ManualYieldReasonAccepted: + return true, nil + case emulator.ManualYieldReasonRejected: + return false, nil + case emulator.ManualYieldReasonException: + return false, ErrException + default: + panic(unreachable) + } +} + +// process processes a request, be it an avance-state or an inspect-state request. +// It returns the accepted state and any collected responses. +// +// It expects the machine to be ready to receive requests before execution, +// and leaves the machine in a state ready to receive requests after an execution with no errors. +func (machine *RollupsMachine) process( + request []byte, + requestType requestType, +) (accepted bool, _ []Output, _ []Report, _ error) { + // Writes the request's data. + err := machine.inner.WriteMemory(emulator.CmioRxBufferStart, request) + if err != nil { + err := fmt.Errorf("could not write the request's data to the memory: %w", err) + return false, nil, nil, errCartesiMachine(err) + } + + // Writes the request's type and length. + fromhost := ((uint64(requestType) << 32) | (uint64(len(request)) & 0xffffffff)) //nolint:mnd + err = machine.inner.WriteHtifFromHostData(fromhost) + if err != nil { + err := fmt.Errorf("could not write HTIF fromhost data: %w", err) + return false, nil, nil, errCartesiMachine(err) + } + + // Green-lights the machine to keep running. + err = machine.inner.ResetIFlagsY() + if err != nil { + err := fmt.Errorf("could not reset iflagsY: %w", err) + return false, nil, nil, errCartesiMachine(err) + } + + outputs, reports, err := machine.runAndCollect() + if err != nil { + return false, outputs, reports, err + } + + accepted, err = machine.lastRequestWasAccepted() + + return accepted, outputs, reports, err +} + +// runAndCollect runs the machine until it manually yields. +// It returns any collected responses. +func (machine *RollupsMachine) runAndCollect() ([]Output, []Report, error) { + startingCycle, err := machine.readMachineCycle() + if err != nil { + err := fmt.Errorf("could not read the machine's cycle: %w", err) + return nil, nil, errCartesiMachine(err) + } + maxCycle := startingCycle + machine.Max + slog.Debug("runAndCollect", + "startingCycle", startingCycle, + "maxCycle", maxCycle, + "leftover", maxCycle-startingCycle) + + outputs := []Output{} + reports := []Report{} + for { + var ( + breakReason emulator.BreakReason + err error + ) + breakReason, startingCycle, err = machine.run(startingCycle, maxCycle) + if err != nil { + return outputs, reports, err + } + + switch breakReason { //nolint:exhaustive + case emulator.BreakReasonYieldedManually: + return outputs, reports, nil // returns with the responses + case emulator.BreakReasonYieldedAutomatically: + break // breaks from the switch to read the outputs/reports + default: + panic(unreachable) + } + + yieldReason, err := machine.readYieldReason() + if err != nil { + err := fmt.Errorf("could not read the yield reason: %w", err) + return outputs, reports, errCartesiMachine(err) + } + + switch yieldReason { //nolint:exhaustive + case emulator.AutomaticYieldReasonProgress: + return outputs, reports, ErrProgress + case emulator.AutomaticYieldReasonOutput: + output, err := machine.readMemory() + if err != nil { + err := fmt.Errorf("could not read the output from the memory: %w", err) + return outputs, reports, errCartesiMachine(err) + } + if len(outputs) == maxOutputs { + return outputs, reports, ErrOutputsLimitExceeded + } + outputs = append(outputs, output) + case emulator.AutomaticYieldReasonReport: + report, err := machine.readMemory() + if err != nil { + err := fmt.Errorf("could not read the report from the memory: %w", err) + return outputs, reports, errCartesiMachine(err) + } + reports = append(reports, report) + default: + panic(unreachable) + } + } +} + +// run runs the machine until it yields. +// +// If there are no errors, it returns one of two possible break reasons: +// - emulator.BreakReasonYieldedManually +// - emulator.BreakReasonYieldedAutomatically +// +// Otherwise, it returns one of four possible errors: +// - ErrCartesiMachine +// - ErrHalted +// - ErrSoftYield +// - ErrCycleLimitExceeded +func (machine *RollupsMachine) run( + startingCycle Cycle, + maxCycle Cycle, +) (emulator.BreakReason, Cycle, error) { + currentCycle := startingCycle + slog.Debug("run", "startingCycle", startingCycle, "leftover", maxCycle-startingCycle) + + for { + // Calculates the increment. + increment := min(machine.Inc, maxCycle-currentCycle) + + // Returns with an error if the next run would exceed Max cycles. + if currentCycle+increment >= maxCycle { + return emulator.BreakReasonReachedTargetMcycle, currentCycle, ErrCycleLimitExceeded + } + + // Runs the machine. + breakReason, err := machine.inner.Run(currentCycle + increment) + if err != nil { + assert(breakReason == emulator.BreakReasonFailed, breakReason.String()) + err := fmt.Errorf("machine run failed: %w", err) + return breakReason, currentCycle, errCartesiMachine(err) + } + + // Gets the current cycle. + currentCycle, err = machine.readMachineCycle() + if err != nil { + err := fmt.Errorf("could not read the machine's cycle: %w", err) + return emulator.BreakReasonFailed, currentCycle, errCartesiMachine(err) + } + slog.Debug("run", "currentCycle", currentCycle, "leftover", maxCycle-currentCycle) + + switch breakReason { + case emulator.BreakReasonFailed: + panic(unreachable) // covered above + case emulator.BreakReasonHalted: + return emulator.BreakReasonHalted, currentCycle, ErrHalted + case emulator.BreakReasonYieldedManually, emulator.BreakReasonYieldedAutomatically: + return breakReason, currentCycle, nil // returns with the break reason + case emulator.BreakReasonYieldedSoftly: + return emulator.BreakReasonYieldedSoftly, currentCycle, ErrSoftYield + case emulator.BreakReasonReachedTargetMcycle: + continue // keeps on running + default: + panic(unreachable) + } + } +} + +// ------------------------------------------------------------------------------------------------ + +// readMemory reads the machine's memory to retrieve the data from emitted outputs/reports. +func (machine *RollupsMachine) readMemory() ([]byte, error) { + tohost, err := machine.inner.ReadHtifToHostData() + if err != nil { + return nil, err + } + length := tohost & 0x00000000ffffffff //nolint:mnd + return machine.inner.ReadMemory(emulator.CmioTxBufferStart, length) +} + +func (machine *RollupsMachine) readYieldReason() (emulator.HtifYieldReason, error) { + value, err := machine.inner.ReadHtifToHostData() + return emulator.HtifYieldReason(value >> 32), err //nolint:mnd +} + +func (machine *RollupsMachine) readMachineCycle() (Cycle, error) { + cycle, err := machine.inner.ReadMCycle() + return Cycle(cycle), err +} + +// ------------------------------------------------------------------------------------------------ + +func assert(condition bool, s string) { + if !condition { + panic("assertion error: " + s) + } +} diff --git a/pkg/rollupsmachine/machine_test.go b/pkg/rollupsmachine/machine_test.go new file mode 100644 index 000000000..45c5744b4 --- /dev/null +++ b/pkg/rollupsmachine/machine_test.go @@ -0,0 +1,486 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package rollupsmachine + +import ( + "encoding/hex" + "fmt" + "log" + "log/slog" + "os" + "testing" + + "github.com/cartesi/rollups-node/pkg/emulator" + "github.com/cartesi/rollups-node/test/snapshot" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +func init() { + log.SetFlags(log.Ltime) + slog.SetLogLoggerLevel(slog.LevelDebug) +} + +const ( + cycles = uint64(1_000_000_000) + serverVerbosity = ServerVerbosityInfo +) + +func payload(s string) string { + return fmt.Sprintf("echo '{ \"payload\": \"0x%s\" }'", hex.EncodeToString([]byte(s))) +} + +// ------------------------------------------------------------------------------------------------ + +// TestRollupsMachine runs all the tests for the rollupsmachine package. +func TestRollupsMachine(t *testing.T) { + suite.Run(t, new(RollupsMachineSuite)) +} + +type RollupsMachineSuite struct{ suite.Suite } + +func (s *RollupsMachineSuite) TestLoad() { suite.Run(s.T(), new(LoadSuite)) } +func (s *RollupsMachineSuite) TestFork() { suite.Run(s.T(), new(ForkSuite)) } +func (s *RollupsMachineSuite) TestAdvance() { suite.Run(s.T(), new(AdvanceSuite)) } +func (s *RollupsMachineSuite) TestInspect() { suite.Run(s.T(), new(InspectSuite)) } +func (s *RollupsMachineSuite) TestCycles() { suite.Run(s.T(), new(CyclesSuite)) } + +// ------------------------------------------------------------------------------------------------ + +// Missing: +// - "could not create the remote machine manager" +// - "could not read iflagsY" +// - "could not read the yield reason" +// - machine.Close() +type LoadSuite struct { + suite.Suite + address string + + acceptSnapshot *snapshot.Snapshot + rejectSnapshot *snapshot.Snapshot + exceptionSnapshot *snapshot.Snapshot + noticeSnapshot *snapshot.Snapshot +} + +func (s *LoadSuite) SetupSuite() { + var ( + require = s.Require() + script string + err error + ) + + script = "rollup accept" + s.acceptSnapshot, err = snapshot.FromScript(script, cycles) + require.Nil(err) + require.Equal(emulator.BreakReasonYieldedManually, s.acceptSnapshot.BreakReason) + + script = "rollup reject" + s.rejectSnapshot, err = snapshot.FromScript(script, cycles) + require.Nil(err) + require.Equal(emulator.BreakReasonYieldedManually, s.rejectSnapshot.BreakReason) + + script = payload("Paul Atreides") + " | rollup exception" + s.exceptionSnapshot, err = snapshot.FromScript(script, cycles) + require.Nil(err) + require.Equal(emulator.BreakReasonYieldedManually, s.exceptionSnapshot.BreakReason) + + script = payload("Hari Seldon") + " | rollup notice" + s.noticeSnapshot, err = snapshot.FromScript(script, cycles) + require.Nil(err) + require.Equal(emulator.BreakReasonYieldedAutomatically, s.noticeSnapshot.BreakReason) +} + +func (s *LoadSuite) TearDownSuite() { + s.acceptSnapshot.Close() + s.rejectSnapshot.Close() + s.exceptionSnapshot.Close() + s.noticeSnapshot.Close() +} + +func (s *LoadSuite) SetupTest() { + address, err := StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) + s.Require().Nil(err) + s.address = address +} + +func (s *LoadSuite) TearDownTest() { + err := StopServer(s.address) + s.Require().Nil(err) +} + +func (s *LoadSuite) TestOkAccept() { + require := s.Require() + config := &emulator.MachineRuntimeConfig{} + machine, err := Load(s.acceptSnapshot.Path(), s.address, config) + require.Nil(err) + require.NotNil(machine) +} + +func (s *LoadSuite) TestOkReject() { + require := s.Require() + config := &emulator.MachineRuntimeConfig{} + machine, err := Load(s.rejectSnapshot.Path(), s.address, config) + require.Nil(err) + require.NotNil(machine) +} + +func (s *LoadSuite) TestInvalidAddress() { + require := s.Require() + config := &emulator.MachineRuntimeConfig{} + machine, err := Load(s.acceptSnapshot.Path(), "invalid-address", config) + require.ErrorContains(err, "could not load the machine") + require.ErrorIs(err, ErrCartesiMachine) + require.Nil(machine) +} + +func (s *LoadSuite) TestInvalidPath() { + require := s.Require() + config := &emulator.MachineRuntimeConfig{} + machine, err := Load("invalid-path", s.address, config) + require.ErrorContains(err, "could not load the machine") + require.ErrorIs(err, ErrCartesiMachine) + require.Nil(machine) +} + +func (s *LoadSuite) TestNotAtManualYield() { + require := s.Require() + config := &emulator.MachineRuntimeConfig{} + machine, err := Load(s.noticeSnapshot.Path(), s.address, config) + require.NotNil(err) + require.ErrorIs(err, ErrNotAtManualYield) + require.Nil(machine) +} + +func (s *LoadSuite) TestException() { + require := s.Require() + config := &emulator.MachineRuntimeConfig{} + machine, err := Load(s.exceptionSnapshot.Path(), s.address, config) + require.NotNil(err) + require.ErrorIs(err, ErrException) + require.Nil(machine) +} + +// ------------------------------------------------------------------------------------------------ + +type ForkSuite struct{ suite.Suite } + +func (s *ForkSuite) TestOk() { + require := s.Require() + + // Creates the snapshot. + script := "while true; do rollup accept; done" + snapshot, err := snapshot.FromScript(script, cycles) + require.Nil(err) + require.Equal(emulator.BreakReasonYieldedManually, snapshot.BreakReason) + defer func() { require.Nil(snapshot.Close()) }() + + // Starts the server. + address, err := StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) + require.Nil(err) + + // Loads the machine. + machine, err := Load(snapshot.Path(), address, &emulator.MachineRuntimeConfig{}) + require.Nil(err) + require.NotNil(machine) + defer func() { require.Nil(machine.Close()) }() + + // Forks the machine. + forkMachine, forkAddress, err := machine.Fork() + require.Nil(err) + require.NotNil(forkMachine) + require.NotEqual(address, forkAddress) + require.Nil(forkMachine.Close()) +} + +// ------------------------------------------------------------------------------------------------ + +type AdvanceSuite struct { + suite.Suite + snapshotEcho *snapshot.Snapshot + snapshotReject *snapshot.Snapshot + address string +} + +func (s *AdvanceSuite) SetupSuite() { + var ( + require = s.Require() + script string + err error + ) + + script = "ioctl-echo-loop --vouchers=1 --notices=3 --reports=5 --verbose=1" + s.snapshotEcho, err = snapshot.FromScript(script, cycles) + require.Nil(err) + require.Equal(emulator.BreakReasonYieldedManually, s.snapshotEcho.BreakReason) + + script = "while true; do rollup reject; done" + s.snapshotReject, err = snapshot.FromScript(script, cycles) + require.Nil(err) + require.Equal(emulator.BreakReasonYieldedManually, s.snapshotReject.BreakReason) +} + +func (s *AdvanceSuite) TearDownSuite() { + s.snapshotEcho.Close() + s.snapshotReject.Close() +} + +func (s *AdvanceSuite) SetupTest() { + address, err := StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) + s.Require().Nil(err) + s.address = address +} + +func (s *AdvanceSuite) TestEchoLoop() { + require := s.Require() + + // Loads the machine. + machine, err := Load(s.snapshotEcho.Path(), s.address, &emulator.MachineRuntimeConfig{}) + require.Nil(err) + require.NotNil(machine) + defer func() { require.Nil(machine.Close()) }() + + // Encodes the input. + input := Input{Data: []byte("Ender Wiggin")} + encodedInput, err := input.Encode() + require.Nil(err) + + // Sends the advance-state request. + accepted, outputs, reports, outputsHash, err := machine.Advance(encodedInput) + require.Nil(err) + require.True(accepted) + require.Len(outputs, 4) + require.Len(reports, 5) + require.NotEmpty(outputsHash) + + // Checks the responses. + require.Equal(input.Data, expectVoucher(s.T(), outputs[0]).Data) + for i := 1; i < 4; i++ { + require.Equal(input.Data, expectNotice(s.T(), outputs[i]).Data) + } + for _, report := range reports { + require.Equal(input.Data, report) + } +} + +func (s *AdvanceSuite) TestAcceptRejectException() { + require := s.Require() + + // Creates the snapshot. + script := `rollup accept + rollup accept + rollup reject + echo '{"payload": "0x53616e64776f726d" }' | rollup exception` + snapshot, err := snapshot.FromScript(script, cycles) + require.Nil(err) + require.Equal(emulator.BreakReasonYieldedManually, snapshot.BreakReason) + defer func() { require.Nil(snapshot.Close()) }() + + // Loads the machine. + machine, err := Load(snapshot.Path(), s.address, &emulator.MachineRuntimeConfig{}) + require.Nil(err) + require.NotNil(machine) + defer func() { require.Nil(machine.Close()) }() + + // Encodes the input. + input := Input{Data: []byte("Shai-Hulud")} + encodedInput, err := input.Encode() + require.Nil(err) + + { // Accept. + accepted, outputs, reports, outputsHash, err := machine.Advance(encodedInput) + require.Nil(err) + require.True(accepted) + require.Empty(outputs) + require.Empty(reports) + require.NotEmpty(outputsHash) + } + + { // Reject. + accepted, outputs, reports, outputsHash, err := machine.Advance(encodedInput) + require.Nil(err) + require.False(accepted) + require.Nil(outputs) + require.Empty(reports) + require.Empty(outputsHash) + } + + { // Exception + _, _, _, _, err := machine.Advance(encodedInput) + require.Equal(ErrException, err) + } +} + +func (s *AdvanceSuite) TestHalted() { + require := s.Require() + + // Creates the snapshot. + script := `rollup accept; echo "Done"` + snapshot, err := snapshot.FromScript(script, cycles) + require.Nil(err) + require.Equal(emulator.BreakReasonYieldedManually, snapshot.BreakReason) + defer func() { require.Nil(snapshot.Close()) }() + + // Loads the machine. + machine, err := Load(snapshot.Path(), s.address, &emulator.MachineRuntimeConfig{}) + require.Nil(err) + require.NotNil(machine) + defer func() { require.Nil(machine.Close()) }() + + // Encodes the input. + input := Input{Data: []byte("Fremen")} + encodedInput, err := input.Encode() + require.Nil(err) + + _, _, _, _, err = machine.Advance(encodedInput) + require.Equal(ErrHalted, err) +} + +// ------------------------------------------------------------------------------------------------ + +type CyclesSuite struct { + suite.Suite + snapshot *snapshot.Snapshot + address string + machine *RollupsMachine + input []byte +} + +func (s *CyclesSuite) SetupSuite() { + require := s.Require() + script := "ioctl-echo-loop --vouchers=1 --notices=1 --reports=1 --verbose=1" + snapshot, err := snapshot.FromScript(script, cycles) + require.Nil(err) + require.Equal(emulator.BreakReasonYieldedManually, snapshot.BreakReason) + s.snapshot = snapshot + + quote := `"I must not fear. Fear is the mind-killer." -- Dune, Frank Herbert` + input, err := Input{Data: []byte(quote)}.Encode() + require.Nil(err) + s.input = input +} + +func (s *CyclesSuite) TearDownSuite() { + s.snapshot.Close() +} + +func (s *CyclesSuite) SetupSubTest() { + require := s.Require() + var err error + + s.address, err = StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) + require.Nil(err) + + s.machine, err = Load(s.snapshot.Path(), s.address, &emulator.MachineRuntimeConfig{}) + require.Nil(err) + require.NotNil(s.machine) +} + +func (s *CyclesSuite) TearDownSubTest() { + err := s.machine.Close() + s.Require().Nil(err) +} + +// When we send a request to the machine with machine.Max set too low, +// the function call should return the ErrCycleLimitExceeded error. +func (s *CyclesSuite) TestCycleLimitExceeded() { + // Exits before calling machine.Run. + s.Run("Max=0", func() { + require := s.Require() + s.machine.Max = 0 + _, _, _, _, err := s.machine.Advance(s.input) + require.Equal(ErrCycleLimitExceeded, err) + }) + + // Runs for exactly one cycle. + s.Run("Max=1", func() { + require := s.Require() + s.machine.Max = 1 + _, _, _, _, err := s.machine.Advance(s.input) + require.Equal(ErrCycleLimitExceeded, err) + }) + + // Calls machine.Run many times. + s.Run("Max=100000", func() { + require := s.Require() + s.machine.Max = 100000 + s.machine.Inc = 10000 + _, _, _, _, err := s.machine.Advance(s.input) + require.Equal(ErrCycleLimitExceeded, err) + }) +} + +// ------------------------------------------------------------------------------------------------ + +type InspectSuite struct { + suite.Suite + snapshotEcho *snapshot.Snapshot + address string +} + +func (s *InspectSuite) SetupSuite() { + var ( + require = s.Require() + script string + err error + ) + + script = "ioctl-echo-loop --vouchers=3 --notices=5 --reports=7 --verbose=1" + s.snapshotEcho, err = snapshot.FromScript(script, cycles) + require.Nil(err) + require.Equal(emulator.BreakReasonYieldedManually, s.snapshotEcho.BreakReason) +} + +func (s *InspectSuite) TearDownSuite() { + s.snapshotEcho.Close() +} + +func (s *InspectSuite) SetupTest() { + address, err := StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) + s.Require().Nil(err) + s.address = address +} + +func (s *InspectSuite) TestEchoLoop() { + require := s.Require() + + // Loads the machine. + machine, err := Load(s.snapshotEcho.Path(), s.address, &emulator.MachineRuntimeConfig{}) + require.Nil(err) + require.NotNil(machine) + defer func() { require.Nil(machine.Close()) }() + + query := []byte("Bene Gesserit") + + // Sends the inspect-state request. + accepted, reports, err := machine.Inspect(query) + require.Nil(err) + require.True(accepted) + require.Len(reports, 7) + + // Checks the responses. + for _, report := range reports { + require.Equal(query, report) + } +} + +// ------------------------------------------------------------------------------------------------ + +// expectVoucher decodes the output and asserts that it is a voucher. +func expectVoucher(t *testing.T, output Output) *Voucher { + voucher, notice, err := DecodeOutput(output) + require.Nil(t, err) + require.NotNil(t, voucher) + require.Nil(t, notice) + return voucher +} + +// expectNotice decodes the output and asserts that it is a notice. +func expectNotice(t *testing.T, output Output) *Notice { + voucher, notice, err := DecodeOutput(output) + require.Nil(t, err) + require.Nil(t, voucher) + require.NotNil(t, notice) + return notice +} diff --git a/pkg/rollupsmachine/server.go b/pkg/rollupsmachine/server.go new file mode 100644 index 000000000..d4d29ac56 --- /dev/null +++ b/pkg/rollupsmachine/server.go @@ -0,0 +1,126 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package rollupsmachine + +import ( + "fmt" + "io" + "log/slog" + "os/exec" + "regexp" + "strconv" + + "github.com/cartesi/rollups-node/internal/linewriter" + "github.com/cartesi/rollups-node/pkg/emulator" +) + +type ServerVerbosity string + +const ( + ServerVerbosityTrace ServerVerbosity = "trace" + ServerVerbosityDebug ServerVerbosity = "debug" + ServerVerbosityInfo ServerVerbosity = "info" + ServerVerbosityWarn ServerVerbosity = "warn" + ServerVerbosityError ServerVerbosity = "error" + ServerVerbosityFatal ServerVerbosity = "fatal" +) + +// StartServer starts a JSON RPC remote cartesi machine server. +// +// It configures the server's logging verbosity and initializes its address to localhost:port. +// If verbosity is an invalid LogLevel, a default value will be used instead. +// If port is 0, a random valid port will be used instead. +// +// StartServer also redirects the server's stdout and stderr to the provided io.Writers. +// +// It returns the server's address. +func StartServer(verbosity ServerVerbosity, port uint32, stdout, stderr io.Writer) (string, error) { + // Configures the command's arguments. + args := []string{} + if verbosity.valid() { + args = append(args, "--log-level="+string(verbosity)) + } + if port != 0 { + args = append(args, fmt.Sprintf("--server-address=localhost:%d", port)) + } + + // Creates the command. + cmd := exec.Command("jsonrpc-remote-cartesi-machine", args...) + + // Redirects stdout and stderr. + intercepter := portIntercepter{ + inner: stderr, + port: make(chan uint32), + found: new(bool), + } + cmd.Stdout = stdout + cmd.Stderr = linewriter.New(intercepter) + + // Starts the server. + slog.Info("running", "command", cmd.String()) + if err := cmd.Start(); err != nil { + return "", err + } + + // Waits for the intercepter to write the port to the channel. + if actualPort := <-intercepter.port; port == 0 { + port = actualPort + } else if port != actualPort { + panic(fmt.Sprintf("mismatching ports (%d != %d)", port, actualPort)) + } + + return fmt.Sprintf("localhost:%d", port), nil +} + +// StopServer shuts down the JSON RPC remote cartesi machine server hosted at address. +func StopServer(address string) error { + slog.Info("Stopping server at", "address", address) + remote, err := emulator.NewRemoteMachineManager(address) + if err != nil { + return err + } + defer remote.Delete() + return remote.Shutdown() +} + +// ------------------------------------------------------------------------------------------------ + +func (verbosity ServerVerbosity) valid() bool { + return verbosity == ServerVerbosityTrace || + verbosity == ServerVerbosityDebug || + verbosity == ServerVerbosityInfo || + verbosity == ServerVerbosityWarn || + verbosity == ServerVerbosityError || + verbosity == ServerVerbosityFatal +} + +// portIntercepter sends the server's port through the port channel as soon as it reads it. +// It then closes the channel and keeps on writing to the inner writer. +// +// It expects to be wrapped by a linewriter.LineWriter. +type portIntercepter struct { + inner io.Writer + port chan uint32 + found *bool +} + +var regex = regexp.MustCompile("initial server bound to port ([0-9]+)") + +func (writer portIntercepter) Write(p []byte) (n int, err error) { + if *writer.found { + return writer.inner.Write(p) + } else { + matches := regex.FindStringSubmatch(string(p)) + if matches != nil { + port, err := strconv.ParseUint(matches[1], 10, 32) + if err != nil { + return 0, err + } + *writer.found = true + writer.port <- uint32(port) + close(writer.port) + } + return writer.inner.Write(p) + } +} From 377aef774100cc0224d2f6f1a0e3816d0003547f Mon Sep 17 00:00:00 2001 From: Renan Santos Date: Thu, 8 Aug 2024 15:46:24 -0300 Subject: [PATCH 2/3] refactor: add unit tests to the rollupsmachine --- pkg/emulator/pma.go | 3 + pkg/rollupsmachine/abi.json | 27 + .../cartesimachine/interface.go | 30 + pkg/rollupsmachine/cartesimachine/machine.go | 235 ++++ pkg/rollupsmachine/cartesimachine/server.go | 126 ++ pkg/rollupsmachine/error.go | 29 +- pkg/rollupsmachine/io.go | 82 +- pkg/rollupsmachine/machine.go | 434 +++---- pkg/rollupsmachine/machine_test.go | 1019 ++++++++++++++--- pkg/rollupsmachine/server.go | 18 +- 10 files changed, 1502 insertions(+), 501 deletions(-) create mode 100644 pkg/rollupsmachine/abi.json create mode 100644 pkg/rollupsmachine/cartesimachine/interface.go create mode 100644 pkg/rollupsmachine/cartesimachine/machine.go create mode 100644 pkg/rollupsmachine/cartesimachine/server.go diff --git a/pkg/emulator/pma.go b/pkg/emulator/pma.go index 36c9205f7..ed28f972e 100644 --- a/pkg/emulator/pma.go +++ b/pkg/emulator/pma.go @@ -9,4 +9,7 @@ import "C" const ( CmioRxBufferStart uint64 = C.PMA_CMIO_RX_BUFFER_START_DEF CmioTxBufferStart uint64 = C.PMA_CMIO_TX_BUFFER_START_DEF + + CmioRxBufferLog2Size uint64 = C.PMA_CMIO_RX_BUFFER_LOG2_SIZE_DEF + CmioTxBufferLog2Size uint64 = C.PMA_CMIO_TX_BUFFER_LOG2_SIZE_DEF ) diff --git a/pkg/rollupsmachine/abi.json b/pkg/rollupsmachine/abi.json new file mode 100644 index 000000000..8e84bb97a --- /dev/null +++ b/pkg/rollupsmachine/abi.json @@ -0,0 +1,27 @@ +[{ + "type" : "function", + "name" : "EvmAdvance", + "inputs" : [ + { "type" : "uint256" }, + { "type" : "address" }, + { "type" : "address" }, + { "type" : "uint256" }, + { "type" : "uint256" }, + { "type" : "uint256" }, + { "type" : "bytes" } + ] +}, { + "type" : "function", + "name" : "Voucher", + "inputs" : [ + { "type" : "address" }, + { "type" : "uint256" }, + { "type" : "bytes" } + ] +}, { + "type" : "function", + "name" : "Notice", + "inputs" : [ + { "type" : "bytes" } + ] +}] diff --git a/pkg/rollupsmachine/cartesimachine/interface.go b/pkg/rollupsmachine/cartesimachine/interface.go new file mode 100644 index 000000000..87fd3c9e2 --- /dev/null +++ b/pkg/rollupsmachine/cartesimachine/interface.go @@ -0,0 +1,30 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +// Package cartesimachine abstracts into an interface the functionalities expected from a machine +// library. It provides an implementation for that interface using the emulator package. +package cartesimachine + +import "github.com/cartesi/rollups-node/pkg/emulator" + +type ( + RequestType uint8 + YieldReason uint8 +) + +type CartesiMachine interface { + Fork() (CartesiMachine, error) + Continue() error + Run(until uint64) (emulator.BreakReason, error) + Close() error + + IsAtManualYield() (bool, error) + ReadYieldReason() (emulator.HtifYieldReason, error) + ReadHash() ([32]byte, error) + ReadCycle() (uint64, error) + ReadMemory() ([]byte, error) + WriteRequest([]byte, RequestType) error + + PayloadLengthLimit() uint + Address() string +} diff --git a/pkg/rollupsmachine/cartesimachine/machine.go b/pkg/rollupsmachine/cartesimachine/machine.go new file mode 100644 index 000000000..bf8cd3972 --- /dev/null +++ b/pkg/rollupsmachine/cartesimachine/machine.go @@ -0,0 +1,235 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package cartesimachine + +import ( + "errors" + "fmt" + "math" + + "github.com/cartesi/rollups-node/pkg/emulator" +) + +const ( + AdvanceStateRequest RequestType = 0 + InspectStateRequest RequestType = 1 +) + +var ( + ErrCartesiMachine = errors.New("cartesi machine internal error") + + ErrOrphanServer = errors.New("cartesi machine server was left orphan") +) + +type cartesiMachine struct { + inner *emulator.Machine + server *emulator.RemoteMachineManager + + address string // address of the JSON RPC remote cartesi machine server +} + +// Load loads the machine stored at path into the remote server from address. +func Load(path, address string, config *emulator.MachineRuntimeConfig) (CartesiMachine, error) { + machine := &cartesiMachine{address: address} + + // Creates the server machine manager (the server's manager). + server, err := emulator.NewRemoteMachineManager(address) + if err != nil { + err = fmt.Errorf("could not create the remote machine manager: %w", err) + return nil, errCartesiMachine(err) + } + machine.server = server + + // Loads the machine stored at path into the server. + inner, err := server.LoadMachine(path, config) + if err != nil { + defer machine.server.Delete() + err = fmt.Errorf("could not load the machine: %w", err) + return nil, errCartesiMachine(err) + } + machine.inner = inner + + return machine, nil +} + +// Fork forks the machine. +// +// When Fork returns with the ErrOrphanServer error, it also returns with a non-nil CartesiMachine +// the can be used to retrieve the orphan server's address. +func (machine *cartesiMachine) Fork() (CartesiMachine, error) { + newMachine := new(cartesiMachine) + + // Forks the server. + address, err := machine.server.Fork() + if err != nil { + err = fmt.Errorf("could not fork the machine: %w", err) + return nil, errCartesiMachine(err) + } + newMachine.address = address + + // Instantiates the new remote machine manager. + server, err := emulator.NewRemoteMachineManager(address) + if err != nil { + err = fmt.Errorf("could not create the new remote machine manager: %w", err) + errOrphanServer := errOrphanServerWithAddress(address) + return newMachine, errors.Join(ErrCartesiMachine, err, errOrphanServer) + } + newMachine.server = server + + // Gets the inner machine reference from the server. + inner, err := newMachine.server.GetMachine() + if err != nil { + err = fmt.Errorf("could not get the machine from the server: %w", err) + return newMachine, errors.Join(ErrCartesiMachine, err, newMachine.closeServer()) + } + newMachine.inner = inner + + return newMachine, nil +} + +func (machine *cartesiMachine) Run(until uint64) (emulator.BreakReason, error) { + breakReason, err := machine.inner.Run(until) + if err != nil { + assert(breakReason == emulator.BreakReasonFailed, breakReason.String()) + err = fmt.Errorf("machine run failed: %w", err) + return breakReason, errCartesiMachine(err) + } + return breakReason, nil +} + +func (machine *cartesiMachine) IsAtManualYield() (bool, error) { + iflagsY, err := machine.inner.ReadIFlagsY() + if err != nil { + err = fmt.Errorf("could not read iflagsY: %w", err) + return iflagsY, errCartesiMachine(err) + } + return iflagsY, nil +} + +func (machine *cartesiMachine) ReadYieldReason() (emulator.HtifYieldReason, error) { + tohost, err := machine.readHtifToHostData() + if err != nil { + return emulator.HtifYieldReason(0), err + } + yieldReason := tohost >> 32 //nolint:mnd + return emulator.HtifYieldReason(yieldReason), nil +} + +func (machine *cartesiMachine) ReadHash() ([32]byte, error) { + hash, err := machine.inner.GetRootHash() + if err != nil { + err := fmt.Errorf("could not get the machine's root hash: %w", err) + return hash, errCartesiMachine(err) + } + return hash, nil +} + +func (machine *cartesiMachine) ReadMemory() ([]byte, error) { + tohost, err := machine.readHtifToHostData() + if err != nil { + return nil, err + } + length := tohost & 0x00000000ffffffff //nolint:mnd + + read, err := machine.inner.ReadMemory(emulator.CmioTxBufferStart, length) + if err != nil { + err := fmt.Errorf("could not read from the memory: %w", err) + return nil, errCartesiMachine(err) + } + + return read, nil +} + +func (machine *cartesiMachine) WriteRequest(data []byte, type_ RequestType) error { + // Writes the request's data. + err := machine.inner.WriteMemory(emulator.CmioRxBufferStart, data) + if err != nil { + err := fmt.Errorf("could not write to the memory: %w", err) + return errCartesiMachine(err) + } + + // Writes the request's type and length. + fromhost := ((uint64(type_) << 32) | (uint64(len(data)) & 0xffffffff)) //nolint:mnd + err = machine.inner.WriteHtifFromHostData(fromhost) + if err != nil { + err := fmt.Errorf("could not write HTIF fromhost data: %w", err) + return errCartesiMachine(err) + } + + return nil +} + +func (machine *cartesiMachine) Continue() error { + err := machine.inner.ResetIFlagsY() + if err != nil { + err = fmt.Errorf("could not reset iflagsY: %w", err) + return errCartesiMachine(err) + } + return nil +} + +func (machine *cartesiMachine) ReadCycle() (uint64, error) { + cycle, err := machine.inner.ReadMCycle() + if err != nil { + err = fmt.Errorf("could not read the machine's current cycle: %w", err) + return cycle, errCartesiMachine(err) + } + return cycle, nil +} + +func (machine cartesiMachine) PayloadLengthLimit() uint { + expo := float64(emulator.CmioRxBufferLog2Size) + var payloadLengthLimit = uint(math.Pow(2, expo)) //nolint:mnd + return payloadLengthLimit +} + +func (machine cartesiMachine) Address() string { + return machine.address +} + +// Close closes the cartesi machine. It also shuts down the remote cartesi machine server. +func (machine *cartesiMachine) Close() error { + machine.inner.Delete() + machine.inner = nil + return machine.closeServer() +} + +// ------------------------------------------------------------------------------------------------ + +// closeServer shuts down the server and deletes its reference. +func (machine *cartesiMachine) closeServer() error { + err := machine.server.Shutdown() + if err != nil { + err = fmt.Errorf("could not shut down the server: %w", err) + err = errors.Join(errCartesiMachine(err), errOrphanServerWithAddress(machine.address)) + } + machine.server.Delete() + machine.server = nil + return err +} + +func (machine *cartesiMachine) readHtifToHostData() (uint64, error) { + tohost, err := machine.inner.ReadHtifToHostData() + if err != nil { + err = fmt.Errorf("could not read HTIF tohost data: %w", err) + return tohost, errCartesiMachine(err) + } + return tohost, nil +} + +// ------------------------------------------------------------------------------------------------ + +func errCartesiMachine(err error) error { + return errors.Join(ErrCartesiMachine, err) +} + +func errOrphanServerWithAddress(address string) error { + return fmt.Errorf("%w at address %s", ErrOrphanServer, address) +} + +func assert(condition bool, s string) { + if !condition { + panic("assertion error: " + s) + } +} diff --git a/pkg/rollupsmachine/cartesimachine/server.go b/pkg/rollupsmachine/cartesimachine/server.go new file mode 100644 index 000000000..b7c131bf0 --- /dev/null +++ b/pkg/rollupsmachine/cartesimachine/server.go @@ -0,0 +1,126 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package cartesimachine + +import ( + "fmt" + "io" + "log/slog" + "os/exec" + "regexp" + "strconv" + + "github.com/cartesi/rollups-node/internal/linewriter" + "github.com/cartesi/rollups-node/pkg/emulator" +) + +type ServerVerbosity string + +const ( + ServerVerbosityTrace ServerVerbosity = "trace" + ServerVerbosityDebug ServerVerbosity = "debug" + ServerVerbosityInfo ServerVerbosity = "info" + ServerVerbosityWarn ServerVerbosity = "warn" + ServerVerbosityError ServerVerbosity = "error" + ServerVerbosityFatal ServerVerbosity = "fatal" +) + +// StartServer starts a JSON RPC remote cartesi machine server. +// +// It configures the server's logging verbosity and initializes its address to 127.0.0.1:port. +// If verbosity is an invalid LogLevel, a default value will be used instead. +// If port is 0, a random valid port will be used instead. +// +// StartServer also redirects the server's stdout and stderr to the provided io.Writers. +// +// It returns the server's address. +func StartServer(verbosity ServerVerbosity, port uint32, stdout, stderr io.Writer) (string, error) { + // Configures the command's arguments. + args := []string{} + if verbosity.valid() { + args = append(args, "--log-level="+string(verbosity)) + } + if port != 0 { + args = append(args, fmt.Sprintf("--server-address=127.0.0.1:%d", port)) + } + + // Creates the command. + cmd := exec.Command("jsonrpc-remote-cartesi-machine", args...) + + // Redirects stdout and stderr. + interceptor := portInterceptor{ + inner: stderr, + port: make(chan uint32), + found: new(bool), + } + cmd.Stdout = stdout + cmd.Stderr = linewriter.New(interceptor) + + // Starts the server. + slog.Info("running", "command", cmd.String()) + if err := cmd.Start(); err != nil { + return "", err + } + + // Waits for the interceptor to write the port to the channel. + if actualPort := <-interceptor.port; port == 0 { + port = actualPort + } else if port != actualPort { + panic(fmt.Sprintf("mismatching ports (%d != %d)", port, actualPort)) + } + + return fmt.Sprintf("127.0.0.1:%d", port), nil +} + +// StopServer shuts down the JSON RPC remote cartesi machine server hosted at address. +func StopServer(address string) error { + slog.Info("Stopping server at", "address", address) + remote, err := emulator.NewRemoteMachineManager(address) + if err != nil { + return err + } + defer remote.Delete() + return remote.Shutdown() +} + +// ------------------------------------------------------------------------------------------------ + +func (verbosity ServerVerbosity) valid() bool { + return verbosity == ServerVerbosityTrace || + verbosity == ServerVerbosityDebug || + verbosity == ServerVerbosityInfo || + verbosity == ServerVerbosityWarn || + verbosity == ServerVerbosityError || + verbosity == ServerVerbosityFatal +} + +// portInterceptor sends the server's port through the port channel as soon as it reads it. +// It then closes the channel and keeps on writing to the inner writer. +// +// It expects to be wrapped by a linewriter.LineWriter. +type portInterceptor struct { + inner io.Writer + port chan uint32 + found *bool +} + +var portRegex = regexp.MustCompile("remote machine bound to [^:]+:([0-9]+)") + +func (writer portInterceptor) Write(p []byte) (n int, err error) { + if *writer.found { + return writer.inner.Write(p) + } else { + matches := portRegex.FindStringSubmatch(string(p)) + if matches != nil { + port, err := strconv.ParseUint(matches[1], 10, 32) + if err != nil { + return 0, err + } + *writer.found = true + writer.port <- uint32(port) + close(writer.port) + } + return writer.inner.Write(p) + } +} diff --git a/pkg/rollupsmachine/error.go b/pkg/rollupsmachine/error.go index b0c209cd5..e683d3179 100644 --- a/pkg/rollupsmachine/error.go +++ b/pkg/rollupsmachine/error.go @@ -10,21 +10,16 @@ import ( "github.com/cartesi/rollups-node/internal/node/model" ) -const unreachable = "internal error: entered unreacheable code" - var ( - ErrCartesiMachine = errors.New("cartesi machine internal error") - - // Misc. - ErrException = errors.New("last request yielded an exception") - ErrHalted = errors.New("machine halted") - ErrProgress = errors.New("machine yielded progress") - ErrSoftYield = errors.New("machine yielded softly") - ErrCycleLimitExceeded = errors.New("cycle limit exceeded") - ErrOutputsLimitExceeded = errors.New("outputs length limit exceeded") - // ErrPayloadLengthLimitExceeded = errors.New("payload length limit exceeded") + ErrUnreachable = errors.New("internal error: entered unreachable code") - ErrOrphanServer = errors.New("cartesi machine server was left orphan") + ErrException = errors.New("last request yielded an exception") + ErrHalted = errors.New("machine halted") + ErrProgress = errors.New("machine yielded progress") + ErrSoftYield = errors.New("machine yielded softly") + ErrCycleLimitExceeded = errors.New("cycle limit exceeded") + ErrOutputsLimitExceeded = errors.New("outputs limit exceeded") + ErrPayloadLengthLimitExceeded = errors.New("payload length limit exceeded") // Load ErrNotAtManualYield = errors.New("not at manual yield") @@ -32,11 +27,3 @@ var ( // Advance ErrHashLength = fmt.Errorf("hash does not have exactly %d bytes", model.HashLength) ) - -func errOrphanServerWithAddress(address string) error { - return fmt.Errorf("%w at address %s", ErrOrphanServer, address) -} - -func errCartesiMachine(err error) error { - return errors.Join(ErrCartesiMachine, err) -} diff --git a/pkg/rollupsmachine/io.go b/pkg/rollupsmachine/io.go index 17d00d9b4..f25f6b0f8 100644 --- a/pkg/rollupsmachine/io.go +++ b/pkg/rollupsmachine/io.go @@ -4,6 +4,7 @@ package rollupsmachine import ( + _ "embed" "fmt" "math/big" "strings" @@ -12,10 +13,26 @@ import ( "github.com/ethereum/go-ethereum/common" ) +var ( + //go:embed abi.json + jsonABI string + + ioABI abi.ABI +) + +func init() { + var err error + ioABI, err = abi.JSON(strings.NewReader(jsonABI)) + if err != nil { + panic(err) + } +} + +// An Input is sent by a advance-state request. type Input struct { ChainId uint64 - AppContract [20]byte - Sender [20]byte + AppContract Address + Sender Address BlockNumber uint64 BlockTimestamp uint64 // PrevRandao uint64 @@ -23,20 +40,24 @@ type Input struct { Data []byte } +// A Query is sent by a inspect-state request. type Query struct { Data []byte } +// A Voucher is a type of machine output. type Voucher struct { - Address [20]byte + Address Address Value *big.Int Data []byte } +// A Notice is a type of machine output. type Notice struct { Data []byte } +// Encode encodes an input. func (input Input) Encode() ([]byte, error) { chainId := new(big.Int).SetUint64(input.ChainId) appContract := common.BytesToAddress(input.AppContract[:]) @@ -49,19 +70,7 @@ func (input Input) Encode() ([]byte, error) { index, input.Data) } -func (query Query) Encode() ([]byte, error) { - return query.Data, nil -} - -func decodeArguments(payload []byte) (arguments []any, _ error) { - method, err := ioABI.MethodById(payload) - if err != nil { - return nil, err - } - - return method.Inputs.Unpack(payload[4:]) -} - +// DecodeOutput decodes an output into either a voucher or a notice. func DecodeOutput(payload []byte) (*Voucher, *Notice, error) { arguments, err := decodeArguments(payload) if err != nil { @@ -74,7 +83,7 @@ func DecodeOutput(payload []byte) (*Voucher, *Notice, error) { return nil, notice, nil case 3: //nolint:mnd voucher := &Voucher{ - Address: [20]byte(arguments[0].(common.Address)), + Address: Address(arguments[0].(common.Address)), Value: arguments[1].(*big.Int), Data: arguments[2].([]byte), } @@ -84,40 +93,13 @@ func DecodeOutput(payload []byte) (*Voucher, *Notice, error) { } } -var ioABI abi.ABI - -func init() { - json := `[{ - "type" : "function", - "name" : "EvmAdvance", - "inputs" : [ - { "type" : "uint256" }, - { "type" : "address" }, - { "type" : "address" }, - { "type" : "uint256" }, - { "type" : "uint256" }, - { "type" : "uint256" }, - { "type" : "bytes" } - ] - }, { - "type" : "function", - "name" : "Voucher", - "inputs" : [ - { "type" : "address" }, - { "type" : "uint256" }, - { "type" : "bytes" } - ] - }, { - "type" : "function", - "name" : "Notice", - "inputs" : [ - { "type" : "bytes" } - ] - }]` +// ------------------------------------------------------------------------------------------------ - var err error - ioABI, err = abi.JSON(strings.NewReader(json)) +func decodeArguments(payload []byte) (arguments []any, _ error) { + method, err := ioABI.MethodById(payload) if err != nil { - panic(err) + return nil, err } + + return method.Inputs.Unpack(payload[4:]) } diff --git a/pkg/rollupsmachine/machine.go b/pkg/rollupsmachine/machine.go index ff4f5649e..1d1d80703 100644 --- a/pkg/rollupsmachine/machine.go +++ b/pkg/rollupsmachine/machine.go @@ -1,216 +1,143 @@ // (c) Cartesi and individual authors (see AUTHORS) // SPDX-License-Identifier: Apache-2.0 (see LICENSE) +// Package rollupsmachine provides the RollupsMachine struct that wraps a +// cartesimachine.CartesiMachine with rollups-oriented functionalities. +// +// Moreover, the Input struct provides a method for encoding payloads, and the DecodeOutput +// function decodes an output into a voucher or a notice. package rollupsmachine import ( - "errors" "fmt" "log/slog" - "github.com/cartesi/rollups-node/internal/node/model" "github.com/cartesi/rollups-node/pkg/emulator" + "github.com/cartesi/rollups-node/pkg/rollupsmachine/cartesimachine" ) -// Convenient type aliases. -type ( - Cycle = uint64 - Output = []byte - Report = []byte -) - -type requestType uint8 - const ( - DefaultInc = Cycle(10000000) - DefaultMax = Cycle(1000000000) - - advanceStateRequest requestType = 0 - inspectStateRequest requestType = 1 + maxOutputs = 65536 // 2^16 + addressLength = 20 + hashLength = 32 +) - maxOutputs = 65536 // 2^16 +// Convenient type aliases. +type ( + Cycle = uint64 + Output = []byte + Report = []byte + Address = [addressLength]byte + Hash = [hashLength]byte ) -// A RollupsMachine wraps an emulator.Machine and provides five basic functions: -// Fork, Destroy, Hash, Advance and Inspect. +// RollupsMachine wraps a cartesimachine.CartesiMachine to provide four core features: forking, +// getting the merkle tree's root hash, sending advance-state requests, and sending inspect-state +// requests. +// +// When processing an advance-state or an inspect-state request, the machine will run in increments +// of inc cycles, for no more than max cycles. type RollupsMachine struct { - // For each request, the machine will run in increments of Inc cycles, - // for no more than Max cycles. - // - // If these fields are left undefined, - // the machine will use the DefaultInc and DefaultMax values. - Inc, Max Cycle + inner cartesimachine.CartesiMachine - address string - - inner *emulator.Machine - remote *emulator.RemoteMachineManager + inc, max Cycle } -// Load loads the machine stored at path into the remote server from address. -// It then checks if the machine is in a valid state to receive advance and inspect requests. -func Load(path, address string, config *emulator.MachineRuntimeConfig) (*RollupsMachine, error) { - // Creates the machine with default values for Inc and Max. - machine := &RollupsMachine{Inc: DefaultInc, Max: DefaultMax, address: address} - - // Creates the remote machine manager. - remote, err := emulator.NewRemoteMachineManager(address) - if err != nil { - err = fmt.Errorf("could not create the remote machine manager: %w", err) - return nil, errCartesiMachine(err) - } - machine.remote = remote - - // Loads the machine stored at path into the server. - // Creates the inner machine reference. - inner, err := remote.LoadMachine(path, config) - if err != nil { - defer machine.remote.Delete() - err = fmt.Errorf("could not load the machine: %w", err) - return nil, errCartesiMachine(err) - } - machine.inner = inner +// New checks if the provided cartesimachine.CartesiMachine is in a valid state to receive advance +// and inspect requests. If so, New returns a RollupsMachine that wraps the +// cartesimachine.CartesiMachine. +func New(inner cartesimachine.CartesiMachine, inc, max Cycle) (*RollupsMachine, error) { + machine := &RollupsMachine{inner: inner, inc: inc, max: max} // Ensures that the machine is at a manual yield. - isAtManualYield, err := machine.inner.ReadIFlagsY() + isAtManualYield, err := machine.inner.IsAtManualYield() if err != nil { - defer machine.remote.Delete() - err = fmt.Errorf("could not read iflagsY: %w", err) - return nil, errors.Join(errCartesiMachine(err), machine.closeInner()) + return nil, err } if !isAtManualYield { - defer machine.remote.Delete() - return nil, errors.Join(ErrNotAtManualYield, machine.closeInner()) + return nil, ErrNotAtManualYield } - // Ensures that the last request the machine received did not yield and exception. + // Ensures that the last request the machine received did not yield an exception. _, err = machine.lastRequestWasAccepted() if err != nil { - defer machine.remote.Delete() - return nil, errors.Join(err, machine.closeInner()) + return nil, err } return machine, nil } -// Fork forks an existing cartesi machine. -func (machine *RollupsMachine) Fork() (_ *RollupsMachine, address string, _ error) { - // Creates the new machine based on the old machine. - newMachine := &RollupsMachine{Inc: machine.Inc, Max: machine.Max} - - // Forks the remote server's process. - address, err := machine.remote.Fork() - if err != nil { - err = fmt.Errorf("could not fork the machine: %w", err) - return nil, address, errCartesiMachine(err) - } - - // Instantiates the new remote machine manager. - newMachine.remote, err = emulator.NewRemoteMachineManager(address) - if err != nil { - err = fmt.Errorf("could not create the new remote machine manager: %w", err) - errOrphanServer := errOrphanServerWithAddress(address) - return nil, address, errors.Join(errCartesiMachine(err), errOrphanServer) - } - - // Gets the inner machine reference from the remote server. - newMachine.inner, err = newMachine.remote.GetMachine() +// Fork forks the machine. +func (machine *RollupsMachine) Fork() (*RollupsMachine, error) { + inner, err := machine.inner.Fork() if err != nil { - err = fmt.Errorf("could not get the machine from the server: %w", err) - return nil, address, errors.Join(errCartesiMachine(err), newMachine.closeServer()) + return nil, err } - - return newMachine, address, nil + return &RollupsMachine{inner: inner, inc: machine.inc, max: machine.max}, nil } // Hash returns the machine's merkle tree root hash. -func (machine RollupsMachine) Hash() (model.Hash, error) { - hash, err := machine.inner.GetRootHash() - if err != nil { - err := fmt.Errorf("could not get the machine's root hash: %w", err) - return model.Hash(hash), errCartesiMachine(err) - } - return model.Hash(hash), nil +func (machine RollupsMachine) Hash() (Hash, error) { + return machine.inner.ReadHash() } -// Advance sends an input to the cartesi machine. +// Advance sends an input to the machine. // It returns a boolean indicating whether or not the request was accepted. // It also returns the corresponding outputs, reports, and the hash of the outputs. -// -// If the request was not accepted, the function does not return outputs. -func (machine *RollupsMachine) Advance(input []byte) (bool, []Output, []Report, model.Hash, error) { - var outputsHash model.Hash - - accepted, outputs, reports, err := machine.process(input, advanceStateRequest) +// If the request is not accepted, the function does not return outputs. +func (machine *RollupsMachine) Advance(input []byte) (bool, []Output, []Report, Hash, error) { + accepted, outputs, reports, err := machine.process(input, cartesimachine.AdvanceStateRequest) if err != nil { - return accepted, outputs, reports, outputsHash, err + return accepted, outputs, reports, Hash{}, err } if !accepted { - return accepted, nil, reports, model.Hash{}, nil + return accepted, nil, reports, Hash{}, nil } else { - hashBytes, err := machine.readMemory() + hashBytes, err := machine.inner.ReadMemory() if err != nil { - err := fmt.Errorf("could not read the outputs' hash from the memory: %w", err) - return accepted, outputs, reports, outputsHash, errCartesiMachine(err) + err = fmt.Errorf("could not read the outputs' hash: %w", err) + return accepted, outputs, reports, Hash{}, err } - if length := len(hashBytes); length != model.HashLength { - err := fmt.Errorf("%w (it has %d bytes)", ErrHashLength, length) - return accepted, outputs, reports, outputsHash, err + if length := len(hashBytes); length != hashLength { + err = fmt.Errorf("%w (it has %d bytes)", ErrHashLength, length) + return accepted, outputs, reports, Hash{}, err } + var outputsHash Hash copy(outputsHash[:], hashBytes) - return accepted, outputs, reports, outputsHash, nil } } -// Inspect sends a query to the cartesi machine. +// Inspect sends a query to the machine. // It returns a boolean indicating whether or not the request was accepted // It also returns the corresponding reports. func (machine *RollupsMachine) Inspect(query []byte) (bool, []Report, error) { - accepted, _, reports, err := machine.process(query, inspectStateRequest) + accepted, _, reports, err := machine.process(query, cartesimachine.InspectStateRequest) return accepted, reports, err } -// Close destroys the inner cartesi machine, deletes its reference, -// shutsdown the server, and deletes the server's reference. +// Close closes the inner cartesi machine. +// It returns nil if the machine has already been closed. func (machine *RollupsMachine) Close() error { - return errors.Join(machine.closeInner(), machine.closeServer()) -} - -// ------------------------------------------------------------------------------------------------ - -// closeInner destroys the machine and deletes its reference. -func (machine *RollupsMachine) closeInner() error { - defer machine.inner.Delete() - err := machine.inner.Destroy() - if err != nil { - err = fmt.Errorf("could not destroy the machine: %w", err) - err = errCartesiMachine(err) + if machine.inner == nil { + return nil } + err := machine.inner.Close() + machine.inner = nil return err } -// closeServer shutsdown the server and deletes its reference. -func (machine *RollupsMachine) closeServer() error { - defer machine.remote.Delete() - err := machine.remote.Shutdown() - if err != nil { - err = fmt.Errorf("could not shutdown the server: %w", err) - err = errCartesiMachine(err) - err = errors.Join(err, errOrphanServerWithAddress(machine.address)) - } - return err -} +// ------------------------------------------------------------------------------------------------ // lastRequestWasAccepted returns true if the last request was accepted and false otherwise. +// It returns the ErrException error if the last request yielded an exception. // // The machine MUST be at a manual yield when calling this function. func (machine *RollupsMachine) lastRequestWasAccepted() (bool, error) { - yieldReason, err := machine.readYieldReason() + yieldReason, err := machine.inner.ReadYieldReason() if err != nil { - err := fmt.Errorf("could not read the yield reason: %w", err) - return false, errCartesiMachine(err) + return false, err } switch yieldReason { //nolint:exhaustive case emulator.ManualYieldReasonAccepted: @@ -220,7 +147,7 @@ func (machine *RollupsMachine) lastRequestWasAccepted() (bool, error) { case emulator.ManualYieldReasonException: return false, ErrException default: - panic(unreachable) + panic(ErrUnreachable) } } @@ -231,31 +158,25 @@ func (machine *RollupsMachine) lastRequestWasAccepted() (bool, error) { // and leaves the machine in a state ready to receive requests after an execution with no errors. func (machine *RollupsMachine) process( request []byte, - requestType requestType, + requestType cartesimachine.RequestType, ) (accepted bool, _ []Output, _ []Report, _ error) { - // Writes the request's data. - err := machine.inner.WriteMemory(emulator.CmioRxBufferStart, request) - if err != nil { - err := fmt.Errorf("could not write the request's data to the memory: %w", err) - return false, nil, nil, errCartesiMachine(err) + if length := uint(len(request)); length > machine.inner.PayloadLengthLimit() { + return false, nil, nil, ErrPayloadLengthLimitExceeded } - // Writes the request's type and length. - fromhost := ((uint64(requestType) << 32) | (uint64(len(request)) & 0xffffffff)) //nolint:mnd - err = machine.inner.WriteHtifFromHostData(fromhost) + // Writes the request. + err := machine.inner.WriteRequest(request, requestType) if err != nil { - err := fmt.Errorf("could not write HTIF fromhost data: %w", err) - return false, nil, nil, errCartesiMachine(err) + return false, nil, nil, err } // Green-lights the machine to keep running. - err = machine.inner.ResetIFlagsY() + err = machine.inner.Continue() if err != nil { - err := fmt.Errorf("could not reset iflagsY: %w", err) - return false, nil, nil, errCartesiMachine(err) + return false, nil, nil, err } - outputs, reports, err := machine.runAndCollect() + outputs, reports, err := machine.run() if err != nil { return false, outputs, reports, err } @@ -265,159 +186,132 @@ func (machine *RollupsMachine) process( return accepted, outputs, reports, err } -// runAndCollect runs the machine until it manually yields. +type yieldType uint8 + +const ( + manualYield yieldType = iota + automaticYield +) + +// run runs the machine until it manually yields. // It returns any collected responses. -func (machine *RollupsMachine) runAndCollect() ([]Output, []Report, error) { - startingCycle, err := machine.readMachineCycle() +func (machine *RollupsMachine) run() ([]Output, []Report, error) { + currentCycle, err := machine.inner.ReadCycle() if err != nil { - err := fmt.Errorf("could not read the machine's cycle: %w", err) - return nil, nil, errCartesiMachine(err) + return nil, nil, err } - maxCycle := startingCycle + machine.Max - slog.Debug("runAndCollect", - "startingCycle", startingCycle, - "maxCycle", maxCycle, - "leftover", maxCycle-startingCycle) + + limitCycle := currentCycle + machine.max + slog.Debug("run", + "startingCycle", currentCycle, + "limitCycle", limitCycle, + "leftover", limitCycle-currentCycle) outputs := []Output{} reports := []Report{} + for { - var ( - breakReason emulator.BreakReason - err error - ) - breakReason, startingCycle, err = machine.run(startingCycle, maxCycle) - if err != nil { - return outputs, reports, err + var yt *yieldType + var err error + + // Steps the machine as many times as needed until it manually/automatically yields. + for yt == nil { + yt, currentCycle, err = machine.step(currentCycle, limitCycle) + if err != nil { + return outputs, reports, err + } } - switch breakReason { //nolint:exhaustive - case emulator.BreakReasonYieldedManually: - return outputs, reports, nil // returns with the responses - case emulator.BreakReasonYieldedAutomatically: - break // breaks from the switch to read the outputs/reports - default: - panic(unreachable) + // Returns with the responses when the machine manually yields. + if *yt == manualYield { + return outputs, reports, nil } - yieldReason, err := machine.readYieldReason() + // Asserts the machine yielded automatically. + if *yt != automaticYield { + panic(ErrUnreachable) + } + + yieldReason, err := machine.inner.ReadYieldReason() if err != nil { - err := fmt.Errorf("could not read the yield reason: %w", err) - return outputs, reports, errCartesiMachine(err) + return outputs, reports, err } switch yieldReason { //nolint:exhaustive case emulator.AutomaticYieldReasonProgress: return outputs, reports, ErrProgress case emulator.AutomaticYieldReasonOutput: - output, err := machine.readMemory() + output, err := machine.inner.ReadMemory() if err != nil { - err := fmt.Errorf("could not read the output from the memory: %w", err) - return outputs, reports, errCartesiMachine(err) + return outputs, reports, fmt.Errorf("could not read the output: %w", err) } if len(outputs) == maxOutputs { return outputs, reports, ErrOutputsLimitExceeded } outputs = append(outputs, output) case emulator.AutomaticYieldReasonReport: - report, err := machine.readMemory() + report, err := machine.inner.ReadMemory() if err != nil { - err := fmt.Errorf("could not read the report from the memory: %w", err) - return outputs, reports, errCartesiMachine(err) + return outputs, reports, fmt.Errorf("could not read the report: %w", err) } reports = append(reports, report) default: - panic(unreachable) + panic(ErrUnreachable) } } } -// run runs the machine until it yields. -// -// If there are no errors, it returns one of two possible break reasons: -// - emulator.BreakReasonYieldedManually -// - emulator.BreakReasonYieldedAutomatically -// -// Otherwise, it returns one of four possible errors: -// - ErrCartesiMachine -// - ErrHalted -// - ErrSoftYield -// - ErrCycleLimitExceeded -func (machine *RollupsMachine) run( - startingCycle Cycle, - maxCycle Cycle, -) (emulator.BreakReason, Cycle, error) { - currentCycle := startingCycle - slog.Debug("run", "startingCycle", startingCycle, "leftover", maxCycle-startingCycle) - - for { - // Calculates the increment. - increment := min(machine.Inc, maxCycle-currentCycle) - - // Returns with an error if the next run would exceed Max cycles. - if currentCycle+increment >= maxCycle { - return emulator.BreakReasonReachedTargetMcycle, currentCycle, ErrCycleLimitExceeded - } - - // Runs the machine. - breakReason, err := machine.inner.Run(currentCycle + increment) - if err != nil { - assert(breakReason == emulator.BreakReasonFailed, breakReason.String()) - err := fmt.Errorf("machine run failed: %w", err) - return breakReason, currentCycle, errCartesiMachine(err) - } - - // Gets the current cycle. - currentCycle, err = machine.readMachineCycle() - if err != nil { - err := fmt.Errorf("could not read the machine's cycle: %w", err) - return emulator.BreakReasonFailed, currentCycle, errCartesiMachine(err) - } - slog.Debug("run", "currentCycle", currentCycle, "leftover", maxCycle-currentCycle) - - switch breakReason { - case emulator.BreakReasonFailed: - panic(unreachable) // covered above - case emulator.BreakReasonHalted: - return emulator.BreakReasonHalted, currentCycle, ErrHalted - case emulator.BreakReasonYieldedManually, emulator.BreakReasonYieldedAutomatically: - return breakReason, currentCycle, nil // returns with the break reason - case emulator.BreakReasonYieldedSoftly: - return emulator.BreakReasonYieldedSoftly, currentCycle, ErrSoftYield - case emulator.BreakReasonReachedTargetMcycle: - continue // keeps on running - default: - panic(unreachable) - } +// step runs the machine for at most machine.inc cycles (or the amount of cycles left to reach +// limitCycle, whichever is the lowest). +// It returns the yield type and the machine cycle after the step. +// If the machine did not manually/automatically yield, the yield type will be nil (meaning step +// must be called again to complete the computation). +func (machine *RollupsMachine) step(currentCycle, limitCycle Cycle) (*yieldType, Cycle, error) { + startingCycle := currentCycle + + // Returns with an error if the next run would exceed limitCycle. + if currentCycle >= limitCycle && machine.inc != 0 { + return nil, 0, ErrCycleLimitExceeded } -} -// ------------------------------------------------------------------------------------------------ + // Calculates the increment. + increment := min(machine.inc, limitCycle-currentCycle) -// readMemory reads the machine's memory to retrieve the data from emitted outputs/reports. -func (machine *RollupsMachine) readMemory() ([]byte, error) { - tohost, err := machine.inner.ReadHtifToHostData() + // Runs the machine. + breakReason, err := machine.inner.Run(currentCycle + increment) if err != nil { - return nil, err + return nil, 0, err } - length := tohost & 0x00000000ffffffff //nolint:mnd - return machine.inner.ReadMemory(emulator.CmioTxBufferStart, length) -} - -func (machine *RollupsMachine) readYieldReason() (emulator.HtifYieldReason, error) { - value, err := machine.inner.ReadHtifToHostData() - return emulator.HtifYieldReason(value >> 32), err //nolint:mnd -} - -func (machine *RollupsMachine) readMachineCycle() (Cycle, error) { - cycle, err := machine.inner.ReadMCycle() - return Cycle(cycle), err -} -// ------------------------------------------------------------------------------------------------ + // Gets the current cycle. + currentCycle, err = machine.inner.ReadCycle() + if err != nil { + return nil, 0, err + } -func assert(condition bool, s string) { - if !condition { - panic("assertion error: " + s) + slog.Debug("step", + "startingCycle", startingCycle, + "increment", increment, + "currentCycle", currentCycle, + "leftover", limitCycle-currentCycle, + "breakReason", breakReason) + + switch breakReason { + case emulator.BreakReasonYieldedManually: + yt := manualYield + return &yt, currentCycle, nil // returns with the yield type + case emulator.BreakReasonYieldedAutomatically: + yt := automaticYield + return &yt, currentCycle, nil // returns with the yield type + case emulator.BreakReasonReachedTargetMcycle: + return nil, currentCycle, nil // returns with no yield type + case emulator.BreakReasonHalted: + return nil, 0, ErrHalted + case emulator.BreakReasonYieldedSoftly: + return nil, 0, ErrSoftYield + case emulator.BreakReasonFailed: + fallthrough // covered by inner.Run() + default: + panic(ErrUnreachable) } } diff --git a/pkg/rollupsmachine/machine_test.go b/pkg/rollupsmachine/machine_test.go index 45c5744b4..04f2c285a 100644 --- a/pkg/rollupsmachine/machine_test.go +++ b/pkg/rollupsmachine/machine_test.go @@ -4,20 +4,25 @@ package rollupsmachine import ( - "encoding/hex" - "fmt" + "errors" "log" "log/slog" "os" "testing" "github.com/cartesi/rollups-node/pkg/emulator" + "github.com/cartesi/rollups-node/pkg/rollupsmachine/cartesimachine" "github.com/cartesi/rollups-node/test/snapshot" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) +const ( + defaultInc = Cycle(10000000) + defaultMax = Cycle(1000000000) +) + func init() { log.SetFlags(log.Ltime) slog.SetLogLoggerLevel(slog.LevelDebug) @@ -25,13 +30,9 @@ func init() { const ( cycles = uint64(1_000_000_000) - serverVerbosity = ServerVerbosityInfo + serverVerbosity = cartesimachine.ServerVerbosityInfo ) -func payload(s string) string { - return fmt.Sprintf("echo '{ \"payload\": \"0x%s\" }'", hex.EncodeToString([]byte(s))) -} - // ------------------------------------------------------------------------------------------------ // TestRollupsMachine runs all the tests for the rollupsmachine package. @@ -41,30 +42,22 @@ func TestRollupsMachine(t *testing.T) { type RollupsMachineSuite struct{ suite.Suite } -func (s *RollupsMachineSuite) TestLoad() { suite.Run(s.T(), new(LoadSuite)) } +func (s *RollupsMachineSuite) TestNew() { suite.Run(s.T(), new(NewSuite)) } func (s *RollupsMachineSuite) TestFork() { suite.Run(s.T(), new(ForkSuite)) } func (s *RollupsMachineSuite) TestAdvance() { suite.Run(s.T(), new(AdvanceSuite)) } func (s *RollupsMachineSuite) TestInspect() { suite.Run(s.T(), new(InspectSuite)) } -func (s *RollupsMachineSuite) TestCycles() { suite.Run(s.T(), new(CyclesSuite)) } // ------------------------------------------------------------------------------------------------ -// Missing: -// - "could not create the remote machine manager" -// - "could not read iflagsY" -// - "could not read the yield reason" -// - machine.Close() -type LoadSuite struct { +type NewSuite struct { suite.Suite address string - acceptSnapshot *snapshot.Snapshot - rejectSnapshot *snapshot.Snapshot - exceptionSnapshot *snapshot.Snapshot - noticeSnapshot *snapshot.Snapshot + acceptSnapshot *snapshot.Snapshot + rejectSnapshot *snapshot.Snapshot } -func (s *LoadSuite) SetupSuite() { +func (s *NewSuite) SetupSuite() { var ( require = s.Require() script string @@ -80,86 +73,72 @@ func (s *LoadSuite) SetupSuite() { s.rejectSnapshot, err = snapshot.FromScript(script, cycles) require.Nil(err) require.Equal(emulator.BreakReasonYieldedManually, s.rejectSnapshot.BreakReason) - - script = payload("Paul Atreides") + " | rollup exception" - s.exceptionSnapshot, err = snapshot.FromScript(script, cycles) - require.Nil(err) - require.Equal(emulator.BreakReasonYieldedManually, s.exceptionSnapshot.BreakReason) - - script = payload("Hari Seldon") + " | rollup notice" - s.noticeSnapshot, err = snapshot.FromScript(script, cycles) - require.Nil(err) - require.Equal(emulator.BreakReasonYieldedAutomatically, s.noticeSnapshot.BreakReason) } -func (s *LoadSuite) TearDownSuite() { +func (s *NewSuite) TearDownSuite() { s.acceptSnapshot.Close() s.rejectSnapshot.Close() - s.exceptionSnapshot.Close() - s.noticeSnapshot.Close() } -func (s *LoadSuite) SetupTest() { - address, err := StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) +func (s *NewSuite) SetupTest() { + address, err := cartesimachine.StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) s.Require().Nil(err) s.address = address } -func (s *LoadSuite) TearDownTest() { - err := StopServer(s.address) +func (s *NewSuite) TearDownTest() { + err := cartesimachine.StopServer(s.address) s.Require().Nil(err) } -func (s *LoadSuite) TestOkAccept() { +func (s *NewSuite) TestOkAccept() { require := s.Require() + config := &emulator.MachineRuntimeConfig{} - machine, err := Load(s.acceptSnapshot.Path(), s.address, config) + cartesiMachine, err := cartesimachine.Load(s.acceptSnapshot.Path(), s.address, config) + require.NotNil(cartesiMachine) require.Nil(err) - require.NotNil(machine) -} -func (s *LoadSuite) TestOkReject() { - require := s.Require() - config := &emulator.MachineRuntimeConfig{} - machine, err := Load(s.rejectSnapshot.Path(), s.address, config) + rollupsMachine, err := New(cartesiMachine, defaultInc, defaultMax) + require.NotNil(rollupsMachine) require.Nil(err) - require.NotNil(machine) } -func (s *LoadSuite) TestInvalidAddress() { +func (s *NewSuite) TestOkReject() { require := s.Require() - config := &emulator.MachineRuntimeConfig{} - machine, err := Load(s.acceptSnapshot.Path(), "invalid-address", config) - require.ErrorContains(err, "could not load the machine") - require.ErrorIs(err, ErrCartesiMachine) - require.Nil(machine) -} -func (s *LoadSuite) TestInvalidPath() { - require := s.Require() config := &emulator.MachineRuntimeConfig{} - machine, err := Load("invalid-path", s.address, config) - require.ErrorContains(err, "could not load the machine") - require.ErrorIs(err, ErrCartesiMachine) - require.Nil(machine) + cartesiMachine, err := cartesimachine.Load(s.rejectSnapshot.Path(), s.address, config) + require.NotNil(cartesiMachine) + require.Nil(err) + + rollupsMachine, err := New(cartesiMachine, defaultInc, defaultMax) + require.NotNil(rollupsMachine) + require.Nil(err) } -func (s *LoadSuite) TestNotAtManualYield() { +func (s *NewSuite) TestInvalidAddress() { require := s.Require() + config := &emulator.MachineRuntimeConfig{} - machine, err := Load(s.noticeSnapshot.Path(), s.address, config) + cartesiMachine, err := cartesimachine.Load(s.acceptSnapshot.Path(), "invalid address", config) + require.Nil(cartesiMachine) require.NotNil(err) - require.ErrorIs(err, ErrNotAtManualYield) - require.Nil(machine) + + require.ErrorContains(err, "could not load the machine") + require.ErrorIs(err, cartesimachine.ErrCartesiMachine) } -func (s *LoadSuite) TestException() { +func (s *NewSuite) TestInvalidPath() { require := s.Require() + config := &emulator.MachineRuntimeConfig{} - machine, err := Load(s.exceptionSnapshot.Path(), s.address, config) + cartesiMachine, err := cartesimachine.Load("invalid path", s.address, config) + require.Nil(cartesiMachine) require.NotNil(err) - require.ErrorIs(err, ErrException) - require.Nil(machine) + + require.ErrorIs(err, cartesimachine.ErrCartesiMachine) + require.ErrorContains(err, "could not load the machine") } // ------------------------------------------------------------------------------------------------ @@ -177,20 +156,27 @@ func (s *ForkSuite) TestOk() { defer func() { require.Nil(snapshot.Close()) }() // Starts the server. - address, err := StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) + address, err := cartesimachine.StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) require.Nil(err) // Loads the machine. - machine, err := Load(snapshot.Path(), address, &emulator.MachineRuntimeConfig{}) + cartesiMachine, err := cartesimachine.Load( + snapshot.Path(), + address, + &emulator.MachineRuntimeConfig{}) + require.NotNil(cartesiMachine) require.Nil(err) + + machine, err := New(cartesiMachine, defaultInc, defaultMax) require.NotNil(machine) + require.Nil(err) defer func() { require.Nil(machine.Close()) }() // Forks the machine. - forkMachine, forkAddress, err := machine.Fork() + forkMachine, err := machine.Fork() require.Nil(err) require.NotNil(forkMachine) - require.NotEqual(address, forkAddress) + require.NotEqual(address, forkMachine.inner.Address()) require.Nil(forkMachine.Close()) } @@ -227,7 +213,7 @@ func (s *AdvanceSuite) TearDownSuite() { } func (s *AdvanceSuite) SetupTest() { - address, err := StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) + address, err := cartesimachine.StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) s.Require().Nil(err) s.address = address } @@ -236,7 +222,12 @@ func (s *AdvanceSuite) TestEchoLoop() { require := s.Require() // Loads the machine. - machine, err := Load(s.snapshotEcho.Path(), s.address, &emulator.MachineRuntimeConfig{}) + config := &emulator.MachineRuntimeConfig{} + cartesiMachine, err := cartesimachine.Load(s.snapshotEcho.Path(), s.address, config) + require.NotNil(cartesiMachine) + require.Nil(err) + + machine, err := New(cartesiMachine, defaultInc, defaultMax) require.Nil(err) require.NotNil(machine) defer func() { require.Nil(machine.Close()) }() @@ -278,7 +269,12 @@ func (s *AdvanceSuite) TestAcceptRejectException() { defer func() { require.Nil(snapshot.Close()) }() // Loads the machine. - machine, err := Load(snapshot.Path(), s.address, &emulator.MachineRuntimeConfig{}) + config := &emulator.MachineRuntimeConfig{} + cartesiMachine, err := cartesimachine.Load(snapshot.Path(), s.address, config) + require.NotNil(cartesiMachine) + require.Nil(err) + + machine, err := New(cartesiMachine, defaultInc, defaultMax) require.Nil(err) require.NotNil(machine) defer func() { require.Nil(machine.Close()) }() @@ -323,7 +319,12 @@ func (s *AdvanceSuite) TestHalted() { defer func() { require.Nil(snapshot.Close()) }() // Loads the machine. - machine, err := Load(snapshot.Path(), s.address, &emulator.MachineRuntimeConfig{}) + config := &emulator.MachineRuntimeConfig{} + cartesiMachine, err := cartesimachine.Load(snapshot.Path(), s.address, config) + require.NotNil(cartesiMachine) + require.Nil(err) + + machine, err := New(cartesiMachine, defaultInc, defaultMax) require.Nil(err) require.NotNil(machine) defer func() { require.Nil(machine.Close()) }() @@ -339,80 +340,6 @@ func (s *AdvanceSuite) TestHalted() { // ------------------------------------------------------------------------------------------------ -type CyclesSuite struct { - suite.Suite - snapshot *snapshot.Snapshot - address string - machine *RollupsMachine - input []byte -} - -func (s *CyclesSuite) SetupSuite() { - require := s.Require() - script := "ioctl-echo-loop --vouchers=1 --notices=1 --reports=1 --verbose=1" - snapshot, err := snapshot.FromScript(script, cycles) - require.Nil(err) - require.Equal(emulator.BreakReasonYieldedManually, snapshot.BreakReason) - s.snapshot = snapshot - - quote := `"I must not fear. Fear is the mind-killer." -- Dune, Frank Herbert` - input, err := Input{Data: []byte(quote)}.Encode() - require.Nil(err) - s.input = input -} - -func (s *CyclesSuite) TearDownSuite() { - s.snapshot.Close() -} - -func (s *CyclesSuite) SetupSubTest() { - require := s.Require() - var err error - - s.address, err = StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) - require.Nil(err) - - s.machine, err = Load(s.snapshot.Path(), s.address, &emulator.MachineRuntimeConfig{}) - require.Nil(err) - require.NotNil(s.machine) -} - -func (s *CyclesSuite) TearDownSubTest() { - err := s.machine.Close() - s.Require().Nil(err) -} - -// When we send a request to the machine with machine.Max set too low, -// the function call should return the ErrCycleLimitExceeded error. -func (s *CyclesSuite) TestCycleLimitExceeded() { - // Exits before calling machine.Run. - s.Run("Max=0", func() { - require := s.Require() - s.machine.Max = 0 - _, _, _, _, err := s.machine.Advance(s.input) - require.Equal(ErrCycleLimitExceeded, err) - }) - - // Runs for exactly one cycle. - s.Run("Max=1", func() { - require := s.Require() - s.machine.Max = 1 - _, _, _, _, err := s.machine.Advance(s.input) - require.Equal(ErrCycleLimitExceeded, err) - }) - - // Calls machine.Run many times. - s.Run("Max=100000", func() { - require := s.Require() - s.machine.Max = 100000 - s.machine.Inc = 10000 - _, _, _, _, err := s.machine.Advance(s.input) - require.Equal(ErrCycleLimitExceeded, err) - }) -} - -// ------------------------------------------------------------------------------------------------ - type InspectSuite struct { suite.Suite snapshotEcho *snapshot.Snapshot @@ -437,7 +364,7 @@ func (s *InspectSuite) TearDownSuite() { } func (s *InspectSuite) SetupTest() { - address, err := StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) + address, err := cartesimachine.StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) s.Require().Nil(err) s.address = address } @@ -446,7 +373,12 @@ func (s *InspectSuite) TestEchoLoop() { require := s.Require() // Loads the machine. - machine, err := Load(s.snapshotEcho.Path(), s.address, &emulator.MachineRuntimeConfig{}) + config := &emulator.MachineRuntimeConfig{} + cartesiMachine, err := cartesimachine.Load(s.snapshotEcho.Path(), s.address, config) + require.NotNil(cartesiMachine) + require.Nil(err) + + machine, err := New(cartesiMachine, defaultInc, defaultMax) require.Nil(err) require.NotNil(machine) defer func() { require.Nil(machine.Close()) }() @@ -484,3 +416,788 @@ func expectNotice(t *testing.T, output Output) *Notice { require.NotNil(t, notice) return notice } + +// ------------------------------------------------------------------------------------------------ +// Unit tests +// ------------------------------------------------------------------------------------------------ + +func TestRollupsMachineUnit(t *testing.T) { + suite.Run(t, new(UnitSuite)) +} + +type UnitSuite struct{ suite.Suite } + +func (_ *UnitSuite) newMachines() (*CartesiMachineMock, *RollupsMachine) { + mock := new(CartesiMachineMock) + machine := &RollupsMachine{inner: mock, inc: defaultInc, max: defaultMax} + return mock, machine +} + +func (s *UnitSuite) TestNew() { + newCartesiMachine := func() *CartesiMachineMock { + mock := new(CartesiMachineMock) + mock.IsAtManualYieldReturn = true + mock.IsAtManualYieldError = nil + mock.ReadYieldReasonReturn = []emulator.HtifYieldReason{emulator.ManualYieldReasonAccepted} + mock.ReadYieldReasonError = []error{nil} + return mock + } + + s.Run("Ok", func() { + s.Run("Accepted", func() { + require := s.Require() + mock := newCartesiMachine() + + machine, err := New(mock, defaultInc, defaultMax) + require.Nil(err) + require.NotNil(machine) + }) + + s.Run("Rejected", func() { + require := s.Require() + mock := newCartesiMachine() + mock.ReadYieldReasonReturn = []emulator.HtifYieldReason{ + emulator.ManualYieldReasonRejected, + } + + machine, err := New(mock, defaultInc, defaultMax) + require.Nil(err) + require.NotNil(machine) + }) + }) + + s.Run("CartesiMachineState", func() { + s.Run("NotAtManualYield", func() { + require := s.Require() + mock := newCartesiMachine() + mock.IsAtManualYieldReturn = false + + machine, err := New(mock, defaultInc, defaultMax) + require.Equal(ErrNotAtManualYield, err) + require.Nil(machine) + }) + + s.Run("Exception", func() { + require := s.Require() + mock := newCartesiMachine() + mock.ReadYieldReasonReturn = []emulator.HtifYieldReason{ + emulator.ManualYieldReasonException, + } + + machine, err := New(mock, defaultInc, defaultMax) + require.Equal(ErrException, err) + require.Nil(machine) + }) + + s.Run("Panic", func() { + require := s.Require() + require.PanicsWithValue(ErrUnreachable, func() { + mock := newCartesiMachine() + mock.ReadYieldReasonReturn = []emulator.HtifYieldReason{10} + _, _ = New(mock, defaultInc, defaultMax) + }) + }) + }) + + s.Run("CartesiMachineError", func() { + s.Run("IsAtManualYield", func() { + require := s.Require() + errIsAtManualYield := errors.New("IsAtManualYield error") + mock := newCartesiMachine() + mock.IsAtManualYieldError = errIsAtManualYield + + machine, err := New(mock, defaultInc, defaultMax) + require.Equal(errIsAtManualYield, err) + require.Nil(machine) + }) + + s.Run("ReadYieldReason", func() { + require := s.Require() + errReadYieldReason := errors.New("ReadYieldReason error") + mock := newCartesiMachine() + mock.ReadYieldReasonError = []error{errReadYieldReason} + + machine, err := New(mock, defaultInc, defaultMax) + require.Equal(errReadYieldReason, err) + require.Nil(machine) + }) + }) +} + +func (s *UnitSuite) TestFork() { + s.Run("Ok", func() { + require := s.Require() + forkedMock := new(CartesiMachineMock) + mock, machine := s.newMachines() + mock.ForkReturn = forkedMock + mock.ForkError = nil + + fork, err := machine.Fork() + require.Nil(err) + require.NotNil(fork) + require.Equal(forkedMock, fork.inner) + require.Equal(machine.inc, fork.inc) + require.Equal(machine.max, fork.max) + }) + + s.Run("CartesiMachineError", func() { + require := s.Require() + errFork := errors.New("Fork error") + mock, machine := s.newMachines() + mock.ForkReturn = new(CartesiMachineMock) + mock.ForkError = errFork + + fork, err := machine.Fork() + require.Equal(errFork, err) + require.Nil(fork) + }) +} + +func (s *UnitSuite) TestHash() { + machineHash := [32]byte{} + machineHash[0] = 1 + machineHash[31] = 1 + + s.Run("Ok", func() { + require := s.Require() + mock, machine := s.newMachines() + mock.ReadHashReturn = machineHash + mock.ReadHashError = nil + + hash, err := machine.Hash() + require.Nil(err) + require.Equal(machineHash, hash) + require.Equal(uint8(1), hash[0]) + require.Equal(uint8(1), hash[31]) + for i := 1; i < 31; i++ { + require.Equal(uint8(0), hash[i]) + } + }) + + s.Run("CartesiMachineError", func() { + require := s.Require() + errReadHash := errors.New("ReadHash error") + mock, machine := s.newMachines() + mock.ReadHashReturn = machineHash + mock.ReadHashError = errReadHash + + hash, err := machine.Hash() + require.Equal(errReadHash, err) + require.Equal(machineHash, hash) + }) +} + +func (s *UnitSuite) TestAdvance() { + s.T().Skip("TODO") +} + +func (s *UnitSuite) TestInspect() { + s.T().Skip("TODO") +} + +func (s *UnitSuite) TestClose() { + s.Run("Ok", func() { + require := s.Require() + mock, machine := s.newMachines() + mock.CloseError = nil + + err := machine.Close() + require.Nil(err) + require.Nil(machine.inner) + }) + + s.Run("Reentry", func() { + require := s.Require() + mock, machine := s.newMachines() + mock.CloseError = nil + + err := machine.Close() + require.Nil(err) + + require.NotPanics(func() { + err := machine.Close() + require.Nil(err) + }) + }) + + s.Run("CartesiMachineError", func() { + require := s.Require() + errClose := errors.New("Close error") + mock, machine := s.newMachines() + mock.CloseError = errClose + + err := machine.Close() + require.Equal(errClose, err) + }) +} + +func (s *UnitSuite) TestLastRequestWasAccepted() {} + +func (s *UnitSuite) TestProcess() {} + +func (s *UnitSuite) TestRun() { + newMachines := func() (*CartesiMachineMock, *RollupsMachine) { + mock, machine := s.newMachines() + mock.RunReturn = []emulator.BreakReason{0, emulator.BreakReasonYieldedManually} + mock.RunError = []error{nil, nil} + mock.ReadCycleError = []error{nil, nil} + return mock, machine + } + var newMachinesONRN func() (*CartesiMachineMock, *RollupsMachine) + + s.Run("Step", func() { + s.Run("Once", func() { + require := s.Require() + mock, machine := newMachines() + + mock.Cycle = 0 + machine.inc = 2 + machine.max = 10 + + outputs, reports, err := machine.run() + require.Nil(err) + require.Empty(outputs) + require.Empty(reports) + require.Equal(uint(1), mock.Steps-1) + }) + + s.Run("Multiple", func() { + require := s.Require() + mock, machine := newMachines() + mock.RunReturn = []emulator.BreakReason{0, + emulator.BreakReasonReachedTargetMcycle, + emulator.BreakReasonReachedTargetMcycle, + emulator.BreakReasonYieldedManually, + } + mock.RunError = []error{nil, nil, nil, nil} + mock.ReadCycleError = []error{nil, nil, nil, nil} + + mock.Cycle = 10 + machine.inc = 3 + machine.max = 8 + + outputs, reports, err := machine.run() + require.Nil(err) + require.Empty(outputs) + require.Empty(reports) + require.Equal(uint(3), mock.Steps-1) + }) + }) + + s.Run("Responses", func() { + s.Run("Outputs=1/Reports=0", func() { + require := s.Require() + mock, machine := newMachines() + + mock.RunReturn = []emulator.BreakReason{0, + emulator.BreakReasonYieldedAutomatically, + emulator.BreakReasonYieldedManually, + } + mock.RunError = []error{nil, nil, nil} + mock.ReadCycleError = []error{nil, nil, nil} + + mock.ReadYieldReasonReturn = []emulator.HtifYieldReason{ + emulator.AutomaticYieldReasonOutput, + } + mock.ReadYieldReasonError = []error{nil} + mock.ReadMemoryReturn = [][]byte{[]byte("an output")} + mock.ReadMemoryError = []error{nil} + + mock.Cycle = 0 + machine.inc = 2 + machine.max = 10 + + outputs, reports, err := machine.run() + require.Nil(err) + require.Len(outputs, 1) + require.Empty(reports) + + require.Equal([]byte("an output"), outputs[0]) + + require.Equal(uint(2), mock.Steps-1) + require.Equal(uint(1), mock.Responses) + }) + + s.Run("Outputs=0/Reports=1", func() { + require := s.Require() + mock, machine := newMachines() + + mock.RunReturn = []emulator.BreakReason{0, + emulator.BreakReasonYieldedAutomatically, + emulator.BreakReasonYieldedManually, + } + mock.RunError = []error{nil, nil, nil} + mock.ReadCycleError = []error{nil, nil, nil} + + mock.ReadYieldReasonReturn = []emulator.HtifYieldReason{ + emulator.AutomaticYieldReasonReport, + } + mock.ReadYieldReasonError = []error{nil} + mock.ReadMemoryReturn = [][]byte{[]byte("a report")} + mock.ReadMemoryError = []error{nil} + + mock.Cycle = 0 + machine.inc = 2 + machine.max = 10 + + outputs, reports, err := machine.run() + require.Nil(err) + require.Empty(outputs) + require.Len(reports, 1) + + require.Equal([]byte("a report"), reports[0]) + + require.Equal(uint(2), mock.Steps-1) + require.Equal(uint(1), mock.Responses) + }) + + newMachinesONRN = func() (*CartesiMachineMock, *RollupsMachine) { + mock, machine := newMachines() + + mock.RunReturn = []emulator.BreakReason{0, + emulator.BreakReasonYieldedAutomatically, + emulator.BreakReasonYieldedAutomatically, + emulator.BreakReasonYieldedAutomatically, + emulator.BreakReasonYieldedAutomatically, + emulator.BreakReasonYieldedAutomatically, + emulator.BreakReasonYieldedManually, + } + mock.RunError = []error{nil, nil, nil, nil, nil, nil, nil} + mock.ReadCycleError = []error{nil, nil, nil, nil, nil, nil, nil} + + mock.ReadYieldReasonReturn = []emulator.HtifYieldReason{ + emulator.AutomaticYieldReasonOutput, + emulator.AutomaticYieldReasonReport, + emulator.AutomaticYieldReasonOutput, + emulator.AutomaticYieldReasonReport, + emulator.AutomaticYieldReasonReport, + } + mock.ReadYieldReasonError = []error{nil, nil, nil, nil, nil} + mock.ReadMemoryReturn = [][]byte{ + []byte("output 1"), + []byte("report 1"), + []byte("output 2"), + []byte("report 2"), + []byte("report 3"), + } + mock.ReadMemoryError = []error{nil, nil, nil, nil, nil} + + mock.Cycle = 0 + machine.inc = 2 + machine.max = 20 + + return mock, machine + } + + s.Run("Outputs=N/Reports=N", func() { + require := s.Require() + mock, machine := newMachinesONRN() + + outputs, reports, err := machine.run() + require.Nil(err) + require.Len(outputs, 2) + require.Len(reports, 3) + + require.Equal([]byte("output 1"), outputs[0]) + require.Equal([]byte("output 2"), outputs[1]) + require.Equal([]byte("report 1"), reports[0]) + require.Equal([]byte("report 2"), reports[1]) + require.Equal([]byte("report 3"), reports[2]) + + require.Equal(uint(6), mock.Steps-1) + require.Equal(uint(5), mock.Responses) + }) + }) + + s.Run("CycleLimitExceeded", func() { + require := s.Require() + mock, machine := newMachinesONRN() + machine.max = 5 + + outputs, reports, err := machine.run() + require.Equal(ErrCycleLimitExceeded, err) + require.Len(outputs, 2) + require.Len(reports, 1) + + require.Equal([]byte("output 1"), outputs[0]) + require.Equal([]byte("output 2"), outputs[1]) + require.Equal([]byte("report 1"), reports[0]) + + require.Equal(uint(3), mock.Steps-1) + require.Equal(uint(3), mock.Responses) + }) +} + +func (s *UnitSuite) TestStep() { + newMachines := func() (*CartesiMachineMock, *RollupsMachine) { + mock, machine := s.newMachines() + machine.inc = 0 + machine.max = 0 + mock.RunReturn = []emulator.BreakReason{emulator.BreakReasonReachedTargetMcycle} + mock.RunError = []error{nil} + mock.ReadCycleError = []error{nil} + return mock, machine + } + + s.Run("Cycles", func() { + s.Run("Current < Limit", func() { + s.Run("Inc == 0", func() { + require := s.Require() + mock, machine := newMachines() + + currentCycle := uint64(5) + limitCycle := uint64(6) + machine.inc = 0 + mock.Cycle = currentCycle + + yt, newCurrentCycle, err := machine.step(currentCycle, limitCycle) + require.Nil(err) + require.Nil(yt) + require.Equal(currentCycle, newCurrentCycle) + }) + + s.Run("Inc < Leftover", func() { + require := s.Require() + mock, machine := newMachines() + + currentCycle := uint64(10) + limitCycle := uint64(14) + machine.inc = 2 + mock.Cycle = currentCycle + + yt, newCurrentCycle, err := machine.step(currentCycle, limitCycle) + require.Nil(err) + require.Nil(yt) + require.Equal(currentCycle+machine.inc, newCurrentCycle) + }) + + s.Run("Inc == Leftover", func() { + require := s.Require() + mock, machine := newMachines() + + currentCycle := uint64(0) + limitCycle := uint64(3) + machine.inc = 3 + mock.Cycle = currentCycle + + yt, newCurrentCycle, err := machine.step(currentCycle, limitCycle) + require.Nil(err) + require.Nil(yt) + require.Equal(limitCycle, newCurrentCycle) + require.Equal(newCurrentCycle, currentCycle+machine.inc) + }) + + s.Run("Inc > Leftover", func() { + require := s.Require() + mock, machine := newMachines() + + currentCycle := uint64(1) + limitCycle := uint64(4) + machine.inc = 5 + mock.Cycle = currentCycle + + yt, newCurrentCycle, err := machine.step(currentCycle, limitCycle) + require.Nil(err) + require.Nil(yt) + require.Equal(limitCycle, newCurrentCycle) + require.Less(newCurrentCycle, currentCycle+machine.inc) + }) + }) + + s.Run("Current == Limit", func() { + s.Run("Inc != 0", func() { + require := s.Require() + mock, machine := newMachines() + + currentCycle := uint64(6) + limitCycle := currentCycle + machine.inc = 2 + mock.Cycle = currentCycle + + yt, newCurrentCycle, err := machine.step(currentCycle, limitCycle) + require.Equal(ErrCycleLimitExceeded, err) + require.Nil(yt) + require.Zero(newCurrentCycle) + }) + + s.Run("Inc == 0", func() { + require := s.Require() + mock, machine := newMachines() + + currentCycle := uint64(6) + limitCycle := currentCycle + machine.inc = 0 + mock.Cycle = currentCycle + + yt, newCurrentCycle, err := machine.step(currentCycle, limitCycle) + require.Nil(err) + require.Nil(yt) + require.Equal(currentCycle, newCurrentCycle) + }) + }) + + s.Run("Current > Limit", func() { + s.Run("Inc != 0", func() { + require := s.Require() + mock, machine := newMachines() + + currentCycle := uint64(9) + limitCycle := uint64(4) + machine.inc = 1 + mock.Cycle = currentCycle + + yt, newCurrentCycle, err := machine.step(currentCycle, limitCycle) + require.Equal(ErrCycleLimitExceeded, err) + require.Nil(yt) + require.Zero(newCurrentCycle) + }) + + s.Run("Inc == 0", func() { + require := s.Require() + mock, machine := newMachines() + + currentCycle := uint64(9) + limitCycle := uint64(4) + machine.inc = 0 + mock.Cycle = currentCycle + + yt, newCurrentCycle, err := machine.step(currentCycle, limitCycle) + require.Nil(err) + require.Nil(yt) + require.Equal(currentCycle, newCurrentCycle) + }) + }) + }) + + s.Run("CartesiMachineError", func() { + s.Run("Run", func() { + require := s.Require() + errRun := errors.New("Run error") + mock, machine := newMachines() + mock.RunError[0] = errRun + + currentCycle := uint64(5) + limitCycle := uint64(12) + machine.inc = 5 + mock.Cycle = currentCycle + + yt, newCurrentCycle, err := machine.step(currentCycle, limitCycle) + require.Equal(errRun, err) + require.Nil(yt) + require.Zero(newCurrentCycle) + }) + + s.Run("ReadCycle", func() { + require := s.Require() + errReadCycle := errors.New("ReadCycle error") + mock, machine := newMachines() + mock.ReadCycleError[0] = errReadCycle + + currentCycle := uint64(100) + limitCycle := uint64(1000) + machine.inc = 100 + mock.Cycle = currentCycle + + yt, newCurrentCycle, err := machine.step(currentCycle, limitCycle) + require.Equal(errReadCycle, err) + require.Nil(yt) + require.Zero(newCurrentCycle) + }) + }) + + s.Run("Panic", func() { + s.Run("BreakReasonFailed", func() { + require := s.Require() + mock, machine := newMachines() + mock.RunReturn[0] = emulator.BreakReasonFailed + + currentCycle := uint64(10) + limitCycle := uint64(100) + machine.inc = 10 + mock.Cycle = currentCycle + + require.PanicsWithValue(ErrUnreachable, func() { + _, _, _ = machine.step(currentCycle, limitCycle) + }) + }) + + s.Run("BreakReasonInvalid", func() { + require := s.Require() + mock, machine := newMachines() + mock.RunReturn[0] = 10 // invalid break reason + + currentCycle := uint64(5) + limitCycle := uint64(50) + machine.inc = 6 + mock.Cycle = currentCycle + + require.PanicsWithValue(ErrUnreachable, func() { + _, _, _ = machine.step(currentCycle, limitCycle) + }) + }) + }) + + s.Run("ManualYield", func() { + require := s.Require() + mock, machine := newMachines() + mock.RunReturn[0] = emulator.BreakReasonYieldedManually + + currentCycle := uint64(3) + limitCycle := uint64(17) + machine.inc = 9 + mock.Cycle = currentCycle + + yt, newCurrentCycle, err := machine.step(currentCycle, limitCycle) + require.Nil(err) + require.NotNil(yt) + require.Equal(manualYield, *yt) + require.Equal(currentCycle+machine.inc, newCurrentCycle) + }) + + s.Run("AutomaticYield", func() { + require := s.Require() + mock, machine := newMachines() + mock.RunReturn[0] = emulator.BreakReasonYieldedAutomatically + + currentCycle := uint64(8) + limitCycle := uint64(17) + machine.inc = 9 + mock.Cycle = currentCycle + + yt, newCurrentCycle, err := machine.step(currentCycle, limitCycle) + require.Nil(err) + require.NotNil(yt) + require.Equal(automaticYield, *yt) + require.Equal(limitCycle, newCurrentCycle) + }) + + s.Run("Halted", func() { + require := s.Require() + mock, machine := newMachines() + mock.RunReturn[0] = emulator.BreakReasonHalted + + currentCycle := uint64(4) + limitCycle := uint64(6) + machine.inc = 1 + mock.Cycle = currentCycle + + yt, newCurrentCycle, err := machine.step(currentCycle, limitCycle) + require.Equal(ErrHalted, err) + require.Nil(yt) + require.Zero(newCurrentCycle) + }) + + s.Run("SoftYield", func() { + require := s.Require() + mock, machine := newMachines() + mock.RunReturn[0] = emulator.BreakReasonYieldedSoftly + + currentCycle := uint64(3) + limitCycle := uint64(8) + machine.inc = 4 + mock.Cycle = currentCycle + + yt, newCurrentCycle, err := machine.step(currentCycle, limitCycle) + require.Equal(ErrSoftYield, err) + require.Nil(yt) + require.Zero(newCurrentCycle) + }) +} + +// ------------------------------------------------------------------------------------------------ +// Mock +// ------------------------------------------------------------------------------------------------ + +type CartesiMachineMock struct { + ForkReturn cartesimachine.CartesiMachine + ForkError error + + ContinueError error + + CloseError error + + IsAtManualYieldReturn bool + IsAtManualYieldError error + + ReadHashReturn [32]byte + ReadHashError error + + WriteRequestError error + + AddressReturn string + + Responses uint + ReadYieldReasonReturn []emulator.HtifYieldReason + ReadYieldReasonError []error + ReadMemoryReturn [][]byte + ReadMemoryError []error + + Steps uint + Cycle uint64 + RunReturn []emulator.BreakReason + RunError []error + ReadCycleError []error +} + +func (machine *CartesiMachineMock) Fork() (cartesimachine.CartesiMachine, error) { + return machine.ForkReturn, machine.ForkError +} + +func (machine *CartesiMachineMock) Continue() error { + return machine.ContinueError +} + +func (machine *CartesiMachineMock) Close() error { + return machine.CloseError +} + +func (machine *CartesiMachineMock) IsAtManualYield() (bool, error) { + return machine.IsAtManualYieldReturn, machine.IsAtManualYieldError +} + +func (machine *CartesiMachineMock) ReadHash() ([32]byte, error) { + return machine.ReadHashReturn, machine.ReadHashError +} + +func (machine *CartesiMachineMock) WriteRequest(data []byte, _ cartesimachine.RequestType) error { + return machine.WriteRequestError +} + +func (machine *CartesiMachineMock) PayloadLengthLimit() uint { + return 100000 +} + +func (machine *CartesiMachineMock) Address() string { + return machine.AddressReturn +} + +// ------------------------------------------------------------------------------------------------ + +func (machine *CartesiMachineMock) ReadYieldReason() (emulator.HtifYieldReason, error) { + yieldReason := machine.ReadYieldReasonReturn[machine.Responses] + err := machine.ReadYieldReasonError[machine.Responses] + return yieldReason, err +} + +func (machine *CartesiMachineMock) ReadMemory() ([]byte, error) { + bytes := machine.ReadMemoryReturn[machine.Responses] + err := machine.ReadMemoryError[machine.Responses] + machine.Responses++ + return bytes, err +} + +// ------------------------------------------------------------------------------------------------ + +func (machine *CartesiMachineMock) Run(cycle uint64) (emulator.BreakReason, error) { + machine.Cycle += cycle - machine.Cycle + return machine.RunReturn[machine.Steps], machine.RunError[machine.Steps] +} + +func (machine *CartesiMachineMock) ReadCycle() (uint64, error) { + if err := machine.ReadCycleError[machine.Steps]; err != nil { + return machine.Cycle, err + } + cycle, err := machine.Cycle, machine.ReadCycleError[machine.Steps] + machine.Steps++ + return cycle, err +} diff --git a/pkg/rollupsmachine/server.go b/pkg/rollupsmachine/server.go index d4d29ac56..ddf499ab9 100644 --- a/pkg/rollupsmachine/server.go +++ b/pkg/rollupsmachine/server.go @@ -49,13 +49,13 @@ func StartServer(verbosity ServerVerbosity, port uint32, stdout, stderr io.Write cmd := exec.Command("jsonrpc-remote-cartesi-machine", args...) // Redirects stdout and stderr. - intercepter := portIntercepter{ + interceptor := portInterceptor{ inner: stderr, port: make(chan uint32), found: new(bool), } cmd.Stdout = stdout - cmd.Stderr = linewriter.New(intercepter) + cmd.Stderr = linewriter.New(interceptor) // Starts the server. slog.Info("running", "command", cmd.String()) @@ -63,8 +63,8 @@ func StartServer(verbosity ServerVerbosity, port uint32, stdout, stderr io.Write return "", err } - // Waits for the intercepter to write the port to the channel. - if actualPort := <-intercepter.port; port == 0 { + // Waits for the interceptor to write the port to the channel. + if actualPort := <-interceptor.port; port == 0 { port = actualPort } else if port != actualPort { panic(fmt.Sprintf("mismatching ports (%d != %d)", port, actualPort)) @@ -95,23 +95,23 @@ func (verbosity ServerVerbosity) valid() bool { verbosity == ServerVerbosityFatal } -// portIntercepter sends the server's port through the port channel as soon as it reads it. +// portInterceptor sends the server's port through the port channel as soon as it reads it. // It then closes the channel and keeps on writing to the inner writer. // // It expects to be wrapped by a linewriter.LineWriter. -type portIntercepter struct { +type portInterceptor struct { inner io.Writer port chan uint32 found *bool } -var regex = regexp.MustCompile("initial server bound to port ([0-9]+)") +var portRegex = regexp.MustCompile("initial server bound to port ([0-9]+)") -func (writer portIntercepter) Write(p []byte) (n int, err error) { +func (writer portInterceptor) Write(p []byte) (n int, err error) { if *writer.found { return writer.inner.Write(p) } else { - matches := regex.FindStringSubmatch(string(p)) + matches := portRegex.FindStringSubmatch(string(p)) if matches != nil { port, err := strconv.ParseUint(matches[1], 10, 32) if err != nil { From 87a24ee7fa79e25572a07924cc3724cc6fed3eb4 Mon Sep 17 00:00:00 2001 From: Renan Santos Date: Tue, 13 Aug 2024 15:23:39 -0300 Subject: [PATCH 3/3] feat: change emulator version --- build/compose-devnet.yaml | 2 +- build/docker-bake.hcl | 4 +- pkg/addresses/addresses.go | 2 +- pkg/emulator/emulator_test.go | 2 +- pkg/emulator/remote.go | 6 +- pkg/rollupsmachine/abi.json | 1 + pkg/rollupsmachine/io.go | 10 +-- pkg/rollupsmachine/machine_test.go | 15 ++-- pkg/rollupsmachine/server.go | 126 ----------------------------- setup_env.sh | 2 +- 10 files changed, 23 insertions(+), 147 deletions(-) delete mode 100644 pkg/rollupsmachine/server.go diff --git a/build/compose-devnet.yaml b/build/compose-devnet.yaml index 07d0d7c2b..0086feb2d 100644 --- a/build/compose-devnet.yaml +++ b/build/compose-devnet.yaml @@ -24,7 +24,7 @@ services: CARTESI_BLOCKCHAIN_WS_ENDPOINT: "ws://devnet:8545" CARTESI_LEGACY_BLOCKCHAIN_ENABLED: "false" CARTESI_BLOCKCHAIN_FINALITY_OFFSET: "1" - CARTESI_CONTRACTS_APPLICATION_ADDRESS: "0x00D13Ee2EB6D14eD8A2CA9DAD09D3345F95bE731" + CARTESI_CONTRACTS_APPLICATION_ADDRESS: "0x1b0FAD42f016a9EBa358c7491A67fa1fAE82912A" CARTESI_CONTRACTS_ICONSENSUS_ADDRESS: "0x3fd5dc9dCf5Df3c7002C0628Eb9AD3bb5e2ce257" CARTESI_CONTRACTS_INPUT_BOX_ADDRESS: "0x593E5BCf894D6829Dd26D0810DA7F064406aebB6" CARTESI_CONTRACTS_INPUT_BOX_DEPLOYMENT_BLOCK_NUMBER: "10" diff --git a/build/docker-bake.hcl b/build/docker-bake.hcl index 6d9a0b1da..629d3a08b 100644 --- a/build/docker-bake.hcl +++ b/build/docker-bake.hcl @@ -22,8 +22,8 @@ target "common" { RUST_VERSION = "1.78.0" GO_VERSION = "1.22.1" FOUNDRY_NIGHTLY_VERSION = "293fad73670b7b59ca901c7f2105bf7a29165a90" - MACHINE_EMULATOR_VERSION = "0.17.0" - MACHINE_TOOLS_VERSION = "0.15.0" + MACHINE_EMULATOR_VERSION = "0.18.1" + MACHINE_TOOLS_VERSION = "0.16.1" MACHINE_IMAGE_KERNEL_VERSION = "0.20.0" MACHINE_KERNEL_VERSION = "6.5.13" MACHINE_XGENEXT2FS_VERSION = "1.5.6" diff --git a/pkg/addresses/addresses.go b/pkg/addresses/addresses.go index 0b80318bc..f13c36976 100644 --- a/pkg/addresses/addresses.go +++ b/pkg/addresses/addresses.go @@ -39,7 +39,7 @@ type Book struct { func GetTestBook() *Book { return &Book{ Application: common.HexToAddress( - "0x00D13Ee2EB6D14eD8A2CA9DAD09D3345F95bE731"), + "0x1b0FAD42f016a9EBa358c7491A67fa1fAE82912A"), ApplicationFactory: common.HexToAddress( "0xA1DA32BF664109D62208a1cb0d69aACc6a484873"), Authority: common.HexToAddress( diff --git a/pkg/emulator/emulator_test.go b/pkg/emulator/emulator_test.go index c92b59a09..ce09a58b3 100644 --- a/pkg/emulator/emulator_test.go +++ b/pkg/emulator/emulator_test.go @@ -16,7 +16,7 @@ import ( var ( imagesPath = "/usr/share/cartesi-machine/images/" - address = "localhost:8081" + address = "127.0.0.1:8081" ) func init() { diff --git a/pkg/emulator/remote.go b/pkg/emulator/remote.go index 024e180a8..cad2a1562 100644 --- a/pkg/emulator/remote.go +++ b/pkg/emulator/remote.go @@ -12,7 +12,7 @@ import ( // A connection to the remote jsonrpc machine manager. type RemoteMachineManager struct { - c *C.cm_jsonrpc_mg_mgr + c *C.cm_jsonrpc_mgr Address string } @@ -22,13 +22,13 @@ func NewRemoteMachineManager(address string) (*RemoteMachineManager, error) { cRemoteAddress := C.CString(address) defer C.free(unsafe.Pointer(cRemoteAddress)) var msg *C.char - code := C.cm_create_jsonrpc_mg_mgr(cRemoteAddress, &manager.c, &msg) + code := C.cm_create_jsonrpc_mgr(cRemoteAddress, &manager.c, &msg) return manager, newError(code, msg) } func (remote *RemoteMachineManager) Delete() { if remote.c != nil { - C.cm_delete_jsonrpc_mg_mgr(remote.c) + C.cm_delete_jsonrpc_mgr(remote.c) remote.c = nil } } diff --git a/pkg/rollupsmachine/abi.json b/pkg/rollupsmachine/abi.json index 8e84bb97a..41ff52ec1 100644 --- a/pkg/rollupsmachine/abi.json +++ b/pkg/rollupsmachine/abi.json @@ -8,6 +8,7 @@ { "type" : "uint256" }, { "type" : "uint256" }, { "type" : "uint256" }, + { "type" : "uint256" }, { "type" : "bytes" } ] }, { diff --git a/pkg/rollupsmachine/io.go b/pkg/rollupsmachine/io.go index f25f6b0f8..84c4a2f21 100644 --- a/pkg/rollupsmachine/io.go +++ b/pkg/rollupsmachine/io.go @@ -35,9 +35,9 @@ type Input struct { Sender Address BlockNumber uint64 BlockTimestamp uint64 - // PrevRandao uint64 - Index uint64 - Data []byte + PrevRandao uint64 + Index uint64 + Data []byte } // A Query is sent by a inspect-state request. @@ -64,10 +64,10 @@ func (input Input) Encode() ([]byte, error) { sender := common.BytesToAddress(input.Sender[:]) blockNumber := new(big.Int).SetUint64(input.BlockNumber) blockTimestamp := new(big.Int).SetUint64(input.BlockTimestamp) - // prevRandao := new(big.Int).SetUint64(input.PrevRandao) + prevRandao := new(big.Int).SetUint64(input.PrevRandao) index := new(big.Int).SetUint64(input.Index) return ioABI.Pack("EvmAdvance", chainId, appContract, sender, blockNumber, blockTimestamp, - index, input.Data) + prevRandao, index, input.Data) } // DecodeOutput decodes an output into either a voucher or a notice. diff --git a/pkg/rollupsmachine/machine_test.go b/pkg/rollupsmachine/machine_test.go index 04f2c285a..611756051 100644 --- a/pkg/rollupsmachine/machine_test.go +++ b/pkg/rollupsmachine/machine_test.go @@ -46,6 +46,7 @@ func (s *RollupsMachineSuite) TestNew() { suite.Run(s.T(), new(NewSuite)) } func (s *RollupsMachineSuite) TestFork() { suite.Run(s.T(), new(ForkSuite)) } func (s *RollupsMachineSuite) TestAdvance() { suite.Run(s.T(), new(AdvanceSuite)) } func (s *RollupsMachineSuite) TestInspect() { suite.Run(s.T(), new(InspectSuite)) } +func (s *RollupsMachineSuite) TestUnit() { suite.Run(s.T(), new(UnitSuite)) } // ------------------------------------------------------------------------------------------------ @@ -97,7 +98,7 @@ func (s *NewSuite) TestOkAccept() { config := &emulator.MachineRuntimeConfig{} cartesiMachine, err := cartesimachine.Load(s.acceptSnapshot.Path(), s.address, config) require.NotNil(cartesiMachine) - require.Nil(err) + require.Nil(err, "%v", err) rollupsMachine, err := New(cartesiMachine, defaultInc, defaultMax) require.NotNil(rollupsMachine) @@ -421,10 +422,6 @@ func expectNotice(t *testing.T, output Output) *Notice { // Unit tests // ------------------------------------------------------------------------------------------------ -func TestRollupsMachineUnit(t *testing.T) { - suite.Run(t, new(UnitSuite)) -} - type UnitSuite struct{ suite.Suite } func (_ *UnitSuite) newMachines() (*CartesiMachineMock, *RollupsMachine) { @@ -631,9 +628,13 @@ func (s *UnitSuite) TestClose() { }) } -func (s *UnitSuite) TestLastRequestWasAccepted() {} +func (s *UnitSuite) TestLastRequestWasAccepted() { + s.T().Skip("TODO") +} -func (s *UnitSuite) TestProcess() {} +func (s *UnitSuite) TestProcess() { + s.T().Skip("TODO") +} func (s *UnitSuite) TestRun() { newMachines := func() (*CartesiMachineMock, *RollupsMachine) { diff --git a/pkg/rollupsmachine/server.go b/pkg/rollupsmachine/server.go deleted file mode 100644 index ddf499ab9..000000000 --- a/pkg/rollupsmachine/server.go +++ /dev/null @@ -1,126 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -package rollupsmachine - -import ( - "fmt" - "io" - "log/slog" - "os/exec" - "regexp" - "strconv" - - "github.com/cartesi/rollups-node/internal/linewriter" - "github.com/cartesi/rollups-node/pkg/emulator" -) - -type ServerVerbosity string - -const ( - ServerVerbosityTrace ServerVerbosity = "trace" - ServerVerbosityDebug ServerVerbosity = "debug" - ServerVerbosityInfo ServerVerbosity = "info" - ServerVerbosityWarn ServerVerbosity = "warn" - ServerVerbosityError ServerVerbosity = "error" - ServerVerbosityFatal ServerVerbosity = "fatal" -) - -// StartServer starts a JSON RPC remote cartesi machine server. -// -// It configures the server's logging verbosity and initializes its address to localhost:port. -// If verbosity is an invalid LogLevel, a default value will be used instead. -// If port is 0, a random valid port will be used instead. -// -// StartServer also redirects the server's stdout and stderr to the provided io.Writers. -// -// It returns the server's address. -func StartServer(verbosity ServerVerbosity, port uint32, stdout, stderr io.Writer) (string, error) { - // Configures the command's arguments. - args := []string{} - if verbosity.valid() { - args = append(args, "--log-level="+string(verbosity)) - } - if port != 0 { - args = append(args, fmt.Sprintf("--server-address=localhost:%d", port)) - } - - // Creates the command. - cmd := exec.Command("jsonrpc-remote-cartesi-machine", args...) - - // Redirects stdout and stderr. - interceptor := portInterceptor{ - inner: stderr, - port: make(chan uint32), - found: new(bool), - } - cmd.Stdout = stdout - cmd.Stderr = linewriter.New(interceptor) - - // Starts the server. - slog.Info("running", "command", cmd.String()) - if err := cmd.Start(); err != nil { - return "", err - } - - // Waits for the interceptor to write the port to the channel. - if actualPort := <-interceptor.port; port == 0 { - port = actualPort - } else if port != actualPort { - panic(fmt.Sprintf("mismatching ports (%d != %d)", port, actualPort)) - } - - return fmt.Sprintf("localhost:%d", port), nil -} - -// StopServer shuts down the JSON RPC remote cartesi machine server hosted at address. -func StopServer(address string) error { - slog.Info("Stopping server at", "address", address) - remote, err := emulator.NewRemoteMachineManager(address) - if err != nil { - return err - } - defer remote.Delete() - return remote.Shutdown() -} - -// ------------------------------------------------------------------------------------------------ - -func (verbosity ServerVerbosity) valid() bool { - return verbosity == ServerVerbosityTrace || - verbosity == ServerVerbosityDebug || - verbosity == ServerVerbosityInfo || - verbosity == ServerVerbosityWarn || - verbosity == ServerVerbosityError || - verbosity == ServerVerbosityFatal -} - -// portInterceptor sends the server's port through the port channel as soon as it reads it. -// It then closes the channel and keeps on writing to the inner writer. -// -// It expects to be wrapped by a linewriter.LineWriter. -type portInterceptor struct { - inner io.Writer - port chan uint32 - found *bool -} - -var portRegex = regexp.MustCompile("initial server bound to port ([0-9]+)") - -func (writer portInterceptor) Write(p []byte) (n int, err error) { - if *writer.found { - return writer.inner.Write(p) - } else { - matches := portRegex.FindStringSubmatch(string(p)) - if matches != nil { - port, err := strconv.ParseUint(matches[1], 10, 32) - if err != nil { - return 0, err - } - *writer.found = true - writer.port <- uint32(port) - close(writer.port) - } - return writer.inner.Write(p) - } -} diff --git a/setup_env.sh b/setup_env.sh index c0de7a6c5..4480f9d8b 100644 --- a/setup_env.sh +++ b/setup_env.sh @@ -9,7 +9,7 @@ export CARTESI_BLOCKCHAIN_HTTP_ENDPOINT="http://localhost:8545" export CARTESI_BLOCKCHAIN_WS_ENDPOINT="ws://localhost:8545" export CARTESI_BLOCKCHAIN_FINALITY_OFFSET="1" export CARTESI_BLOCKCHAIN_BLOCK_TIMEOUT="60" -export CARTESI_CONTRACTS_APPLICATION_ADDRESS="0x00D13Ee2EB6D14eD8A2CA9DAD09D3345F95bE731" +export CARTESI_CONTRACTS_APPLICATION_ADDRESS="0x1b0FAD42f016a9EBa358c7491A67fa1fAE82912A" export CARTESI_CONTRACTS_ICONSENSUS_ADDRESS="0x3fd5dc9dCf5Df3c7002C0628Eb9AD3bb5e2ce257" export CARTESI_CONTRACTS_INPUT_BOX_ADDRESS="0x593E5BCf894D6829Dd26D0810DA7F064406aebB6" export CARTESI_CONTRACTS_INPUT_BOX_DEPLOYMENT_BLOCK_NUMBER="10"