diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c47ce7f5..002b801fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `Makefile` to help node developers setup their environment - Added experimental sunodo validator mode - Added instructions on how to run the node with Docker +- Added Cartesi Machine C API wrapper ### Changed diff --git a/cmd/cartesi-machine/README.md b/cmd/cartesi-machine/README.md new file mode 100644 index 000000000..4b16c4422 --- /dev/null +++ b/cmd/cartesi-machine/README.md @@ -0,0 +1,31 @@ +# Go bindings to the Cartesi Machine C API + +## Quick Start + +Ensure that the emulator headers and libraries are installed or point to them with: +``` +export CGO_CFLAGS="-I/foo/machine-emulator/src" +export CGO_LDFLAGS="-L/foo/machine-emulator/src" + +``` + +Build +``` +go build +``` + +Point to the directory containing the image files +``` +export CARTESI_IMAGES_PATH= +``` + +Run +``` +go run cmd/cartesi-machine/main.go --help +go run cmd/cartesi-machine/main.go +go run cmd/cartesi-machine/main.go --command="ls -l" +go run cmd/cartesi-machine/main.go --max-mcycle=0 --store=/tmp/maquina +go run cmd/cartesi-machine/main.go --load=/tmp/maquina --command="ls -l" +go run cmd/cartesi-machine/main.go --load=/tmp/maquina --initial-hash --final-hash +go run cmd/cartesi-machine/main.go --remote-address="localhost:5000"--load=/tmp/maquina --initial-hash --final-hash --command="ls -l" +``` \ No newline at end of file diff --git a/cmd/cartesi-machine/main.go b/cmd/cartesi-machine/main.go new file mode 100644 index 000000000..ebd336621 --- /dev/null +++ b/cmd/cartesi-machine/main.go @@ -0,0 +1,205 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +// Simple command line interface to the Cartesi Machine C API wrapper + +package main + +import ( + "flag" + "fmt" + "math" + "os" + "strings" + + "github.com/cartesi/rollups-node/pkg/emulator" +) + +func main() { + var machine *emulator.Machine + defer machine.Delete() + var mgr *emulator.RemoteMachineManager + defer mgr.Delete() + var err error + runtimeConfig := &emulator.MachineRuntimeConfig{} + + // Parse command line arguments + loadDir := flag.String("load", "", "load machine previously stored in ") + storeDir := flag.String("store", "", + "store machine to , where \"%h\" is substituted by the state hash in the directory name") + remoteAddress := flag.String("remote-address", "", + "use a remote cartesi machine listening to
instead of running a local cartesi machine") + remoteShutdown := flag.Bool("remote-shutdown", false, + "shutdown the remote cartesi machine after the execution") + noRemoteCreate := flag.Bool("no-remote-create", false, + "use existing cartesi machine in the remote server instead of creating a new one") + noRemoteDestroy := flag.Bool("no-remote-destroy", false, + "do not destroy the cartesi machine in the remote server after the execution") + ramImage := flag.String("ram-image", "", + "name of file containing RAM image") + dtbImage := flag.String("dtb-image", "", + "name of file containing DTB image (default: auto generated flattened device tree)") + maxMcycle := flag.Uint64("max-mcycle", math.MaxUint64, + "stop at a given mcycle") + initialHash := flag.Bool("initial-hash", false, + "print initial state hash before running machine") + finalHash := flag.Bool("final-hash", false, + "print final state hash when done") + commandLine := flag.String("command", "", + "command to run in the machine") + flag.Parse() + + // Connect to remote server and load/get machine + if remoteAddress != nil && *remoteAddress != "" { + fmt.Println("Connecting to remote server at ", *remoteAddress) + if mgr, err = emulator.NewRemoteMachineManager(*remoteAddress); err != nil { + fmt.Fprintln(os.Stderr, "****** Error creating remote machine manager: ", err) + os.Exit(1) + } + if noRemoteCreate != nil && *noRemoteCreate { + fmt.Println("Using existing remote machine") + if machine, err = mgr.GetMachine(); err != nil { + fmt.Fprintln(os.Stderr, "****** Error getting remote machine: ", err) + os.Exit(1) + } + } else if loadDir != nil && *loadDir != "" { + fmt.Println("Loading remote machine from ", *loadDir) + if machine, err = mgr.LoadMachine(*loadDir, runtimeConfig); err != nil { + fmt.Fprintln(os.Stderr, "****** Error loading machine: ", err) + os.Exit(1) + } + } + } else if loadDir != nil && *loadDir != "" { + fmt.Println("Loading machine from ", *loadDir) + if machine, err = emulator.LoadMachine(*loadDir, runtimeConfig); err != nil { + fmt.Fprintln(os.Stderr, "****** Error loading machine: ", err) + os.Exit(1) + } + } + + // No machine yet: build configuration and create machine + if machine == nil { + // build machine configuration + images_path := strings.TrimRight(os.Getenv("CARTESI_IMAGES_PATH"), "/") + "/" + cfg := emulator.NewDefaultMachineConfig() + cfg.Processor.Mimpid = math.MaxUint64 + cfg.Processor.Marchid = math.MaxUint64 + cfg.Processor.Mvendorid = math.MaxUint64 + cfg.Ram.ImageFilename = images_path + "linux.bin" + if ramImage != nil && *ramImage != "" { + fmt.Println("Using RAM image: ", *ramImage) + cfg.Ram.ImageFilename = *ramImage + } + cfg.Ram.Length = 64 << 20 + cfg.FlashDrive = []emulator.MemoryRangeConfig{ + { + Start: 0x80000000000000, + Length: 0xffffffffffffffff, + Shared: false, + ImageFilename: images_path + "rootfs.ext2", + }, + } + cfg.Dtb.Bootargs = "quiet earlycon=sbi console=hvc0 rootfstype=ext2 root=/dev/pmem0 rw init=/usr/sbin/cartesi-init" + if dtbImage != nil && *dtbImage != "" { + cfg.Dtb.ImageFilename = *dtbImage + } + cfg.Dtb.Init = `echo "Opa!" + busybox mkdir -p /run/drive-label && echo "root" > /run/drive-label/pmem0\ + USER=dapp + ` + if commandLine != nil && *commandLine != "" { + cfg.Dtb.Init = *commandLine + } + // create machine using configuration + if mgr == nil { + fmt.Println("Creating local machine") + if machine, err = emulator.NewMachine(cfg, runtimeConfig); err != nil { + fmt.Fprintln(os.Stderr, "****** Error creating machine: ", err) + os.Exit(1) + } + } else { + fmt.Println("Creating remote machine") + if machine, err = mgr.NewMachine(cfg, runtimeConfig); err != nil { + fmt.Fprintln(os.Stderr, "****** Error creating remote machine: ", err) + os.Exit(1) + } + + } + } + + // No machine yet? Too bad + if machine == nil { + fmt.Fprintln(os.Stderr, "****** No machine to run") + os.Exit(1) + } + + // Print initial hash + if initialHash != nil && *initialHash { + if hash, err := machine.GetRootHash(); err != nil { + fmt.Fprintln(os.Stderr, "****** Error getting root hash: ", err) + os.Exit(1) + } else { + fmt.Println("Initial hash: ", hash.String()) + } + } + + // Run machine + var breakReason emulator.BreakReason + if breakReason, err = machine.Run(*maxMcycle); err != nil { + fmt.Fprintln(os.Stderr, "****** Error running machine: ", err) + os.Exit(1) + } + switch breakReason { + case emulator.BreakReasonFailed: + fmt.Println("Machine failed") + case emulator.BreakReasonHalted: + fmt.Println("Machine halted") + case emulator.BreakReasonYieldedManually: + fmt.Println("Machine yielded manually") + case emulator.BreakReasonYieldedAutomatically: + fmt.Println("Machine yielded automatically") + case emulator.BreakReasonYieldedSoftly: + fmt.Println("Machine yielded softly") + case emulator.BreakReasonReachedTargetMcycle: + fmt.Println("Machine reached target mcycle") + default: + fmt.Println("Machine stopped for unknown reason") + } + + cycle, _ := machine.ReadCSR(emulator.ProcCsrMcycle) + fmt.Println("mcycle: ", cycle) + + // Print final hash + if finalHash != nil && *finalHash { + if hash, err := machine.GetRootHash(); err == nil { + fmt.Println("Final hash: ", hash.String()) + } + } + + // Store machine + if storeDir != nil && *storeDir != "" { + fmt.Println("Storing machine in ", *storeDir) + if err = machine.Store(*storeDir); err != nil { + fmt.Fprintln(os.Stderr, "****** Error storing machine: ", err) + os.Exit(1) + } + } + + // Cleanup + if mgr != nil { + if !*noRemoteDestroy { + fmt.Println("Destroying remote machine") + if err = machine.Destroy(); err != nil { + fmt.Fprintln(os.Stderr, "****** Error destroying remote machine: ", err) + os.Exit(1) + } + } + if *remoteShutdown { + fmt.Println("Shutting down remote machine") + if err = mgr.Shutdown(); err != nil { + fmt.Fprintln(os.Stderr, "****** Error shutting down remote server: ", err) + os.Exit(1) + } + } + } +} diff --git a/internal/services/linewriter.go b/internal/linewriter/linewriter.go similarity index 72% rename from internal/services/linewriter.go rename to internal/linewriter/linewriter.go index 44dc3e482..6f610a676 100644 --- a/internal/services/linewriter.go +++ b/internal/linewriter/linewriter.go @@ -1,28 +1,28 @@ // (c) Cartesi and individual authors (see AUTHORS) // SPDX-License-Identifier: Apache-2.0 (see LICENSE) -package services +package linewriter import ( "bytes" "io" ) -// lineWriter accumulates the received data in a buffer and writes it to the inner writer when it +// LineWriter accumulates the received data in a buffer and writes it to the inner writer when it // encounters a new line, ignoring empty lines in the process. -// lineWriter assumes the inner writer does not returns an error. -type lineWriter struct { +// LineWriter assumes the inner writer does not returns an error. +type LineWriter struct { inner io.Writer buffer bytes.Buffer } -func newLineWriter(inner io.Writer) *lineWriter { - return &lineWriter{ +func New(inner io.Writer) *LineWriter { + return &LineWriter{ inner: inner, } } -func (w *lineWriter) Write(data []byte) (int, error) { +func (w *LineWriter) Write(data []byte) (int, error) { _, err := w.buffer.Write(data) if err != nil { // Not possible given bytes.Buffer spec diff --git a/internal/services/linewriter_test.go b/internal/linewriter/linewriter_test.go similarity index 97% rename from internal/services/linewriter_test.go rename to internal/linewriter/linewriter_test.go index a65481cc2..c87443068 100644 --- a/internal/services/linewriter_test.go +++ b/internal/linewriter/linewriter_test.go @@ -1,7 +1,7 @@ // (c) Cartesi and individual authors (see AUTHORS) // SPDX-License-Identifier: Apache-2.0 (see LICENSE) -package services +package linewriter import ( "bytes" @@ -24,7 +24,7 @@ func (w *mockWriter) Write(p []byte) (int, error) { type LineWriterSuite struct { suite.Suite mock *mockWriter - writer *lineWriter + writer *LineWriter } func TestLineWriterSuite(t *testing.T) { @@ -33,7 +33,7 @@ func TestLineWriterSuite(t *testing.T) { func (s *LineWriterSuite) SetupTest() { s.mock = &mockWriter{} - s.writer = newLineWriter(s.mock) + s.writer = New(s.mock) } func (s *LineWriterSuite) TestItWritesLines() { diff --git a/internal/node/machineadvancer/advancer.go b/internal/node/machineadvancer/advancer.go new file mode 100644 index 000000000..5b79c3e76 --- /dev/null +++ b/internal/node/machineadvancer/advancer.go @@ -0,0 +1,35 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package machineadvancer + +import "github.com/cartesi/rollups-node/internal/node/nodemachine" + +type Input = []byte +type Output = []byte +type Report = []byte +type Hash = [32]byte + +func GetInputs() []Input { + return []Input{} +} + +func Store(outputs []Output, reports []Report, outputsHash Hash, machineHash Hash) error { + return nil +} + +func StartAdvanceServer(machine *nodemachine.NodeMachine) { + for { + for _, input := range GetInputs() { + outputs, reports, outputsHash, machineHash, err := machine.Advance(input) + if err != nil { + panic("TODO") + } + + err = Store(outputs, reports, outputsHash, machineHash) + if err != nil { + panic("TODO") + } + } + } +} diff --git a/internal/node/nodemachine/machine.go b/internal/node/nodemachine/machine.go new file mode 100644 index 000000000..cb4405201 --- /dev/null +++ b/internal/node/nodemachine/machine.go @@ -0,0 +1,108 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package nodemachine + +import ( + "context" + + "github.com/cartesi/rollups-node/internal/node/nodemachine/pmutex" + "github.com/cartesi/rollups-node/pkg/model" + "github.com/cartesi/rollups-node/pkg/rollupsmachine" + + "golang.org/x/sync/semaphore" +) + +type NodeMachine struct { + *rollupsmachine.RollupsMachine + + // Ensures advance/inspect mutual exclusion when accessing the inner RollupsMachine. + // Advances have a higher priority than Inspects to acquire the lock. + mutex *pmutex.PMutex + + // Controls how many inspects can be concurrently active. + inspects *semaphore.Weighted +} + +func New(machine *rollupsmachine.RollupsMachine, maxConcurrentInspects int8) *NodeMachine { + return &NodeMachine{ + RollupsMachine: machine, + mutex: pmutex.New(), + inspects: semaphore.NewWeighted(int64(maxConcurrentInspects)), + } +} + +func (machine *NodeMachine) Advance(input []byte) ( + outputs []rollupsmachine.Output, + reports []rollupsmachine.Report, + outputsHash model.Hash, + machineHash model.Hash, + err error) { + + var fork *rollupsmachine.RollupsMachine + + { // Forks the machine. + machine.mutex.HLock() + defer machine.mutex.Unlock() + fork, err = machine.Fork() + if err != nil { + return outputs, reports, outputsHash, machineHash, err + } + } + + // Sends the advance-state request. + outputs, reports, outputsHash, err = fork.Advance(input) + if err != nil { + return outputs, reports, outputsHash, machineHash, err + } + + // Gets the post-advance machine hash. + machineHash, err = fork.Hash() + if err != nil { + return outputs, reports, outputsHash, machineHash, err + } + + { // Destroys the old machine and updates the current one. + machine.mutex.HLock() + defer machine.mutex.Unlock() + err = machine.Destroy() + if err != nil { + return outputs, reports, outputsHash, machineHash, err + } + machine.RollupsMachine = fork + } + + return outputs, reports, outputsHash, machineHash, err +} + +func (machine *NodeMachine) Inspect(ctx context.Context, query []byte) ( + []rollupsmachine.Report, + error) { + + // Controls how many inspects can be concurrently active. + err := machine.inspects.Acquire(ctx, 1) + if err != nil { + return nil, err + } + defer machine.inspects.Release(1) + + // Forks the machine. + var forkedMachine *rollupsmachine.RollupsMachine + { + machine.mutex.LLock() + defer machine.mutex.Unlock() + forkedMachine, err = machine.Fork() + if err != nil { + return nil, err + } + } + + // Sends the inspect-state request. + reports, err := forkedMachine.Inspect(query) + if err != nil { + return nil, err + } + + // Destroys the forked machine and returns the reports. + return reports, forkedMachine.Destroy() +} diff --git a/internal/node/nodemachine/pmutex/pmutex.go b/internal/node/nodemachine/pmutex/pmutex.go new file mode 100644 index 000000000..1603a6869 --- /dev/null +++ b/internal/node/nodemachine/pmutex/pmutex.go @@ -0,0 +1,56 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package pmutex + +import ( + "sync" + "sync/atomic" +) + +// A PMutex is a mutual exclusion lock with priority capabilities. +// A call to HLock always acquires the mutex before LLock. +type PMutex struct { + // Main mutex. + mutex *sync.Mutex + + // Condition variable for the waiting low-priority threads. + waitingLow *sync.Cond + + // Quantity of high-priority threads waiting to acquire the lock. + waitingHigh *atomic.Int32 +} + +// New creates a new PMutex. +func New() *PMutex { + mutex := &sync.Mutex{} + return &PMutex{ + mutex: mutex, + waitingLow: sync.NewCond(mutex), + waitingHigh: &atomic.Int32{}, + } +} + +// HLock acquires the mutex for the high-priority threads. +func (lock *PMutex) HLock() { + lock.waitingHigh.Add(1) + lock.mutex.Lock() + lock.waitingHigh.Add(-1) +} + +// LLock acquires the mutex for the low-priority threads. +// It waits until there are no high-priority threads trying to acquire the lock. +func (lock *PMutex) LLock() { + lock.mutex.Lock() + for lock.waitingHigh.Load() != 0 { + // NOTE: a cond.Wait() releases the lock uppon being called + // and tries to acquire it after being awakened. + lock.waitingLow.Wait() + } +} + +// Unlock releases the mutex for both types of threads. +func (lock *PMutex) Unlock() { + lock.waitingLow.Broadcast() + lock.mutex.Unlock() +} diff --git a/internal/services/command.go b/internal/services/command.go index ad33b3910..f97f4840d 100644 --- a/internal/services/command.go +++ b/internal/services/command.go @@ -13,6 +13,8 @@ import ( "strings" "syscall" "time" + + "github.com/cartesi/rollups-node/internal/linewriter" ) const ( @@ -43,8 +45,8 @@ type CommandService struct { func (s CommandService) Start(ctx context.Context, ready chan<- struct{}) error { cmd := exec.CommandContext(ctx, s.Path, s.Args...) cmd.Env = s.Env - cmd.Stderr = newLineWriter(commandLogger{s.Name}) - cmd.Stdout = newLineWriter(commandLogger{s.Name}) + cmd.Stderr = linewriter.New(commandLogger{s.Name}) + cmd.Stdout = linewriter.New(commandLogger{s.Name}) cmd.Cancel = func() error { err := cmd.Process.Signal(syscall.SIGTERM) if err != nil { diff --git a/internal/services/server-manager.go b/internal/services/server-manager.go index cb2719d0c..49d5b1792 100644 --- a/internal/services/server-manager.go +++ b/internal/services/server-manager.go @@ -14,6 +14,8 @@ import ( "strings" "syscall" "time" + + "github.com/cartesi/rollups-node/internal/linewriter" ) // ServerManager is a variation of CommandService used to manually stop @@ -48,8 +50,8 @@ func (s ServerManager) Start(ctx context.Context, ready chan<- struct{}) error { cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout } else { - cmd.Stderr = newLineWriter(commandLogger{s.Name}) - cmd.Stdout = newLineWriter(commandLogger{s.Name}) + cmd.Stderr = linewriter.New(commandLogger{s.Name}) + cmd.Stdout = linewriter.New(commandLogger{s.Name}) } // Without a delay, cmd.Wait() will block forever waiting for the I/O pipes // to be closed diff --git a/pkg/emulator/emulator.go b/pkg/emulator/emulator.go new file mode 100644 index 000000000..be011f0e1 --- /dev/null +++ b/pkg/emulator/emulator.go @@ -0,0 +1,631 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package emulator + +// #cgo LDFLAGS: -lcartesi -lcartesi_jsonrpc +// #include +// #include "cartesi-machine/jsonrpc-machine-c-api.h" +import "C" + +import ( + "encoding/hex" + "fmt" + "unsafe" +) + +// ------------------------------------------------------------------------------------------------ +// Error +// ------------------------------------------------------------------------------------------------ + +type ErrorCode int32 + +const ( + ErrCodeOk ErrorCode = C.CM_ERROR_OK + ErrCodeInvalidArgument ErrorCode = C.CM_ERROR_INVALID_ARGUMENT + ErrCodeDomainError ErrorCode = C.CM_ERROR_DOMAIN_ERROR + ErrCodeLengthError ErrorCode = C.CM_ERROR_LENGTH_ERROR + ErrCodeOutOfRange ErrorCode = C.CM_ERROR_OUT_OF_RANGE + ErrCodeLogicError ErrorCode = C.CM_ERROR_LOGIC_ERROR + ErrCodeBadOptionalAccess ErrorCode = C.CM_ERROR_BAD_OPTIONAL_ACCESS + ErrCodeRuntimeError ErrorCode = C.CM_ERROR_RUNTIME_ERROR + ErrCodeRangeError ErrorCode = C.CM_ERROR_RANGE_ERROR + ErrCodeOverflowError ErrorCode = C.CM_ERROR_OVERFLOW_ERROR + ErrCodeUnderflowError ErrorCode = C.CM_ERROR_UNDERFLOW_ERROR + ErrCodeRegexError ErrorCode = C.CM_ERROR_REGEX_ERROR + ErrCodeSystemIosBaseFailure ErrorCode = C.CM_ERROR_SYSTEM_IOS_BASE_FAILURE + ErrCodeFilesystemError ErrorCode = C.CM_ERROR_FILESYSTEM_ERROR + ErrCodeAtomicTxError ErrorCode = C.CM_ERROR_ATOMIC_TX_ERROR + ErrCodeNonexistingLocalTime ErrorCode = C.CM_ERROR_NONEXISTING_LOCAL_TIME + ErrCodeAmbiguousLocalTime ErrorCode = C.CM_ERROR_AMBIGUOUS_LOCAL_TIME + ErrCodeFormatError ErrorCode = C.CM_ERROR_FORMAT_ERROR + ErrCodeBadTypeid ErrorCode = C.CM_ERROR_BAD_TYPEID + ErrCodeBadCast ErrorCode = C.CM_ERROR_BAD_CAST + ErrCodeBadAnyCast ErrorCode = C.CM_ERROR_BAD_ANY_CAST + ErrCodeBadWeakPtr ErrorCode = C.CM_ERROR_BAD_WEAK_PTR + ErrCodeBadFunctionCall ErrorCode = C.CM_ERROR_BAD_FUNCTION_CALL + ErrCodeBadAlloc ErrorCode = C.CM_ERROR_BAD_ALLOC + ErrCodeBadArrayNewLength ErrorCode = C.CM_ERROR_BAD_ARRAY_NEW_LENGTH + ErrCodeBadException ErrorCode = C.CM_ERROR_BAD_EXCEPTION + ErrCodeBadVariantAccess ErrorCode = C.CM_ERROR_BAD_VARIANT_ACCESS + ErrCodeException ErrorCode = C.CM_ERROR_EXCEPTION + ErrCodeUnknown ErrorCode = C.CM_ERROR_UNKNOWN +) + +type Error struct { + Code ErrorCode + Msg string +} + +func (e Error) Error() string { + return fmt.Sprintf("cartesi machine error %d (%s)", e.Code, e.Msg) +} + +func newError(code C.int, msg *C.char) error { + defer C.cm_delete_cstring(msg) + if code != C.CM_ERROR_OK { + return &Error{Code: ErrorCode(code), Msg: C.GoString(msg)} + } + return nil +} + +// ------------------------------------------------------------------------------------------------ +// Types +// ------------------------------------------------------------------------------------------------ + +type BreakReason int32 + +const ( + BreakReasonFailed BreakReason = C.CM_BREAK_REASON_FAILED + BreakReasonHalted BreakReason = C.CM_BREAK_REASON_HALTED + BreakReasonYieldedManually BreakReason = C.CM_BREAK_REASON_YIELDED_MANUALLY + BreakReasonYieldedAutomatically BreakReason = C.CM_BREAK_REASON_YIELDED_AUTOMATICALLY + BreakReasonYieldedSoftly BreakReason = C.CM_BREAK_REASON_YIELDED_SOFTLY + BreakReasonReachedTargetMcycle BreakReason = C.CM_BREAK_REASON_REACHED_TARGET_MCYCLE +) + +func (reason BreakReason) String() (s string) { + switch reason { + case BreakReasonFailed: + s = "failed" + case BreakReasonHalted: + s = "halted" + case BreakReasonYieldedManually: + s = "yielded manually" + case BreakReasonYieldedAutomatically: + s = "yielded automatically" + case BreakReasonYieldedSoftly: + s = "yielded softly" + case BreakReasonReachedTargetMcycle: + s = "reached target mcycle" + default: + return "invalid break reason" + } + return "break reason: " + s + +} + +type ProcessorCSR int32 + +const ( + ProcCsrPc ProcessorCSR = C.CM_PROC_PC + ProcCsrFcsr ProcessorCSR = C.CM_PROC_FCSR + ProcCsrMvendorid ProcessorCSR = C.CM_PROC_MVENDORID + ProcCsrMarchid ProcessorCSR = C.CM_PROC_MARCHID + ProcCsrMimpid ProcessorCSR = C.CM_PROC_MIMPID + ProcCsrMcycle ProcessorCSR = C.CM_PROC_MCYCLE + ProcCsrIcycleinstret ProcessorCSR = C.CM_PROC_ICYCLEINSTRET + ProcCsrMstatus ProcessorCSR = C.CM_PROC_MSTATUS + ProcCsrMtvec ProcessorCSR = C.CM_PROC_MTVEC + ProcCsrMscratch ProcessorCSR = C.CM_PROC_MSCRATCH + ProcCsrMepc ProcessorCSR = C.CM_PROC_MEPC + ProcCsrMcause ProcessorCSR = C.CM_PROC_MCAUSE + ProcCsrMtval ProcessorCSR = C.CM_PROC_MTVAL + ProcCsrMisa ProcessorCSR = C.CM_PROC_MISA + ProcCsrMie ProcessorCSR = C.CM_PROC_MIE + ProcCsrMip ProcessorCSR = C.CM_PROC_MIP + ProcCsrMedeleg ProcessorCSR = C.CM_PROC_MEDELEG + ProcCsrMideleg ProcessorCSR = C.CM_PROC_MIDELEG + ProcCsrMcounteren ProcessorCSR = C.CM_PROC_MCOUNTEREN + ProcCsrMenvcfg ProcessorCSR = C.CM_PROC_MENVCFG + ProcCsrStvec ProcessorCSR = C.CM_PROC_STVEC + ProcCsrSscratch ProcessorCSR = C.CM_PROC_SSCRATCH + ProcCsrSepc ProcessorCSR = C.CM_PROC_SEPC + ProcCsrScause ProcessorCSR = C.CM_PROC_SCAUSE + ProcCsrStval ProcessorCSR = C.CM_PROC_STVAL + ProcCsrSatp ProcessorCSR = C.CM_PROC_SATP + ProcCsrScounteren ProcessorCSR = C.CM_PROC_SCOUNTEREN + ProcCsrSenvcfg ProcessorCSR = C.CM_PROC_SENVCFG + ProcCsrIlrsc ProcessorCSR = C.CM_PROC_ILRSC + ProcCsrIflags ProcessorCSR = C.CM_PROC_IFLAGS + ProcCsrClintMtimecmp ProcessorCSR = C.CM_PROC_CLINT_MTIMECMP + ProcCsrHtifTohost ProcessorCSR = C.CM_PROC_HTIF_TOHOST + ProcCsrHtifFromhost ProcessorCSR = C.CM_PROC_HTIF_FROMHOST + ProcCsrHtifIhalt ProcessorCSR = C.CM_PROC_HTIF_IHALT + ProcCsrHtifIconsole ProcessorCSR = C.CM_PROC_HTIF_ICONSOLE + ProcCsrHtifIyield ProcessorCSR = C.CM_PROC_HTIF_IYIELD + ProcCsrUarchPc ProcessorCSR = C.CM_PROC_UARCH_PC + ProcCsrUarchCycle ProcessorCSR = C.CM_PROC_UARCH_CYCLE + ProcCsrUarchHaltFlag ProcessorCSR = C.CM_PROC_UARCH_HALT_FLAG +) + +type MachineRuntimeConfig struct { + Concurrency ConcurrencyRuntimeConfig + Htif HtifRuntimeConfig + SkipRootHashCheck bool + SkipVersionCheck bool + SoftYield bool +} + +type HtifRuntimeConfig struct { + NoConsolePutchar bool +} + +type ConcurrencyRuntimeConfig struct { + UpdateMerkleTree uint64 +} + +type MachineConfig struct { + Processor ProcessorConfig + Ram RamConfig + Dtb DtbConfig + FlashDrive []MemoryRangeConfig + Tlb TlbConfig + Clint ClintConfig + Htif HtifConfig + Cmio CmioConfig + Uarch UarchConfig +} + +type ProcessorConfig struct { + X [32]uint64 + F [32]uint64 + Pc uint64 + Fcsr uint64 + Mvendorid uint64 + Marchid uint64 + Mimpid uint64 + Mcycle uint64 + Icycleinstret uint64 + Mstatus uint64 + Mtvec uint64 + Mscratch uint64 + Mepc uint64 + Mcause uint64 + Mtval uint64 + Misa uint64 + Mie uint64 + Mip uint64 + Medeleg uint64 + Mideleg uint64 + Mcounteren uint64 + Menvcfg uint64 + Stvec uint64 + Sscratch uint64 + Sepc uint64 + Scause uint64 + Stval uint64 + Satp uint64 + Scounteren uint64 + Senvcfg uint64 + Ilrsc uint64 + Iflags uint64 +} + +type RamConfig struct { + Length uint64 + ImageFilename string +} + +type DtbConfig struct { + Bootargs string + Init string + Entrypoint string + ImageFilename string +} + +type MemoryRangeConfig struct { + Start uint64 + Length uint64 + Shared bool + ImageFilename string +} + +type TlbConfig struct { + ImageFilename string +} + +type ClintConfig struct { + Mtimecmp uint64 +} + +type HtifConfig struct { + Fromhost uint64 + Tohost uint64 + ConsoleGetchar bool + YieldManual bool + YieldAutomatic bool +} + +type CmioBufferConfig struct { + Shared bool + ImageFilename string +} + +type CmioConfig struct { + RxBuffer CmioBufferConfig + TxBuffer CmioBufferConfig +} + +type UarchRamConfig struct { + ImageFilename string +} + +type UarchProcessorConfig struct { + X [32]uint64 + Pc uint64 + Cycle uint64 + HaltFlag bool +} + +type UarchConfig struct { + Processor UarchProcessorConfig + Ram UarchRamConfig +} + +// ------------------------------------------------------------------------------------------------ +// MachineConfig +// ------------------------------------------------------------------------------------------------ + +func NewDefaultMachineConfig() *MachineConfig { + ref := theirMachineConfigCRef{} + defer ref.free() + ref.cref = C.cm_new_default_machine_config() + return ref.makeGoRef() +} + +func GetDefaultMachineConfig() (*MachineConfig, error) { + theirCfg := theirMachineConfigCRef{} + defer theirCfg.free() + var msg *C.char + code := C.cm_get_default_config(&theirCfg.cref, &msg) + if err := newError(code, msg); err != nil { + return nil, err + } + return theirCfg.makeGoRef(), nil +} + +// ------------------------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------------------------ + +type MerkleTreeHash [32]byte + +func (hash *MerkleTreeHash) String() string { + return hex.EncodeToString(hash[:]) +} + +type ourMemoryRangeConfig struct { + cref *C.cm_memory_range_config +} + +func (config *MemoryRangeConfig) makeCRef() (ref *ourMemoryRangeConfig) { + ref = &ourMemoryRangeConfig{ + cref: (*C.cm_memory_range_config)(C.calloc(1, C.sizeof_cm_memory_range_config)), + } + c := ref.cref + c.start = (C.uint64_t)(config.Start) + c.length = (C.uint64_t)(config.Length) + c.shared = (C.bool)(config.Shared) + c.image_filename = makeCString(&config.ImageFilename) + return ref +} + +func (configRef *ourMemoryRangeConfig) free() { + if configRef == nil || configRef.cref == nil { + return + } + C.free(unsafe.Pointer(configRef.cref.image_filename)) + C.free(unsafe.Pointer(configRef.cref)) + configRef.cref = nil +} + +// cm_machine_runtime_config allocated by us +type ourMachineRuntimeConfigCRef struct { + cref *C.cm_machine_runtime_config +} + +func (config *MachineRuntimeConfig) makeCRef() (ref *ourMachineRuntimeConfigCRef) { + ref = &ourMachineRuntimeConfigCRef{ + cref: (*C.cm_machine_runtime_config)(C.calloc(1, C.sizeof_cm_machine_runtime_config)), + } + cRuntime := ref.cref + cRuntime.skip_root_hash_check = (C.bool)(config.SkipRootHashCheck) + cRuntime.skip_version_check = (C.bool)(config.SkipVersionCheck) + cRuntime.soft_yield = (C.bool)(config.SoftYield) + + cHtif := &ref.cref.htif + htif := &config.Htif + cHtif.no_console_putchar = (C.bool)(htif.NoConsolePutchar) + + cConcurrency := &ref.cref.concurrency + concurrency := &config.Concurrency + cConcurrency.update_merkle_tree = (C.uint64_t)(concurrency.UpdateMerkleTree) + + return ref +} + +func (configRef *ourMachineRuntimeConfigCRef) free() { + if configRef == nil || configRef.cref == nil { + return + } + C.free(unsafe.Pointer(configRef.cref)) + configRef.cref = nil +} + +// cm_machine_config allocated by us +type ourMachineConfigCRef struct { + cref *C.cm_machine_config +} + +func (config *MachineConfig) makeCRef() (ref *ourMachineConfigCRef) { + ref = &ourMachineConfigCRef{ + cref: (*C.cm_machine_config)(C.calloc(1, C.sizeof_cm_machine_config)), + } + // Processor + cProcessor := &ref.cref.processor + processor := &config.Processor + for i := 0; i < 31; i++ { + cProcessor.x[i+1] = (C.uint64_t)(processor.X[i]) + } + for i := 0; i < 31; i++ { + cProcessor.f[i+1] = (C.uint64_t)(processor.F[i]) + } + cProcessor.pc = (C.uint64_t)(processor.Pc) + cProcessor.fcsr = (C.uint64_t)(processor.Fcsr) + cProcessor.mvendorid = (C.uint64_t)(processor.Mvendorid) + cProcessor.marchid = (C.uint64_t)(processor.Marchid) + cProcessor.mimpid = (C.uint64_t)(processor.Mimpid) + cProcessor.mcycle = (C.uint64_t)(processor.Mcycle) + cProcessor.icycleinstret = (C.uint64_t)(processor.Icycleinstret) + cProcessor.mstatus = (C.uint64_t)(processor.Mstatus) + cProcessor.mtvec = (C.uint64_t)(processor.Mtvec) + cProcessor.mscratch = (C.uint64_t)(processor.Mscratch) + cProcessor.mepc = (C.uint64_t)(processor.Mepc) + cProcessor.mcause = (C.uint64_t)(processor.Mcause) + cProcessor.mtval = (C.uint64_t)(processor.Mtval) + cProcessor.misa = (C.uint64_t)(processor.Misa) + cProcessor.mie = (C.uint64_t)(processor.Mie) + cProcessor.mip = (C.uint64_t)(processor.Mip) + cProcessor.medeleg = (C.uint64_t)(processor.Medeleg) + cProcessor.mideleg = (C.uint64_t)(processor.Mideleg) + cProcessor.mcounteren = (C.uint64_t)(processor.Mcounteren) + cProcessor.menvcfg = (C.uint64_t)(processor.Menvcfg) + cProcessor.stvec = (C.uint64_t)(processor.Stvec) + cProcessor.sscratch = (C.uint64_t)(processor.Sscratch) + cProcessor.sepc = (C.uint64_t)(processor.Sepc) + cProcessor.scause = (C.uint64_t)(processor.Scause) + cProcessor.stval = (C.uint64_t)(processor.Stval) + cProcessor.satp = (C.uint64_t)(processor.Satp) + cProcessor.scounteren = (C.uint64_t)(processor.Scounteren) + cProcessor.senvcfg = (C.uint64_t)(processor.Senvcfg) + cProcessor.ilrsc = (C.uint64_t)(processor.Ilrsc) + cProcessor.iflags = (C.uint64_t)(processor.Iflags) + + cRam := &ref.cref.ram + ram := &config.Ram + cRam.length = (C.uint64_t)(ram.Length) + cRam.image_filename = makeCString(&ram.ImageFilename) + + cDtb := &ref.cref.dtb + dtb := &config.Dtb + cDtb.bootargs = makeCString(&dtb.Bootargs) + cDtb.init = makeCString(&dtb.Init) + cDtb.entrypoint = makeCString(&dtb.Entrypoint) + cDtb.image_filename = makeCString(&dtb.ImageFilename) + + // flash + cFlashDrive := &ref.cref.flash_drive + flashDrive := &config.FlashDrive + cFlashDrive.count = (C.ulong)(len(*flashDrive)) + cFlashDrive.entry = (*C.cm_memory_range_config)(C.calloc((C.ulong)(len(*flashDrive)), + C.sizeof_cm_memory_range_config)) + for i, v := range *flashDrive { + offset := C.sizeof_cm_memory_range_config * i + addr := unsafe.Pointer(uintptr(unsafe.Pointer(cFlashDrive.entry)) + uintptr(offset)) + mr := (*C.cm_memory_range_config)(addr) + mr.start = (C.uint64_t)(v.Start) + mr.length = (C.uint64_t)(v.Length) + mr.shared = (C.bool)(v.Shared) + mr.image_filename = makeCString(&v.ImageFilename) + } + + cTlb := &ref.cref.tlb + tlb := &config.Tlb + cTlb.image_filename = makeCString(&tlb.ImageFilename) + + cClint := &ref.cref.clint + clint := &config.Clint + cClint.mtimecmp = (C.uint64_t)(clint.Mtimecmp) + + cHtif := &ref.cref.htif + htif := &config.Htif + cHtif.tohost = (C.uint64_t)(htif.Tohost) + cHtif.fromhost = (C.uint64_t)(htif.Fromhost) + cHtif.console_getchar = (C.bool)(htif.ConsoleGetchar) + cHtif.yield_manual = (C.bool)(htif.YieldManual) + cHtif.yield_automatic = (C.bool)(htif.YieldAutomatic) + + cCmio := &ref.cref.cmio + cmio := &config.Cmio + cCmio.rx_buffer.shared = (C.bool)(cmio.RxBuffer.Shared) + cCmio.rx_buffer.image_filename = makeCString(&cmio.RxBuffer.ImageFilename) + cCmio.tx_buffer.shared = (C.bool)(cmio.TxBuffer.Shared) + cCmio.tx_buffer.image_filename = makeCString(&cmio.TxBuffer.ImageFilename) + + cUarch := &ref.cref.uarch + uarch := &config.Uarch + + cUarchProcessor := &cUarch.processor + uarchProcessor := &uarch.Processor + for i := 0; i < 32; i++ { + cUarchProcessor.x[i] = (C.uint64_t)(uarchProcessor.X[i]) + } + cUarchProcessor.pc = (C.uint64_t)(uarchProcessor.Pc) + cUarchProcessor.cycle = (C.uint64_t)(uarchProcessor.Cycle) + cUarchProcessor.halt_flag = (C.bool)(uarchProcessor.HaltFlag) + + cUarchRam := &cUarch.ram + uarchRam := &uarch.Ram + cUarchRam.image_filename = makeCString(&uarchRam.ImageFilename) + + return ref +} + +func (configCRef *ourMachineConfigCRef) free() { + if configCRef == nil || configCRef.cref == nil { + return + } + C.free(unsafe.Pointer(configCRef.cref.ram.image_filename)) + C.free(unsafe.Pointer(configCRef.cref.dtb.bootargs)) + C.free(unsafe.Pointer(configCRef.cref.dtb.init)) + C.free(unsafe.Pointer(configCRef.cref.dtb.entrypoint)) + C.free(unsafe.Pointer(configCRef.cref.dtb.image_filename)) + C.free(unsafe.Pointer(configCRef.cref.flash_drive.entry)) + + C.free(unsafe.Pointer(configCRef.cref.cmio.rx_buffer.image_filename)) + C.free(unsafe.Pointer(configCRef.cref.cmio.tx_buffer.image_filename)) + C.free(unsafe.Pointer(configCRef.cref.uarch.ram.image_filename)) + C.free(unsafe.Pointer(configCRef.cref)) + configCRef.cref = nil +} + +// cm_machine_config allocated by the emulator +type theirMachineConfigCRef struct { + cref *C.cm_machine_config +} + +func (configCRef *theirMachineConfigCRef) free() { + if configCRef != nil && configCRef.cref != nil { + C.cm_delete_machine_config(configCRef.cref) + configCRef.cref = nil + } +} + +func (configCRef *theirMachineConfigCRef) makeGoRef() (cfg *MachineConfig) { + cfg = &MachineConfig{} + c := configCRef.cref + // Processor + processor := &cfg.Processor + for i := 0; i < 30; i++ { + processor.X[i] = (uint64)(c.processor.x[i+1]) + } + for i := 0; i < 31; i++ { + processor.F[i] = (uint64)(c.processor.f[i+1]) + } + processor.Pc = (uint64)(c.processor.pc) + processor.Fcsr = (uint64)(c.processor.fcsr) + processor.Mvendorid = (uint64)(c.processor.mvendorid) + processor.Marchid = (uint64)(c.processor.marchid) + processor.Mimpid = (uint64)(c.processor.mimpid) + processor.Mcycle = (uint64)(c.processor.mcycle) + processor.Icycleinstret = (uint64)(c.processor.icycleinstret) + processor.Mstatus = (uint64)(c.processor.mstatus) + processor.Mtvec = (uint64)(c.processor.mtvec) + processor.Mscratch = (uint64)(c.processor.mscratch) + processor.Mepc = (uint64)(c.processor.mepc) + processor.Mcause = (uint64)(c.processor.mcause) + processor.Mtval = (uint64)(c.processor.mtval) + processor.Misa = (uint64)(c.processor.misa) + processor.Mie = (uint64)(c.processor.mie) + processor.Mip = (uint64)(c.processor.mip) + processor.Medeleg = (uint64)(c.processor.medeleg) + processor.Mideleg = (uint64)(c.processor.mideleg) + processor.Mcounteren = (uint64)(c.processor.mcounteren) + processor.Menvcfg = (uint64)(c.processor.menvcfg) + processor.Stvec = (uint64)(c.processor.stvec) + processor.Sscratch = (uint64)(c.processor.sscratch) + processor.Sepc = (uint64)(c.processor.sepc) + processor.Scause = (uint64)(c.processor.scause) + processor.Stval = (uint64)(c.processor.stval) + processor.Satp = (uint64)(c.processor.satp) + processor.Scounteren = (uint64)(c.processor.scounteren) + processor.Senvcfg = (uint64)(c.processor.senvcfg) + processor.Ilrsc = (uint64)(c.processor.ilrsc) + processor.Iflags = (uint64)(c.processor.iflags) + + // Ram + ram := &cfg.Ram + ram.Length = (uint64)(c.ram.length) + ram.ImageFilename = C.GoString(c.ram.image_filename) + + // Dtb + dtb := &cfg.Dtb + dtb.Bootargs = C.GoString(c.dtb.bootargs) + dtb.Init = C.GoString(c.dtb.init) + dtb.Entrypoint = C.GoString(c.dtb.entrypoint) + dtb.ImageFilename = C.GoString(c.dtb.image_filename) + + // FlashDrive + //flashDrive := &cfg.FlashDrive + for i := 0; i < int(c.flash_drive.count); i++ { + offset := C.sizeof_cm_memory_range_config * i + addr := unsafe.Pointer(uintptr(unsafe.Pointer(c.flash_drive.entry)) + uintptr(offset)) + mr := (*C.cm_memory_range_config)(addr) + cfg.FlashDrive = append(cfg.FlashDrive, MemoryRangeConfig{ + Start: (uint64)(mr.start), + Length: (uint64)(mr.length), + Shared: (bool)(mr.shared), + ImageFilename: C.GoString(mr.image_filename), + }) + } + + // Tlb + tlb := &cfg.Tlb + tlb.ImageFilename = C.GoString(c.tlb.image_filename) + + // Clint + clint := &cfg.Clint + clint.Mtimecmp = (uint64)(c.clint.mtimecmp) + + // Htif + htif := &cfg.Htif + htif.Tohost = (uint64)(c.htif.tohost) + htif.Fromhost = (uint64)(c.htif.fromhost) + htif.ConsoleGetchar = (bool)(c.htif.console_getchar) + htif.YieldManual = (bool)(c.htif.yield_manual) + htif.YieldAutomatic = (bool)(c.htif.yield_automatic) + + // CMIO + cmio := &cfg.Cmio + cmio.RxBuffer = CmioBufferConfig{ + Shared: (bool)(c.cmio.rx_buffer.shared), + ImageFilename: C.GoString(c.cmio.rx_buffer.image_filename), + } + cmio.TxBuffer = CmioBufferConfig{ + Shared: (bool)(c.cmio.tx_buffer.shared), + ImageFilename: C.GoString(c.cmio.tx_buffer.image_filename), + } + + // Uarch + uarch := &cfg.Uarch + uarchProcessor := &uarch.Processor + for i := 0; i < 32; i++ { + uarchProcessor.X[i] = (uint64)(c.uarch.processor.x[i]) + } + uarchProcessor.Pc = (uint64)(c.uarch.processor.pc) + uarchProcessor.Cycle = (uint64)(c.uarch.processor.cycle) + uarchProcessor.HaltFlag = (bool)(c.uarch.processor.halt_flag) + + uarchRam := &uarch.Ram + uarchRam.ImageFilename = C.GoString(c.uarch.ram.image_filename) + + return cfg +} + +func makeCString(s *string) *C.char { + if s == nil || *s == "" { + return nil + } + return C.CString(*s) +} diff --git a/pkg/emulator/htif.go b/pkg/emulator/htif.go new file mode 100644 index 000000000..f6162f8be --- /dev/null +++ b/pkg/emulator/htif.go @@ -0,0 +1,33 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package emulator + +// #include "cartesi-machine/htif-defines.h" +import "C" + +type ( + HtifYieldType uint8 + HtifYieldReason uint8 +) + +const ( + // type + YieldAutomatic = C.HTIF_YIELD_CMD_AUTOMATIC_DEF + YieldManual = C.HTIF_YIELD_CMD_MANUAL_DEF + + // NOTE: these do not form an enum (e.g., automatic-progress == manual-accepted). + + // reason - request + AutomaticYieldReasonProgress HtifYieldReason = C.HTIF_YIELD_AUTOMATIC_REASON_PROGRESS_DEF + AutomaticYieldReasonOutput HtifYieldReason = C.HTIF_YIELD_AUTOMATIC_REASON_TX_OUTPUT_DEF + AutomaticYieldReasonReport HtifYieldReason = C.HTIF_YIELD_AUTOMATIC_REASON_TX_REPORT_DEF + + ManualYieldReasonAccepted HtifYieldReason = C.HTIF_YIELD_MANUAL_REASON_RX_ACCEPTED_DEF + ManualYieldReasonRejected HtifYieldReason = C.HTIF_YIELD_MANUAL_REASON_RX_REJECTED_DEF + ManualYieldReasonException HtifYieldReason = C.HTIF_YIELD_MANUAL_REASON_TX_EXCEPTION_DEF + + // reason - reply + YieldReasonAdvanceState HtifYieldReason = C.HTIF_YIELD_REASON_ADVANCE_STATE_DEF + YieldReasonInspectState HtifYieldReason = C.HTIF_YIELD_REASON_INSPECT_STATE_DEF +) diff --git a/pkg/emulator/machine.go b/pkg/emulator/machine.go new file mode 100644 index 000000000..430c3524b --- /dev/null +++ b/pkg/emulator/machine.go @@ -0,0 +1,244 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package emulator + +// #include +// #include "cartesi-machine/jsonrpc-machine-c-api.h" +import "C" +import "unsafe" + +// A local or remote machine. +type Machine struct { + c *C.cm_machine + remote *RemoteMachineManager +} + +func (machine *Machine) GetInitialConfig() (*MachineConfig, error) { + var msg *C.char + theirCfg := theirMachineConfigCRef{} + defer theirCfg.free() + code := C.cm_get_initial_config(machine.c, &theirCfg.cref, &msg) + if err := newError(code, msg); err != nil { + return nil, err + } + return theirCfg.makeGoRef(), nil +} + +func NewMachine(config *MachineConfig, runtime *MachineRuntimeConfig) (*Machine, error) { + machine := &Machine{} + configRef := config.makeCRef() + defer configRef.free() + runtimeRef := runtime.makeCRef() + defer runtimeRef.free() + var msg *C.char + code := C.cm_create_machine(configRef.cref, runtimeRef.cref, &machine.c, &msg) + return machine, newError(code, msg) +} + +func LoadMachine(dir string, runtime *MachineRuntimeConfig) (*Machine, error) { + machine := &Machine{} + cDir := C.CString(dir) + defer C.free(unsafe.Pointer(cDir)) + runtimeRef := runtime.makeCRef() + defer runtimeRef.free() + var msg *C.char + code := C.cm_load_machine(cDir, runtimeRef.cref, &machine.c, &msg) + return machine, newError(code, msg) +} + +func (machine *Machine) Store(dir string) error { + cDir := C.CString(dir) + defer C.free(unsafe.Pointer(cDir)) + var msg *C.char + code := C.cm_store(machine.c, cDir, &msg) + return newError(code, msg) +} + +func (machine *Machine) Delete() { + if machine.c != nil { + C.cm_delete_machine(machine.c) + machine.c = nil + } +} + +func (machine *Machine) Destroy() error { + var msg *C.char + code := C.cm_destroy(machine.c, &msg) + return newError(code, msg) +} + +func (machine *Machine) Snapshot() error { + var msg *C.char + code := C.cm_snapshot(machine.c, &msg) + return newError(code, msg) +} + +func (machine *Machine) Rollback() error { + var msg *C.char + code := C.cm_rollback(machine.c, &msg) + return newError(code, msg) +} + +func (machine *Machine) Run(mcycleEnd uint64) (BreakReason, error) { + var msg *C.char + var reason C.CM_BREAK_REASON + code := C.cm_machine_run(machine.c, C.uint64_t(mcycleEnd), &reason, &msg) + if err := newError(code, msg); err != nil { + return BreakReasonFailed, err + } + return (BreakReason)(reason), nil +} + +func (machine *Machine) GetRootHash() (hash MerkleTreeHash, _ error) { + var msg *C.char + var chash C.cm_hash + code := C.cm_get_root_hash(machine.c, &chash, &msg) + if err := newError(code, msg); err != nil { + return hash, err + } + + for i := 0; i < 32; i++ { + hash[i] = byte(chash[i]) + } + return hash, nil +} + +func (machine Machine) ReadMCycle() (uint64, error) { + var msg *C.char + var value C.uint64_t + code := C.cm_read_mcycle(machine.c, &value, &msg) + return uint64(value), newError(code, msg) +} + +func (machine *Machine) ReplaceMemoryRange(newRange *MemoryRangeConfig) error { + var msg *C.char + newRangeRef := newRange.makeCRef() + defer newRangeRef.free() + code := C.cm_replace_memory_range(machine.c, newRangeRef.cref, &msg) + return newError(code, msg) +} + +func (machine *Machine) ReadMemory(address, length uint64) ([]byte, error) { + var msg *C.char + data := make([]byte, length) + code := C.cm_read_memory(machine.c, + C.uint64_t(address), + (*C.uchar)(unsafe.Pointer(&data[0])), + C.uint64_t(length), + &msg) + return data, newError(code, msg) +} + +func (machine *Machine) WriteMemory(address uint64, data []byte) error { + var msg *C.char + code := C.cm_write_memory(machine.c, + C.uint64_t(address), + (*C.uchar)(unsafe.Pointer(&data[0])), + C.size_t(len(data)), + &msg) + return newError(code, msg) +} + +func (machine *Machine) ReadCSR(r ProcessorCSR) (uint64, error) { + var msg *C.char + var value C.uint64_t + code := C.cm_read_csr(machine.c, C.CM_PROC_CSR(r), &value, &msg) + return uint64(value), newError(code, msg) +} + +func (machine *Machine) WriteCSR(r ProcessorCSR, value uint64) error { + var msg *C.char + code := C.cm_write_csr(machine.c, C.CM_PROC_CSR(r), C.uint64_t(value), &msg) + return newError(code, msg) +} + +func (machine *Machine) ReadX(i int) (uint64, error) { + var msg *C.char + var value C.uint64_t + code := C.cm_read_x(machine.c, C.int(i), &value, &msg) + return uint64(value), newError(code, msg) +} + +func (machine *Machine) WriteX(i int, value uint64) error { + var msg *C.char + code := C.cm_write_x(machine.c, C.int(i), C.uint64_t(value), &msg) + return newError(code, msg) +} + +func (machine *Machine) ReadF(i int) (uint64, error) { + var msg *C.char + var value C.uint64_t + code := C.cm_read_f(machine.c, C.int(i), &value, &msg) + return uint64(value), newError(code, msg) +} + +func (machine *Machine) WriteF(i int, value uint64) error { + var msg *C.char + code := C.cm_write_f(machine.c, C.int(i), C.uint64_t(value), &msg) + return newError(code, msg) +} + +func (machine *Machine) ReadIFlagsX() (bool, error) { + var msg *C.char + var value C.bool + code := C.cm_read_iflags_X(machine.c, &value, &msg) + return bool(value), newError(code, msg) +} + +func (machine *Machine) ResetIFlagsX() error { + var msg *C.char + code := C.cm_reset_iflags_X(machine.c, &msg) + return newError(code, msg) +} + +func (machine *Machine) SetIFlagsX() error { + var msg *C.char + code := C.cm_set_iflags_X(machine.c, &msg) + return newError(code, msg) +} + +func (machine *Machine) ReadIFlagsY() (bool, error) { + var msg *C.char + var value C.bool + code := C.cm_read_iflags_Y(machine.c, &value, &msg) + return bool(value), newError(code, msg) +} + +func (machine *Machine) ResetIFlagsY() error { + var msg *C.char + code := C.cm_reset_iflags_Y(machine.c, &msg) + return newError(code, msg) +} + +func (machine *Machine) SetIFlagsY() error { + var msg *C.char + code := C.cm_set_iflags_Y(machine.c, &msg) + return newError(code, msg) +} + +func (machine *Machine) ReadIFlagsH() (bool, error) { + var msg *C.char + var value C.bool + code := C.cm_read_iflags_H(machine.c, &value, &msg) + return bool(value), newError(code, msg) +} + +func (machine *Machine) SetIFlagsH() error { + var msg *C.char + code := C.cm_set_iflags_H(machine.c, &msg) + return newError(code, msg) +} + +func (machine *Machine) ReadHtifToHostData() (uint64, error) { + var msg *C.char + var value C.uint64_t + code := C.cm_read_htif_tohost_data(machine.c, &value, &msg) + return uint64(value), newError(code, msg) +} + +func (machine *Machine) WriteHtifFromHostData(value uint64) error { + var msg *C.char + code := C.cm_write_htif_fromhost_data(machine.c, C.uint64_t(value), &msg) + return newError(code, msg) +} diff --git a/pkg/emulator/pma.go b/pkg/emulator/pma.go new file mode 100644 index 000000000..36c9205f7 --- /dev/null +++ b/pkg/emulator/pma.go @@ -0,0 +1,12 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package emulator + +// #include "cartesi-machine/pma-defines.h" +import "C" + +const ( + CmioRxBufferStart uint64 = C.PMA_CMIO_RX_BUFFER_START_DEF + CmioTxBufferStart uint64 = C.PMA_CMIO_TX_BUFFER_START_DEF +) diff --git a/pkg/emulator/remote.go b/pkg/emulator/remote.go new file mode 100644 index 000000000..024e180a8 --- /dev/null +++ b/pkg/emulator/remote.go @@ -0,0 +1,94 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package emulator + +// #include +// #include "cartesi-machine/jsonrpc-machine-c-api.h" +import "C" +import ( + "unsafe" +) + +// A connection to the remote jsonrpc machine manager. +type RemoteMachineManager struct { + c *C.cm_jsonrpc_mg_mgr + + Address string +} + +func NewRemoteMachineManager(address string) (*RemoteMachineManager, error) { + manager := &RemoteMachineManager{Address: address} + 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) + return manager, newError(code, msg) +} + +func (remote *RemoteMachineManager) Delete() { + if remote.c != nil { + C.cm_delete_jsonrpc_mg_mgr(remote.c) + remote.c = nil + } +} + +func (remote *RemoteMachineManager) NewMachine( + config *MachineConfig, + runtime *MachineRuntimeConfig, +) (*Machine, error) { + var msg *C.char + machine := &Machine{remote: remote} + configRef := config.makeCRef() + defer configRef.free() + runtimeRef := runtime.makeCRef() + defer runtimeRef.free() + code := C.cm_create_jsonrpc_machine(remote.c, configRef.cref, runtimeRef.cref, &machine.c, &msg) + return machine, newError(code, msg) +} + +func (remote *RemoteMachineManager) LoadMachine( + directory string, + runtime *MachineRuntimeConfig, +) (*Machine, error) { + var msg *C.char + machine := &Machine{remote: remote} + dir := C.CString(directory) + defer C.free(unsafe.Pointer(dir)) + runtimeRef := runtime.makeCRef() + defer runtimeRef.free() + code := C.cm_load_jsonrpc_machine(remote.c, dir, runtimeRef.cref, &machine.c, &msg) + return machine, newError(code, msg) +} + +func (remote *RemoteMachineManager) GetMachine() (*Machine, error) { + var msg *C.char + machine := &Machine{remote: remote} + code := C.cm_get_jsonrpc_machine(remote.c, &machine.c, &msg) + return machine, newError(code, msg) +} + +func (remote *RemoteMachineManager) GetDefaultMachineConfig() (*MachineConfig, error) { + var msg *C.char + theirCfg := theirMachineConfigCRef{} + defer theirCfg.free() + code := C.cm_jsonrpc_get_default_config(remote.c, &theirCfg.cref, &msg) + if err := newError(code, msg); err != nil { + return nil, err + } + return theirCfg.makeGoRef(), nil +} + +func (remote *RemoteMachineManager) Fork() (newAddress string, _ error) { + var msg *C.char + var address *C.char + defer C.cm_delete_cstring(address) + code := C.cm_jsonrpc_fork(remote.c, &address, &msg) + return C.GoString(address), newError(code, msg) +} + +func (remote *RemoteMachineManager) Shutdown() error { + var msg *C.char + code := C.cm_jsonrpc_shutdown(remote.c, &msg) + return newError(code, msg) +} diff --git a/pkg/gollup/gollup.go b/pkg/gollup/gollup.go new file mode 100644 index 000000000..dcc6c7770 --- /dev/null +++ b/pkg/gollup/gollup.go @@ -0,0 +1,87 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package gollup + +import "github.com/cartesi/rollups-node/pkg/libcmt" + +type OutputEmitter interface { + SendVoucher(address [20]byte, value []byte, data []byte) (uint64, error) + SendNotice(data []byte) (uint64, error) + SendReport(data []byte) error + RaiseException(data []byte) error +} + +type ReportEmitter interface { + SendReport(data []byte) error + RaiseException(data []byte) error +} + +// ------------------------------------------------------------------------------------------------ + +type AdvanceHandler func(OutputEmitter, *libcmt.Input) bool + +type InspectHandler func(ReportEmitter, *libcmt.Query) bool + +type Gollup struct { + rollup *libcmt.Rollup + advanceHandler AdvanceHandler + inspectHandler InspectHandler +} + +func New(advanceHandler AdvanceHandler, inspectHandler InspectHandler) (*Gollup, error) { + rollup, err := libcmt.NewRollup() + if err != nil { + return nil, err + } + return &Gollup{rollup, advanceHandler, inspectHandler}, nil +} + +func (gollup *Gollup) Destroy() { + gollup.rollup.Destroy() +} + +func (gollup *Gollup) Run() error { + accept := true + for { + finish, err := gollup.rollup.Finish(accept) + if err != nil { + return err + } + + switch finish.NextRequestType { + case libcmt.AdvanceStateRequest: + input, err := gollup.rollup.ReadAdvanceState() + if err != nil { + return err + } + accept = gollup.advanceHandler(gollup, input) + case libcmt.InspectStateRequest: + query, err := gollup.rollup.ReadInspectState() + if err != nil { + return err + } + accept = gollup.inspectHandler(gollup, query) + default: + panic("unreachable") + } + } +} + +// ------------------------------------------------------------------------------------------------ + +func (gollup *Gollup) SendVoucher(address [20]byte, value []byte, data []byte) (uint64, error) { + return gollup.rollup.EmitVoucher(address, value, data) +} + +func (gollup *Gollup) SendNotice(data []byte) (uint64, error) { + return gollup.rollup.EmitNotice(data) +} + +func (gollup *Gollup) SendReport(data []byte) error { + return gollup.rollup.EmitReport(data) +} + +func (gollup *Gollup) RaiseException(data []byte) error { + return gollup.rollup.EmitException(data) +} diff --git a/pkg/libcmt/libcmt.go b/pkg/libcmt/libcmt.go new file mode 100644 index 000000000..3d7c3fce9 --- /dev/null +++ b/pkg/libcmt/libcmt.go @@ -0,0 +1,178 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package libcmt + +// #cgo LDFLAGS: -lcmt +// #include +// #include +// #include "libcmt/rollup.h" +// #include "libcmt/io.h" +import "C" +import ( + "fmt" + "unsafe" +) + +type RequestType = uint8 + +const ( + AdvanceStateRequest RequestType = C.HTIF_YIELD_REASON_ADVANCE + InspectStateRequest RequestType = C.HTIF_YIELD_REASON_INSPECT +) + +type Finish struct { + AcceptPreviousRequest bool + NextRequestType RequestType + NextRequestPayloadLength uint32 +} + +type Input struct { + ChainId uint64 + AppContract [20]byte + Sender [20]byte + BlockNumber uint64 + BlockTimestamp uint64 + Index uint64 + Data []byte +} + +type Query struct { + Data []byte +} + +// ------------------------------------------------------------------------------------------------ + +type Rollup struct { + inner *C.cmt_rollup_t +} + +func NewRollup() (*Rollup, error) { + var rollup C.cmt_rollup_t + errno := C.cmt_rollup_init(&rollup) + return &Rollup{inner: &rollup}, toError(errno) +} + +func (rollup *Rollup) Destroy() { + C.cmt_rollup_fini(rollup.inner) +} + +func (rollup *Rollup) Finish(accept bool) (*Finish, error) { + finish := C.cmt_rollup_finish_t{accept_previous_request: C.bool(accept)} + errno := C.cmt_rollup_finish(rollup.inner, &finish) + if err := toError(errno); err != nil { + return nil, err + } + + return &Finish{ + AcceptPreviousRequest: bool(finish.accept_previous_request), + NextRequestType: RequestType(finish.next_request_type), + NextRequestPayloadLength: uint32(finish.next_request_payload_length), + }, nil +} + +// Returns the index. +func (rollup *Rollup) EmitVoucher(address [20]byte, value []byte, voucher []byte) (uint64, error) { + addressLength, addressData := C.uint(20), C.CBytes(address[:]) + defer C.free(addressData) + + valueLength, valueData := C.uint(len(value)), C.CBytes(value) + defer C.free(valueData) + + voucherLength, voucherData := C.uint(len(voucher)), C.CBytes(voucher) + defer C.free(voucherData) + + var index C.uint64_t + err := toError(C.cmt_rollup_emit_voucher(rollup.inner, + addressLength, addressData, + valueLength, valueData, + voucherLength, voucherData, + &index, + )) + + return uint64(index), err +} + +func (rollup *Rollup) EmitNotice(notice []byte) (uint64, error) { + length, data := C.uint(len(notice)), C.CBytes(notice) + defer C.free(data) + var index C.uint64_t + err := toError(C.cmt_rollup_emit_notice(rollup.inner, length, data, &index)) + return uint64(index), err +} + +func (rollup *Rollup) EmitReport(report []byte) error { + length, data := C.uint(len(report)), C.CBytes(report) + defer C.free(data) + return toError(C.cmt_rollup_emit_report(rollup.inner, length, data)) +} + +func (rollup *Rollup) EmitException(exception []byte) error { + length, data := C.uint(len(exception)), C.CBytes(exception) + defer C.free(data) + return toError(C.cmt_rollup_emit_exception(rollup.inner, length, data)) +} + +func (rollup *Rollup) ReadAdvanceState() (*Input, error) { + var advance C.cmt_rollup_advance_t + errno := C.cmt_rollup_read_advance_state(rollup.inner, &advance) + if err := toError(errno); err != nil { + return nil, err + } + // TODO: should I free inner.data? + + var appContract [20]byte + for i, v := range advance.app_contract { + appContract[i] = byte(v) + } + + var sender [20]byte + for i, v := range advance.msg_sender { + sender[i] = byte(v) + } + + return &Input{ + ChainId: uint64(advance.chain_id), + AppContract: [20]byte{}, // TODO + Sender: sender, + BlockNumber: uint64(advance.block_number), + BlockTimestamp: uint64(advance.block_timestamp), + Index: uint64(advance.index), + Data: C.GoBytes(advance.payload, C.int(advance.payload_length)), + }, nil +} + +func (rollup *Rollup) ReadInspectState() (*Query, error) { + var query C.cmt_rollup_inspect_t + errno := C.cmt_rollup_read_inspect_state(rollup.inner, &query) + if err := toError(errno); err != nil { + return nil, err + } + // TODO: should I free query.data? + + return &Query{Data: C.GoBytes(query.payload, C.int(query.payload_length))}, nil +} + +func (rollup *Rollup) LoadMerkle(path string) error { + s := C.CString(path) + defer C.free(unsafe.Pointer(s)) + return toError(C.cmt_rollup_load_merkle(rollup.inner, s)) +} + +func (rollup *Rollup) SaveMerkle(path string) error { + s := C.CString(path) + defer C.free(unsafe.Pointer(s)) + return toError(C.cmt_rollup_save_merkle(rollup.inner, s)) +} + +// ------------------------------------------------------------------------------------------------ + +func toError(errno C.int) error { + if errno < 0 { + s := C.strerror(-errno) + defer C.free(unsafe.Pointer(s)) + return fmt.Errorf("%s (%d)", C.GoString(s), errno) + } else { + return nil + } +} 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 +}