diff --git a/pkg/model/model.go b/pkg/model/model.go new file mode 100644 index 000000000..daa936ca7 --- /dev/null +++ b/pkg/model/model.go @@ -0,0 +1,8 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package model + +const HashSize = 32 + +type Hash = [HashSize]byte diff --git a/pkg/rollupsmachine/error.go b/pkg/rollupsmachine/error.go new file mode 100644 index 000000000..652c0fce2 --- /dev/null +++ b/pkg/rollupsmachine/error.go @@ -0,0 +1,40 @@ +// (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/pkg/model" +) + +const unreachable = "internal error: entered unreacheable code" + +var ( + ErrMaxCycles = errors.New("reached limit cycles") + ErrMaxOutputs = fmt.Errorf("reached maximum number of emitted outputs (%d)", maxOutputs) + ErrHashSize = fmt.Errorf("hash does not have exactly %d bytes", HashSize) + + ErrFailed = errors.New("machine failed") + ErrHalted = errors.New("machine halted") + ErrYieldedWithProgress = errors.New("machine yielded with progress") + ErrYieldedSoftly = errors.New("machine yielded softly") + + // Load (and isReadyForRequests) errors + ErrNewRemoteMachineManager = errors.New("could not create the remote machine manager") + ErrRemoteLoadMachine = errors.New("remote server was not able to load the machine") + ErrNotReadyForRequests = errors.New("machine is not ready to receive requests") + ErrNotAtManualYield = errors.New("not at manual yield") + ErrLastInputWasRejected = errors.New("last input was rejected") + ErrLastInputYieldedAnException = errors.New("last input yielded an exception") + + // Fork errors + ErrFork = errors.New("could not fork the machine") + ErrOrphanFork = errors.New("forked cartesi machine was left orphan") + + // Destroy errors + ErrRemoteShutdown = errors.New("could not shut down the remote machine") + ErrMachineDestroy = errors.New("could not destroy the inner machine") +) diff --git a/pkg/rollupsmachine/io.go b/pkg/rollupsmachine/io.go new file mode 100644 index 000000000..736786a9d --- /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: + 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..62dbe609c --- /dev/null +++ b/pkg/rollupsmachine/machine.go @@ -0,0 +1,363 @@ +// (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/pkg/emulator" + "github.com/cartesi/rollups-node/pkg/model" +) + +type ( + Cycle = uint64 + Output = []byte + Report = []byte + + 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 + + inner *emulator.Machine + remote *emulator.RemoteMachineManager +} + +// Load loads the machine stored at path +// by connecting to the JSON RPC remote cartesi machine at 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} + + // Creates the remote machine manager. + remote, err := emulator.NewRemoteMachineManager(address) + if err != nil { + return nil, errors.Join(ErrNewRemoteMachineManager, 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 { + err = errors.Join(err, machine.remote.Shutdown()) + machine.remote.Delete() + return nil, errors.Join(ErrRemoteLoadMachine, err) + } + machine.inner = inner + + // Checks if the machine is ready to receive requests. + err = machine.isReadyForRequests() + if err != nil { + return nil, errors.Join(ErrNotReadyForRequests, err, machine.Destroy()) + } + + return machine, nil +} + +// Fork forks an existing cartesi machine. +func (machine RollupsMachine) Fork() (*RollupsMachine, error) { + // Creates the new machine based on the old machine. + newMachine := &RollupsMachine{Inc: machine.Inc, Max: machine.Max} + + // TODO : ask canal da machine + // Forks the remote server's process. + address, err := machine.remote.Fork() + if err != nil { + return nil, fmt.Errorf("%w: %w", ErrFork, err) + } + + // Instantiates the new remote machine manager. + newMachine.remote, err = emulator.NewRemoteMachineManager(address) + if err != nil { + format := "%w: %w at address %s " + format += "(could not create the new remote machine manager)" + return nil, fmt.Errorf(format, err, ErrOrphanFork, address) + } + + // Gets the inner machine reference from the remote server. + newMachine.inner, err = newMachine.remote.GetMachine() + if err != nil { + defer machine.remote.Delete() + if shutdownErr := newMachine.remote.Shutdown(); shutdownErr != nil { + format := "%w: %w at address %s " + format += "(could not shut down the remote server)" + return newMachine, fmt.Errorf(format, err, ErrOrphanFork, address) + } else { + return nil, err + } + } + + return newMachine, nil +} + +// Destroy destroys the inner machine reference, +// shuts down the remote cartesi machine server, +// and deletes the remote machine manager. +func (machine *RollupsMachine) Destroy() error { + errMachineDestroy := machine.inner.Destroy() + if errMachineDestroy != nil { + errMachineDestroy = fmt.Errorf("%w: %w", ErrMachineDestroy, errMachineDestroy) + } + + errRemoteShutdown := machine.remote.Shutdown() + if errRemoteShutdown != nil { + errRemoteShutdown = fmt.Errorf("%w: %w", ErrRemoteShutdown, errRemoteShutdown) + } + + machine.remote.Delete() + + if errMachineDestroy != nil || errRemoteShutdown != nil { + return errors.Join(errMachineDestroy, errRemoteShutdown) + } + + return nil +} + +// Hash returns the machine's merkle tree root hash. +func (machine RollupsMachine) Hash() (model.Hash, error) { + hash, err := machine.inner.GetRootHash() + return model.Hash(hash), err +} + +// Advance sends an input to the cartesi machine +// and returns the corresponding outputs (with their hash) and reports. +func (machine *RollupsMachine) Advance(input []byte) ([]Output, []Report, model.Hash, error) { + var hash model.Hash + + outputs, reports, err := machine.process(input, advanceStateRequest) + if err != nil { + return outputs, reports, hash, err + } + + hashBytes, err := machine.readMemory() + if err != nil { + return outputs, reports, hash, err + } + if size := len(hashBytes); size != model.HashSize { + err = fmt.Errorf("%w (it has %d bytes)", ErrHashSize, size) + return outputs, reports, hash, err + } + copy(hash[:], hashBytes) + + return outputs, reports, hash, nil +} + +// Inspect sends a query to the cartesi machine and returns the corresponding reports. +func (machine *RollupsMachine) Inspect(query []byte) ([]Report, error) { + _, reports, err := machine.process(query, inspectStateRequest) + return reports, err +} + +// ------------------------------------------------------------------------------------------------ +// Auxiliary +// ------------------------------------------------------------------------------------------------ + +// isReadyForRequests returns nil if the machine is ready to receive a request, +// otherwise, it returns an error that indicates why that is not the case. +// +// A machine is ready to receive requests if +// (1) it is at a manual yield and +// (2) the last input it received was accepted. +func (machine RollupsMachine) isReadyForRequests() error { + yieldedManually, err := machine.inner.ReadIFlagsY() + if err != nil { + return err + } + if !yieldedManually { + return ErrNotAtManualYield + } + return machine.lastInputWasAccepted() +} + +// lastInputWasAccepted returns nil if the last input sent to the machine was accepted. +// Otherwise, it returns an error that indicates why the input was not accepted. +// +// The machine must be at a manual yield when calling this function. +func (machine RollupsMachine) lastInputWasAccepted() error { + reason, err := machine.readYieldReason() + if err != nil { + return err + } + switch reason { + case emulator.ManualYieldReasonAccepted: + return nil + case emulator.ManualYieldReasonRejected: + return ErrLastInputWasRejected + case emulator.ManualYieldReasonException: + return ErrLastInputYieldedAnException + default: + panic(unreachable) + } +} + +// process processes a request, +// be it an avance-state or an inspect-state request, +// and returns any collected responses. +// +// It expects the machine to be primed before execution. +// It also leaves the machine in a primed state after an execution with no errors. +func (machine *RollupsMachine) process(request []byte, t requestType) ([]Output, []Report, error) { + // Writes the request's data. + err := machine.inner.WriteMemory(emulator.CmioRxBufferStart, request) + if err != nil { + return nil, nil, err + } + + // Writes the request's type and length. + fromhost := ((uint64(t) << 32) | (uint64(len(request)) & 0xffffffff)) + err = machine.inner.WriteHtifFromHostData(fromhost) + if err != nil { + return nil, nil, err + } + + // Green-lights the machine to keep running. + err = machine.inner.ResetIFlagsY() + if err != nil { + return nil, nil, err + } + + outputs, reports, err := machine.runAndCollect() + if err != nil { + return outputs, reports, err + } + + return outputs, reports, machine.lastInputWasAccepted() +} + +// runAndCollect runs the machine until it yields manually. +// It returns any collected responses. +// (The slices with the responses will never be nil, even in case of errors.) +func (machine *RollupsMachine) runAndCollect() ([]Output, []Report, error) { + outputs := []Output{} + reports := []Report{} + + startingCycle, err := machine.readMachineCycle() + if err != nil { + return outputs, reports, err + } + + for { + switch reason, err := machine.runUntilYield(startingCycle); { + case err != nil: + return outputs, reports, err // returns with an error + case reason == emulator.BreakReasonYieldedManually: + return outputs, reports, nil // returns with the responses + case reason == emulator.BreakReasonYieldedAutomatically: + break // breaks from the switch to read the output/report + default: + panic(unreachable) + } + + switch reason, err := machine.readYieldReason(); { + case err != nil: + return outputs, reports, err + case reason == emulator.AutomaticYieldReasonProgress: + return outputs, reports, ErrYieldedWithProgress + case reason == emulator.AutomaticYieldReasonOutput: + output, err := machine.readMemory() + if err != nil { + return outputs, reports, err + } else { + outputs = append(outputs, output) + if len(outputs) > maxOutputs { + return outputs, reports, ErrMaxOutputs + } + } + case reason == emulator.AutomaticYieldReasonReport: + report, err := machine.readMemory() + if err != nil { + return outputs, reports, err + } else { + reports = append(reports, report) + } + default: + panic(unreachable) + } + } +} + +// runUntilYield runs the machine until it yields. +// It returns the yield type or an error if the machine reaches the internal cycle limit. +func (machine *RollupsMachine) runUntilYield(startingCycle Cycle) (emulator.BreakReason, error) { + currentCycle, err := machine.readMachineCycle() + if err != nil { + return emulator.BreakReasonFailed, err + } + + for currentCycle-startingCycle < machine.Max { + reason, err := machine.inner.Run(uint64(currentCycle + machine.Inc)) + if err != nil { + return emulator.BreakReasonFailed, err + } + currentCycle, err = machine.readMachineCycle() + if err != nil { + return emulator.BreakReasonFailed, err + } + + // TODO : diego server manager parametros de configuraĆ§Ć£o + switch reason { + case emulator.BreakReasonReachedTargetMcycle: + continue // continues to run unless the limit cycle has been reached + case emulator.BreakReasonYieldedManually, emulator.BreakReasonYieldedAutomatically: + return reason, nil // returns with the yield reason + case emulator.BreakReasonYieldedSoftly: + return reason, ErrYieldedSoftly + case emulator.BreakReasonFailed: + return reason, ErrFailed + case emulator.BreakReasonHalted: + return reason, ErrHalted + default: + panic(unreachable) + } + } + + return emulator.BreakReasonFailed, ErrMaxCycles +} + +// readMemory reads the machine's memory to retrieve the data from emmited outputs/reports. +func (machine RollupsMachine) readMemory() ([]byte, error) { + tohost, err := machine.inner.ReadHtifToHostData() + if err != nil { + return nil, err + } + length := tohost & 0x00000000ffffffff + return machine.inner.ReadMemory(emulator.CmioTxBufferStart, length) +} + +// writeRequestTypeAndLength writes to the HTIF fromhost register the request's type and length. +func (machine *RollupsMachine) writeRequestTypeAndLength(t requestType, length uint32) error { + fromhost := ((uint64(t) << 32) | (uint64(length) & 0xffffffff)) + return machine.inner.WriteHtifFromHostData(fromhost) +} + +func (machine RollupsMachine) readYieldReason() (emulator.HtifYieldReason, error) { + value, err := machine.inner.ReadHtifToHostData() + return emulator.HtifYieldReason(value >> 32), err +} + +func (machine RollupsMachine) readMachineCycle() (Cycle, error) { + cycle, err := machine.inner.ReadMCycle() + return Cycle(cycle), err +} diff --git a/pkg/rollupsmachine/server.go b/pkg/rollupsmachine/server.go new file mode 100644 index 000000000..050a160a0 --- /dev/null +++ b/pkg/rollupsmachine/server.go @@ -0,0 +1,129 @@ +// (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 the 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 in address. +// +// Most users of the machine library should not call this function. +// We recommend using machine.Destroy() instead. +func StopServer(address string) error { + slog.Warn("Trying to stop server", "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) + } +} diff --git a/pkg/rollupsmachine/tests/.gitignore b/pkg/rollupsmachine/tests/.gitignore new file mode 100644 index 000000000..6eb897e64 --- /dev/null +++ b/pkg/rollupsmachine/tests/.gitignore @@ -0,0 +1,6 @@ +rollup-accept/ +rollup-exception/ +rollup-notice/ +rollup-reject/ + +*/snapshot diff --git a/pkg/rollupsmachine/tests/echo/main.go b/pkg/rollupsmachine/tests/echo/main.go new file mode 100644 index 000000000..0d115bf8c --- /dev/null +++ b/pkg/rollupsmachine/tests/echo/main.go @@ -0,0 +1,93 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package main + +import ( + "log/slog" + + "github.com/cartesi/rollups-node/pkg/gollup" + "github.com/cartesi/rollups-node/pkg/libcmt" + "github.com/cartesi/rollups-node/pkg/rollupsmachine/tests/echo/protocol" +) + +var counter = 0 + +func log(s string) { + slog.Info("============================== " + s) +} + +func advance(emitter gollup.OutputEmitter, input *libcmt.Input) bool { + counter++ + + inputData := protocol.FromBytes[protocol.InputData](input.Data) + if inputData.Reject { + return false + } + if inputData.Exception { + emitter.RaiseException(protocol.AdvanceException) + return false + } + + for i := 0; i < inputData.Vouchers; i++ { + voucherData := protocol.VoucherData{Counter: counter, Quote: inputData.Quote, Index: i} + bytes, err := voucherData.ToBytes() + if err != nil { + panic(err) + } + emitter.SendVoucher(input.Sender, protocol.VoucherValue.Bytes(), bytes) + } + for i := 0; i < inputData.Notices; i++ { + noticeData := protocol.NoticeData{Counter: counter, Quote: inputData.Quote, Index: i} + bytes, err := noticeData.ToBytes() + if err != nil { + panic(err) + } + emitter.SendNotice(bytes) + } + for i := 0; i < inputData.Reports; i++ { + reportData := protocol.Report{Counter: counter, Quote: inputData.Quote, Index: i} + bytes, err := reportData.ToBytes() + if err != nil { + panic(err) + } + emitter.SendReport(bytes) + } + + return true +} + +func inspect(emitter gollup.ReportEmitter, query *libcmt.Query) bool { + queryData := protocol.FromBytes[protocol.QueryData](query.Data) + if queryData.Reject { + return false + } + if queryData.Exception { + emitter.RaiseException(protocol.InspectException) + return false + } + + for i := 0; i < queryData.Reports; i++ { + reportData := protocol.Report{Counter: counter, Quote: queryData.Quote, Index: i} + bytes, err := reportData.ToBytes() + if err != nil { + panic(err) + } + emitter.SendReport(bytes) + } + return true +} + +func main() { + log("Start app.") + defer log("End app.") + gollup, err := gollup.New(advance, inspect) + if err != nil { + panic(err) + } + defer gollup.Destroy() + err = gollup.Run() + if err != nil { + panic(err) + } +} diff --git a/pkg/rollupsmachine/tests/echo/protocol/protocol.go b/pkg/rollupsmachine/tests/echo/protocol/protocol.go new file mode 100644 index 000000000..886e05f10 --- /dev/null +++ b/pkg/rollupsmachine/tests/echo/protocol/protocol.go @@ -0,0 +1,70 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package protocol + +import ( + "encoding/json" + "math/big" +) + +var ( + VoucherValue *big.Int = big.NewInt(115) + + AdvanceException = []byte("big bad output exception") + InspectException = []byte("big bad report exception") +) + +type Data interface { + ToBytes() ([]byte, error) +} + +type InputData struct { + Quote string `json:"quote"` + Vouchers int `json:"vouchers"` + Notices int `json:"notices"` + Reports int `json:"reports"` + Reject bool `json:"reject"` + Exception bool `json:"exception"` +} + +type QueryData struct { + Quote string `json:"quote"` + Reports int `json:"reports"` + Reject bool `json:"reject"` + Exception bool `json:"exception"` +} + +type VoucherData struct { + Counter int `json:"counter"` + Quote string `json:"quote"` + Index int `json:"index"` +} + +type NoticeData struct { + Counter int `json:"counter"` + Quote string `json:"quote"` + Index int `json:"index"` +} + +type Report struct { + Counter int `json:"counter"` + Quote string `json:"quote"` + Index int `json:"index"` +} + +func FromBytes[T Data](bytes []byte) (data T) { + err := json.Unmarshal(bytes, &data) + if err != nil { + panic(err) + } + return +} + +func toBytes(data any) ([]byte, error) { return json.MarshalIndent(data, "", "\t") } + +func (data InputData) ToBytes() ([]byte, error) { return toBytes(data) } +func (data QueryData) ToBytes() ([]byte, error) { return toBytes(data) } +func (data VoucherData) ToBytes() ([]byte, error) { return toBytes(data) } +func (data NoticeData) ToBytes() ([]byte, error) { return toBytes(data) } +func (data Report) ToBytes() ([]byte, error) { return toBytes(data) } diff --git a/pkg/rollupsmachine/tests/machine_test.go b/pkg/rollupsmachine/tests/machine_test.go new file mode 100644 index 000000000..eabfbca92 --- /dev/null +++ b/pkg/rollupsmachine/tests/machine_test.go @@ -0,0 +1,551 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package tests + +import ( + "os" + "testing" + + "github.com/cartesi/rollups-node/pkg/emulator" + "github.com/cartesi/rollups-node/pkg/model" + rm "github.com/cartesi/rollups-node/pkg/rollupsmachine" + "github.com/cartesi/rollups-node/pkg/rollupsmachine/tests/echo/protocol" + + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +// ErrHalted. +// ErrFailed. +// ErrYieldedSoftly. + +// TestRollupsMachine runs all the tests for the rollupsmachine package. +func TestRollupsMachine(t *testing.T) { + initT(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)) } + +// ------------------------------------------------------------------------------------------------ + +type LoadSuite struct{ suite.Suite } + +func (suite *LoadSuite) TestOk() { + require := require.New(suite.T()) + + address, err := rm.StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) + require.Nil(err) + + machine, err := rm.Load("rollup-accept", address, &emulator.MachineRuntimeConfig{}) + require.Nil(err) + require.NotNil(machine) + + err = rm.StopServer(address) + require.Nil(err) +} + +// There is no server running at the given address. +func (suite *LoadSuite) TestInvalidAddress() { + require := require.New(suite.T()) + address := "invalid-address" + machine, err := rm.Load("rollup-accept", address, &emulator.MachineRuntimeConfig{}) + require.NotNil(err) + require.ErrorIs(err, rm.ErrRemoteLoadMachine) + require.Nil(machine) +} + +// There is not a machine stored at the given path. +func (suite *LoadSuite) TestInvalidStoredMachine() { + require := require.New(suite.T()) + + address, err := rm.StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) + require.Nil(err) + + machine, err := rm.Load("invalid-path", address, &emulator.MachineRuntimeConfig{}) + require.NotNil(err) + require.ErrorIs(err, rm.ErrRemoteLoadMachine) + require.Nil(machine) +} + +// The machine is not ready to receive requests, because the last yield was an automatic yield. +func (suite *LoadSuite) TestNotAtManualYield() { + require := require.New(suite.T()) + + address, err := rm.StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) + require.Nil(err) + + machine, err := rm.Load("rollup-notice", address, &emulator.MachineRuntimeConfig{}) + require.NotNil(err) + require.ErrorIs(err, rm.ErrNotReadyForRequests) + require.ErrorIs(err, rm.ErrNotAtManualYield) + require.Nil(machine) +} + +// The machine is not ready to receive requests, because the last input was rejected. +func (suite *LoadSuite) TestLastInputWasRejected() { + require := require.New(suite.T()) + + address, err := rm.StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) + require.Nil(err) + + machine, err := rm.Load("rollup-reject", address, &emulator.MachineRuntimeConfig{}) + require.NotNil(err) + require.ErrorIs(err, rm.ErrNotReadyForRequests) + require.ErrorIs(err, rm.ErrLastInputWasRejected) + require.Nil(machine) +} + +// TODO +// // The machine is not ready to receive requests, because the last input yielded an exception. +// func (suite *LoadSuite) TestLastInputYieldedAnException() { +// slog.Warn("Hey, I'm in TestRollupException!") +// +// address, err := rollupsmachine.StartServer(suite.serverVerbosity, 0, os.Stdout, os.Stderr) +// require.Nil(err) +// +// machine, err := rollupsmachine.Load(address, "rollup-exception", suite.runtimeConfig) +// require.NotNil(err) +// require.ErrorIs(err, rollupsmachine.ErrNotReadyForRequests) +// require.ErrorIs(err, rollupsmachine.ErrLastInputYieldedAnException) +// require.Nil(machine) +// } + +// ------------------------------------------------------------------------------------------------ + +type ForkSuite struct{ EchoSuite } + +// [ACTION 1] When we fork a machine +// [ACTION 2] and then destroy it, +// [EXPECTED] the function calls should not return with any errors. +func (suite *ForkSuite) TestOk() { + require := require.New(suite.T()) + + // Forks the machine. + forkedMachine, err := suite.machine.Fork() + require.Nil(err) + require.NotNil(forkedMachine) + + // Destroys the forked machine. + err = forkedMachine.Destroy() + require.Nil(err) +} + +// [ACTION 1] When we fork a machine +// [ACTION 2] and send an advance-state request to the forked machine, +// [EXPECTED] the father machine should remain unchanged. +func (suite *ForkSuite) TestWithAdvance() { + t := suite.T() + require := require.New(t) + + // Forks the machine. + forkedMachine, err := suite.machine.Fork() + require.Nil(err) + require.NotNil(forkedMachine) + defer func() { + err = forkedMachine.Destroy() + require.Nil(err) + }() + + quote := `"He loved Big Brother." -- 1984, George Orwell` + + { // Sends an input to the forked machine. + inputData := protocol.InputData{Quote: quote, Notices: 1} + outputs, _ := echoAdvance(t, suite.machine, inputData) + + notice := expectNotice(t, outputs[0]) + + noticeData := protocol.FromBytes[protocol.NoticeData](notice.Data) + require.Equal(1, noticeData.Counter) // the father machine received 1 advance-state request + require.Equal(inputData.Quote, noticeData.Quote) + require.Equal(0, noticeData.Index) + } + + { // Checks the father machine state. + queryData := protocol.QueryData{Quote: quote, Reports: 1} + reports := echoInspect(t, forkedMachine, queryData) + + report := protocol.FromBytes[protocol.Report](reports[0]) + require.Equal(0, report.Counter) // the forked machine received 0 advance-state requests + require.Equal(queryData.Quote, report.Quote) + require.Equal(0, report.Index) + } +} + +// TODO: test fork corrupted machine (server shut down). + +// ------------------------------------------------------------------------------------------------ + +type AdvanceSuite struct{ EchoSuite } + +func (suite *AdvanceSuite) TestAccept() { + inputData := protocol.InputData{Reject: false, Exception: false} + input := newInput(suite.T(), appContract, sender, inputData) + outputs, reports, _, err := suite.machine.Advance(input) + + require := suite.Require() + require.Nil(err) + require.Empty(outputs) + require.Empty(reports) +} + +func (suite *AdvanceSuite) TestReject() { + inputData := protocol.InputData{Reject: true, Exception: false} + input := newInput(suite.T(), appContract, sender, inputData) + outputs, reports, _, err := suite.machine.Advance(input) + + require := suite.Require() + require.ErrorIs(err, rm.ErrLastInputWasRejected) + require.Empty(outputs) + require.Empty(reports) +} + +func (suite *AdvanceSuite) TestException() { + inputData := protocol.InputData{Reject: false, Exception: true} + input := newInput(suite.T(), appContract, sender, inputData) + outputs, reports, _, err := suite.machine.Advance(input) + + require := suite.Require() + require.ErrorIs(err, rm.ErrLastInputYieldedAnException) + require.Empty(outputs) + require.Empty(reports) +} + +func (suite *AdvanceSuite) TestNoResponse() { + quote := `"He who controls the spice controls the universe." -- Dune 1984` + inputData := protocol.InputData{Quote: quote} + _, _ = echoAdvance(suite.T(), suite.machine, inputData) +} + +func (suite *AdvanceSuite) TestSingleResponse() { + quote := `"Time is an illusion. Lunchtime doubly so." -- THGTTG, Douglas Adams` + + suite.Run("Vouchers=1", func() { + t := suite.T() + inputData := protocol.InputData{Quote: quote, Vouchers: 1} + outputs, _ := echoAdvance(t, suite.machine, inputData) + voucher := expectVoucher(t, outputs[0]) + + voucherData := protocol.FromBytes[protocol.VoucherData](voucher.Data) + require := suite.Require() + require.Equal(1, voucherData.Counter) + require.Equal(inputData.Quote, voucherData.Quote) + require.Equal(0, voucherData.Index) + }) + + suite.Run("Notices=1", func() { + t := suite.T() + inputData := protocol.InputData{Quote: quote, Notices: 1} + outputs, _ := echoAdvance(t, suite.machine, inputData) + notice := expectNotice(t, outputs[0]) + + noticeData := protocol.FromBytes[protocol.NoticeData](notice.Data) + require := suite.Require() + require.Equal(2, noticeData.Counter) + require.Equal(inputData.Quote, noticeData.Quote) + require.Equal(0, noticeData.Index) + }) + + suite.Run("Reports=1", func() { + inputData := protocol.InputData{Quote: quote, Reports: 1} + _, reports := echoAdvance(suite.T(), suite.machine, inputData) + + reportData := protocol.FromBytes[protocol.Report](reports[0]) + require := suite.Require() + require.Equal(3, reportData.Counter) + require.Equal(inputData.Quote, reportData.Quote) + require.Equal(0, reportData.Index) + }) +} + +func (suite *AdvanceSuite) TestMultipleReponses() { + require := suite.Require() + + inputData := protocol.InputData{ + Quote: `"Any fool can tell a crisis when it arrives. + The real service to the state is to detect it in embryo." + -- Foundation, Isaac Asimov`, + Vouchers: 3, + Notices: 4, + Reports: 5, + } + outputs, reports := echoAdvance(suite.T(), suite.machine, inputData) + + { // outputs + numberOfVouchers := 0 + numberOfNotices := 0 + for _, output := range outputs { + voucher, notice, err := rm.DecodeOutput(output) + require.Nil(err) + if voucher != nil { + require.Nil(notice) + require.Equal(sender, voucher.Address) + require.Equal(protocol.VoucherValue.Int64(), voucher.Value.Int64()) + + voucherData := protocol.FromBytes[protocol.VoucherData](voucher.Data) + require.Equal(1, voucherData.Counter) + require.Equal(inputData.Quote, voucherData.Quote) + require.Equal(numberOfVouchers, voucherData.Index) + numberOfVouchers++ + } else if notice != nil { + require.Nil(voucher) + noticeData := protocol.FromBytes[protocol.NoticeData](notice.Data) + require.Equal(1, noticeData.Counter) + require.Equal(inputData.Quote, noticeData.Quote) + require.Equal(numberOfNotices, noticeData.Index) + numberOfNotices++ + } else { + panic("not a voucher and not a notice") + } + } + require.Equal(inputData.Vouchers, numberOfVouchers) + require.Equal(inputData.Notices, numberOfNotices) + } + + { // reports + for i, report := range reports { + reportData := protocol.FromBytes[protocol.Report](report) + require.Equal(1, reportData.Counter) + require.Equal(inputData.Quote, reportData.Quote) + require.Equal(i, reportData.Index) + } + } +} + +// ------------------------------------------------------------------------------------------------ + +type CyclesSuite struct{ EchoSuite } + +// When we send a request to the machine with machine.Max set too low, +// the function call should return with the ErrMaxCycles error. +func (suite *CyclesSuite) TestMaxCyclesError() { + quote := `"I must not fear. Fear is the mind-killer." -- Dune, Frank Herbert` + + suite.machine.Inc = 1000 + suite.machine.Max = 1 + + t := suite.T() + queryData := protocol.InputData{Quote: quote} + query := newQuery(t, queryData) + reports, err := suite.machine.Inspect(query) + require.Equal(t, rm.ErrMaxCycles, err) + require.Empty(t, reports) +} + +// When we send a request to the machine with machine.Max set too low, +// but the request gets fully processed within one run of machine.Inc cycles, +// then the function call should not return with the ErrMaxCycles error. +// +// If the machine needs two runs to process the input (for example, in case of an automatic yield), +// then the function call should return with the ErrMaxCycles error. +func (suite *CyclesSuite) TestSmallMaxBigInc() { + quote := `"Arrakis teaches the attitude of the knife + - chopping off what's incomplete and saying: + 'Now, it's complete because it's ended here.'" + -- Dune, Frank Herbert` + + suite.machine.Max = 1 + suite.machine.Inc = rm.DefaultMax + + suite.Run("Notices=0", func() { + t := suite.T() + inputData := protocol.InputData{Quote: quote} + _, _ = echoAdvance(t, suite.machine, inputData) + }) + + suite.Run("Notices=1", func() { + t := suite.T() + inputData := protocol.InputData{Quote: quote, Notices: 1} + input := newInput(t, appContract, sender, inputData) + outputs, reports, _, err := suite.machine.Advance(input) + require.Equal(t, rm.ErrMaxCycles, err) + require.Len(t, outputs, 1) + require.Empty(t, reports) + }) +} + +func (suite *CyclesSuite) TestInc() { + quote := `"Isn't it enough to see that a garden is beautiful + without having to believe that there are fairies at the bottom of it, too?" + -- THGTTG, Douglas Adams` + queryData := protocol.InputData{Quote: quote} + + suite.Run("Inc=DefaultInc", func() { + suite.machine.Inc = rm.DefaultInc + query := newQuery(suite.T(), queryData) + reports, err := suite.machine.Inspect(query) + require := suite.Require() + require.Nil(err) + require.Empty(reports) + }) + + suite.Run("Inc=100", func() { + suite.machine.Inc = 100 + query := newQuery(suite.T(), queryData) + reports, err := suite.machine.Inspect(query) + require := suite.Require() + require.Nil(err) + require.Empty(reports) + }) + + suite.Run("Inc=9", func() { + suite.T().Skip() // NOTE: takes too long + suite.machine.Inc = 9 + query := newQuery(suite.T(), queryData) + reports, err := suite.machine.Inspect(query) + require := suite.Require() + require.Nil(err) + require.Empty(reports) + }) +} + +// ------------------------------------------------------------------------------------------------ + +type InspectSuite struct{ EchoSuite } + +func (suite *InspectSuite) TestAccept() { + queryData := protocol.QueryData{Reject: false, Exception: false} + query := newQuery(suite.T(), queryData) + reports, err := suite.machine.Inspect(query) + + require := suite.Require() + require.Nil(err) + require.Empty(reports) +} + +func (suite *InspectSuite) TestReject() { + queryData := protocol.QueryData{Reject: true, Exception: false} + query := newQuery(suite.T(), queryData) + reports, err := suite.machine.Inspect(query) + + require := suite.Require() + require.ErrorIs(err, rm.ErrLastInputWasRejected) + require.Empty(reports) +} + +func (suite *InspectSuite) TestException() { + queryData := protocol.QueryData{Reject: false, Exception: true} + query := newQuery(suite.T(), queryData) + reports, err := suite.machine.Inspect(query) + + require := suite.Require() + require.ErrorIs(err, rm.ErrLastInputYieldedAnException) + require.Empty(reports) +} +func (suite *InspectSuite) TestNoResponse() { + quote := `"He who controls the spice controls the universe." -- Dune 1984` + queryData := protocol.QueryData{Quote: quote} + reports := echoInspect(suite.T(), suite.machine, queryData) + require.Empty(suite.T(), reports) +} + +func (suite *InspectSuite) TestSingleResponse() { + quote := `"Time is an illusion. Lunchtime doubly so." -- THGTTG, Douglas Adams` + queryData := protocol.QueryData{Quote: quote, Reports: 1} + reports := echoInspect(suite.T(), suite.machine, queryData) + + reportData := protocol.FromBytes[protocol.Report](reports[0]) + require := suite.Require() + require.Zero(reportData.Counter) + require.Equal(queryData.Quote, reportData.Quote) + require.Equal(0, reportData.Index) +} + +func (suite *InspectSuite) TestMultipleReponses() { + require := suite.Require() + quote := `"Any fool can tell a crisis when it arrives. + The real service to the state is to detect it in embryo." + -- Foundation, Isaac Asimov` + queryData := protocol.QueryData{Quote: quote, Reports: 7} + reports := echoInspect(suite.T(), suite.machine, queryData) + for i, report := range reports { + reportData := protocol.FromBytes[protocol.Report](report) + require.Zero(reportData.Counter) + require.Equal(queryData.Quote, reportData.Quote) + require.Equal(i, reportData.Index) + } +} + +// ------------------------------------------------------------------------------------------------ + +// EchoSuite is a superclass for other test suites +// that use the "echo" app; it does not contain tests. +type EchoSuite struct { + suite.Suite + machine *rm.RollupsMachine +} + +func (suite *EchoSuite) SetupTest() { + require := require.New(suite.T()) + + // Starts the server. + address, err := rm.StartServer(serverVerbosity, 0, os.Stdout, os.Stderr) + require.Nil(err) + + // Loads the "echo" application. + path := "echo/snapshot" + suite.machine, err = rm.Load(path, address, &emulator.MachineRuntimeConfig{}) + require.Nil(err) + require.NotNil(suite.machine) +} + +func (suite *EchoSuite) TearDownTest() { + // Destroys the machine and shuts down the server. + err := suite.machine.Destroy() + require.Nil(suite.T(), err) +} + +var appContract = [20]byte{} +var sender = [20]byte{} + +// advance sends an advance-state request to an echo machine +// and asserts that it produced the correct amount of outputs and reports. +func echoAdvance(t *testing.T, machine *rm.RollupsMachine, data protocol.InputData) ( + []rm.Output, + []rm.Report, +) { + input := newInput(t, appContract, sender, data) + outputs, reports, outputsHash, err := machine.Advance(input) + require.Nil(t, err) + require.Len(t, outputs, data.Vouchers+data.Notices) + require.Len(t, reports, data.Reports) + require.Len(t, outputsHash, model.HashSize) + return outputs, reports +} + +// echoInspect sends an inspect-state request to an echo machine +// and asserts that it produced the correct amount of reports. +func echoInspect(t *testing.T, machine *rm.RollupsMachine, data protocol.QueryData) []rm.Report { + query := newQuery(t, data) + reports, err := machine.Inspect(query) + require.Nil(t, err) + require.Len(t, reports, data.Reports) + return reports +} + +// expectVoucher decodes the output and asserts that it is a voucher. +func expectVoucher(t *testing.T, output rm.Output) *rm.Voucher { + voucher, notice, err := rm.DecodeOutput(output) + require.Nil(t, err) + require.NotNil(t, voucher) + require.Nil(t, notice) + require.Equal(t, sender, voucher.Address) + require.Equal(t, protocol.VoucherValue.Int64(), voucher.Value.Int64()) + return voucher +} + +// expectNotice decodes the output and asserts that it is a notice. +func expectNotice(t *testing.T, output rm.Output) *rm.Notice { + voucher, notice, err := rm.DecodeOutput(output) + require.Nil(t, err) + require.Nil(t, voucher) + require.NotNil(t, notice) + return notice +} diff --git a/pkg/rollupsmachine/tests/snapshot.go b/pkg/rollupsmachine/tests/snapshot.go new file mode 100644 index 000000000..81c6f9eb5 --- /dev/null +++ b/pkg/rollupsmachine/tests/snapshot.go @@ -0,0 +1,145 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package tests + +import ( + "fmt" + "log/slog" + "math" + "os" + "os/exec" + "strconv" + "strings" + "testing" + + "github.com/cartesi/rollups-node/pkg/emulator" + "github.com/stretchr/testify/require" +) + +var imagesPath = "/usr/local/share/cartesi-machine/images/" + +const riscvCC = "riscv64-linux-gnu-gcc-12" + +func init() { + if s, ok := os.LookupEnv("CARTESI_IMAGES_PATH"); ok { + imagesPath = s + } +} + +func simpleSnapshot(t *testing.T, + name string, + entrypoint string, + cycles uint64, + expectedBreakReason emulator.BreakReason) { + + // Removes previously stored machine. + runCmd(exec.Command("rm", "-rf", name)) + + // Creates the machine configuration. + config := defaultMachineConfig() + config.Dtb.Entrypoint = entrypoint + + machineCreateRunStore(t, name, config, cycles, expectedBreakReason, name) +} + +func crossCompiledSnapshot(t *testing.T, + name string, + cycles uint64, + expectedBreakReason emulator.BreakReason) { + + // Removes previously stored machine. + runCmd(exec.Command("rm", "-rf", name+"/snapshot")) + + // Removes temporary files. + defer runCmd( + exec.Command("rm", "-rf", name+"/temp"), + exec.Command("rm", "-f", name+"/"+name+".ext2")) + + // Builds the riscv64 binary from the Go program. + directory := name + "/temp/" + cmd := exec.Command("go", "build", "-o", directory+name, name+"/main.go") + cmd.Env = append(os.Environ(), "CC="+riscvCC, "CGO_ENABLED=1", "GOOS=linux", "GOARCH=riscv64") + runCmd(cmd) + + // Creates the .ext2 file. + k := uint64(10) + ext2 := name + "/" + name + ".ext2" + blocks := strconv.FormatUint(k*1024, 10) + cmd = exec.Command("xgenext2fs", "-f", "-b", blocks, "-d", directory, ext2) + runCmd(cmd) + + // Modifies the default config. + config := defaultMachineConfig() + config.Dtb.Init = fmt.Sprintf(`echo "Test Cartesi Machine" + busybox mkdir -p /run/drive-label && echo "root" > /run/drive-label/pmem0 + busybox mkdir -p "/mnt/%s" && busybox mount /dev/pmem1 "/mnt/%s" + busybox mkdir -p /run/drive-label && echo "%s" > /run/drive-label/pmem1`, + name, name, name) + config.Dtb.Entrypoint = fmt.Sprintf("CMT_DEBUG=yes /mnt/%s/%s", name, name) + config.FlashDrive = append(config.FlashDrive, emulator.MemoryRangeConfig{ + Start: 0x90000000000000, + Length: math.MaxUint64, + ImageFilename: name + "/" + name + ".ext2", + }) + + machineCreateRunStore(t, name, config, cycles, expectedBreakReason, name+"/snapshot") +} + +// ------------------------------------------------------------------------------------------------ + +func defaultMachineConfig() *emulator.MachineConfig { + config := emulator.NewDefaultMachineConfig() + config.Ram.Length = 64 << 20 + config.Ram.ImageFilename = imagesPath + "linux.bin" + config.Dtb.Bootargs = strings.Join([]string{"quiet", + "no4lvl", + "quiet", + "earlycon=sbi", + "console=hvc0", + "rootfstype=ext2", + "root=/dev/pmem0", + "rw", + "init=/usr/sbin/cartesi-init"}, " ") + config.FlashDrive = []emulator.MemoryRangeConfig{{ + Start: 0x80000000000000, + Length: math.MaxUint64, + ImageFilename: imagesPath + "rootfs.ext2", + }} + return config +} + +func machineCreateRunStore(t *testing.T, + name string, + config *emulator.MachineConfig, + cycles uint64, + expectedBreakReason emulator.BreakReason, + storeAt string) { + + // Creates the (local) machine. + machine, err := emulator.NewMachine(config, &emulator.MachineRuntimeConfig{}) + require.Nil(t, err) + defer machine.Delete() + + // Runs the machine. + reason, err := machine.Run(cycles) + require.Nil(t, err) + require.Equal(t, expectedBreakReason, reason, "%s != %s", expectedBreakReason, reason) + + // Stores the machine. + err = machine.Store(storeAt) + require.Nil(t, err) +} + +func runCmd(cmds ...*exec.Cmd) { + for _, cmd := range cmds { + slog.Debug("running", "command", cmd.String()) + output, err := cmd.CombinedOutput() + if s := string(output); s != "" { + slog.Debug(s) + } + if err != nil { + slog.Error(err.Error()) + } + } +} diff --git a/pkg/rollupsmachine/tests/util.go b/pkg/rollupsmachine/tests/util.go new file mode 100644 index 000000000..c23c053b3 --- /dev/null +++ b/pkg/rollupsmachine/tests/util.go @@ -0,0 +1,84 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package tests + +import ( + "encoding/hex" + "fmt" + "log" + "log/slog" + "testing" + + "github.com/cartesi/rollups-node/pkg/emulator" + "github.com/cartesi/rollups-node/pkg/rollupsmachine" + "github.com/cartesi/rollups-node/pkg/rollupsmachine/tests/echo/protocol" + "github.com/stretchr/testify/require" +) + +const serverVerbosity = rollupsmachine.ServerVerbosityInfo + +func init() { + log.SetFlags(log.Ltime) + slog.SetLogLoggerLevel(slog.LevelDebug) +} + +func initT(t *testing.T) { + var entrypoint string + cycles := uint64(1_000_000_000) + + slog.Debug("CREATING TEST MACHINES =============================") + + entrypoint = rollup("accept") + simpleSnapshot(t, "rollup-accept", entrypoint, cycles, emulator.BreakReasonYieldedManually) + + entrypoint = rollup("reject") + simpleSnapshot(t, "rollup-reject", entrypoint, cycles, emulator.BreakReasonYieldedManually) + + entrypoint = rollup("exception", "Paul Atreides") + simpleSnapshot(t, "rollup-exception", entrypoint, cycles, emulator.BreakReasonYieldedManually) + + entrypoint = rollup("notice", "Hari Seldon") + simpleSnapshot(t, "rollup-notice", entrypoint, cycles, emulator.BreakReasonYieldedAutomatically) + + crossCompiledSnapshot(t, "echo", cycles, emulator.BreakReasonYieldedManually) + + slog.Debug("FINISHED CREATING TEST MACHINES ====================") +} + +func payload(s string) string { + return fmt.Sprintf("echo '{ \"payload\": \"0x%s\" }'", hex.EncodeToString([]byte(s))) +} + +func rollup(s ...string) (cmd string) { + switch s[0] { + case "accept", "reject": + cmd = "rollup " + s[0] + case "notice", "exception": + cmd = payload(s[1]) + " | " + "rollup " + s[0] + default: + panic("invalid rollup action") + } + slog.Debug("stored machine", "command", cmd) + return +} + +func newInput(t *testing.T, appContract, sender [20]byte, data protocol.Data) []byte { + bytes, err := data.ToBytes() + require.Nil(t, err) + input, err := rollupsmachine.Input{ + AppContract: appContract, + Sender: sender, + Data: bytes, + }.Encode() + require.Nil(t, err) + return input +} + +func newQuery(t *testing.T, data protocol.Data) []byte { + bytes, err := data.ToBytes() + require.Nil(t, err) + query, err := rollupsmachine.Query{Data: bytes}.Encode() + require.Nil(t, err) + return query +}