From deeff574cec1f66890cef74572b148da70696c3a Mon Sep 17 00:00:00 2001 From: Renan Santos Date: Tue, 21 May 2024 18:08:21 -0300 Subject: [PATCH] feat: add tooling for tests (packages libcmt, gollup, and snapshot) --- .golangci.yml | 4 + build/Dockerfile | 5 +- test/gollup/gollup.go | 102 ++++++++++++++++ test/libcmt/libcmt.go | 183 ++++++++++++++++++++++++++++ test/libcmt/test/libcmt_test.go | 136 +++++++++++++++++++++ test/snapshot/snapshot.go | 207 ++++++++++++++++++++++++++++++++ 6 files changed, 634 insertions(+), 3 deletions(-) create mode 100644 test/gollup/gollup.go create mode 100644 test/libcmt/libcmt.go create mode 100644 test/libcmt/test/libcmt_test.go create mode 100644 test/snapshot/snapshot.go diff --git a/.golangci.yml b/.golangci.yml index 07203d296..3f3673764 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -14,3 +14,7 @@ linters-settings: mnd: ignored-functions: - "^make" +issues: + exclude-files: + - test/libcmt/libcmt.go + - test/gollup/gollup.go diff --git a/build/Dockerfile b/build/Dockerfile index f696b582e..0488c82a5 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -107,6 +107,7 @@ EOF # STAGE: emulator-devel # # - Install libarchive13 (setup -- required by xgenext2fs). +# - Install gcc-12-riscv64-linux-gnu (setup -- required to compile libcmt applications). # - Install libcmt. # - Install xgenext2fs. # ============================================================================= @@ -122,6 +123,7 @@ RUN < +// #include "libcmt/abi.h" +// #include "libcmt/io.h" +// #include "libcmt/rollup.h" +import "C" +import ( + "errors" + "fmt" +) + +type ( + RequestType uint8 + Address = [20]byte +) + +var ( + ErrClosed = errors.New("rollup already closed") + + ErrFinish = errors.New("failed to finish") + ErrReadAdvanceState = errors.New("failed to read the advance's state") + ErrReadInspectState = errors.New("failed to read the inspect's state") + ErrEmitVoucher = errors.New("failed to emit voucher") + ErrEmitNotice = errors.New("failed to emit notice") + ErrEmitReport = errors.New("failed to emit report") + ErrEmitException = errors.New("failed to emit exception") +) + +const ( + AdvanceState RequestType = C.HTIF_YIELD_REASON_ADVANCE + InspectState RequestType = C.HTIF_YIELD_REASON_INSPECT +) + +type Input struct { + ChainId uint64 + AppContract Address + Sender Address + BlockNumber uint64 + BlockTimestamp uint64 + Index uint64 + Data []byte +} + +type Query struct { + Data []byte +} + +type Rollup struct{ c *C.cmt_rollup_t } + +// NewRollup returns a new [Rollup]. +func NewRollup() (*Rollup, error) { + var c C.cmt_rollup_t + errno := C.cmt_rollup_init(&c) + rollup := &Rollup{c: &c} + return rollup, toError(errno) +} + +// Close closes the rollup, rendering it unusable. +// Close will return an error if it has already been called. +func (rollup *Rollup) Close() error { + if rollup.c == nil { + return ErrClosed + } + C.cmt_rollup_fini(rollup.c) + rollup.c = nil + return nil +} + +// Finish accepts or rejects the previous advance/inspect request. +// It then waits for the next request and returns its type. +func (rollup *Rollup) Finish(accept bool) (RequestType, error) { + finish := C.cmt_rollup_finish_t{accept_previous_request: C.bool(accept)} + errno := C.cmt_rollup_finish(rollup.c, &finish) + if err := toError(errno); err != nil { + return 0, fmt.Errorf("%w: %w", ErrFinish, err) + } + return RequestType(finish.next_request_type), nil +} + +// ReadAdvanceState returns the [Input] from an advance-state request. +func (rollup *Rollup) ReadAdvanceState() (Input, error) { + var advance C.cmt_rollup_advance_t + errno := C.cmt_rollup_read_advance_state(rollup.c, &advance) + if err := toError(errno); err != nil { + return Input{}, fmt.Errorf("%w: %w", ErrReadAdvanceState, err) + } + return Input{ + ChainId: uint64(advance.chain_id), + AppContract: toAddress(advance.app_contract), + Sender: toAddress(advance.msg_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 +} + +// ReadInspectState returns the [Query] from an inspect-state request. +func (rollup *Rollup) ReadInspectState() (Query, error) { + var inspect C.cmt_rollup_inspect_t + errno := C.cmt_rollup_read_inspect_state(rollup.c, &inspect) + if err := toError(errno); err != nil { + return Query{}, fmt.Errorf("%w: %w", ErrReadInspectState, err) + } + return Query{Data: C.GoBytes(inspect.payload, C.int(inspect.payload_length))}, nil +} + +// EmitVoucher emits a voucher and returns its index. +func (rollup *Rollup) EmitVoucher(address Address, value []byte, voucher []byte) (uint64, error) { + addressData := 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.c, + C.CMT_ADDRESS_LENGTH, addressData, + valueLength, valueData, + voucherLength, voucherData, + &index, + )) + + return uint64(index), fmt.Errorf("%w: %w", ErrEmitVoucher, err) +} + +// EmitNotice emits a notice and returns its index. +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.c, length, data, &index)) + return uint64(index), fmt.Errorf("%w: %w", ErrEmitNotice, err) +} + +// EmitReport emits a report. +func (rollup *Rollup) EmitReport(report []byte) error { + length, data := C.uint(len(report)), C.CBytes(report) + // defer C.free(data) + err := toError(C.cmt_rollup_emit_report(rollup.c, length, data)) + return fmt.Errorf("%w: %w", ErrEmitReport, err) + +} + +// EmitException emits an exception. +func (rollup *Rollup) EmitException(exception []byte) error { + length, data := C.uint(len(exception)), C.CBytes(exception) + // defer C.free(data) + err := toError(C.cmt_rollup_emit_exception(rollup.c, length, data)) + return fmt.Errorf("%w: %w", ErrEmitException, err) +} + +// ------------------------------------------------------------------------------------------------ + +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 + } +} + +func toAddress(c [C.CMT_ADDRESS_LENGTH]C.uint8_t) Address { + var address Address + for i, v := range c { + address[i] = byte(v) + } + return address +} diff --git a/test/libcmt/test/libcmt_test.go b/test/libcmt/test/libcmt_test.go new file mode 100644 index 000000000..223d1093e --- /dev/null +++ b/test/libcmt/test/libcmt_test.go @@ -0,0 +1,136 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package test + +import ( + "fmt" + "log" + "log/slog" + "testing" + + "github.com/cartesi/rollups-node/pkg/emulator" + "github.com/cartesi/rollups-node/test/snapshot" + + "github.com/stretchr/testify/suite" +) + +// Basic smoke tests to check if the binding is implemented correctly. +func TestLibcmt(t *testing.T) { + suite.Run(t, new(LibcmtSuite)) +} + +type LibcmtSuite struct{ suite.Suite } + +func (suite *LibcmtSuite) SetupSuite() { + log.SetFlags(log.Ltime) + slog.SetLogLoggerLevel(slog.LevelDebug) +} + +func (suite *LibcmtSuite) test(code string, breakReason emulator.BreakReason) { + header := ` + package main + + import "github.com/cartesi/rollups-node/test/libcmt" + ` + code = header + code + require := suite.Require() + snapshot, err := snapshot.FromGoCode(1_000_000_000, code) + require.Nil(err) + defer func() { require.Nil(snapshot.Close()) }() + require.Equal(breakReason, snapshot.BreakReason, + "expected %s -- actual %s", breakReason, snapshot.BreakReason) +} + +func (suite *LibcmtSuite) TestNewRollup() { + code := ` + func main() { + _, err := libcmt.NewRollup() + if err != nil { for{} } + } + ` + suite.test(code, emulator.BreakReasonHalted) +} + +func (suite *LibcmtSuite) TestClose() { + code := ` + import "errors" + func main() { + rollup, err := libcmt.NewRollup() + if err != nil { for{} } + err = rollup.Close() + if err != nil { for{} } + err = rollup.Close() + if !errors.Is(err, libcmt.ErrClosed) { for{} } + } + ` + suite.test(code, emulator.BreakReasonHalted) +} + +func (suite *LibcmtSuite) TestFinish() { + code := ` + func main() { + rollup, err := libcmt.NewRollup() + if err != nil { for{} } + _, _ = rollup.Finish(%s) + for {} + } + ` + suite.Run("True", func() { + code := fmt.Sprintf(code, "true") + suite.test(code, emulator.BreakReasonYieldedManually) + }) + + suite.Run("False", func() { + code := fmt.Sprintf(code, "false") + suite.test(code, emulator.BreakReasonYieldedManually) + }) +} + +func (suite *LibcmtSuite) TestEmitVoucher() { + code := ` + func main() { + rollup, err := libcmt.NewRollup() + if err != nil { for{} } + _, _ = rollup.EmitVoucher(libcmt.Address{}, []byte{}, []byte("Whiplash")) + for {} + } + ` + suite.test(code, emulator.BreakReasonYieldedAutomatically) +} + +func (suite *LibcmtSuite) TestEmitNotice() { + code := ` + func main() { + rollup, err := libcmt.NewRollup() + if err != nil { for{} } + _, _ = rollup.EmitNotice([]byte("Past Lives")) + for {} + } + ` + suite.test(code, emulator.BreakReasonYieldedAutomatically) +} + +func (suite *LibcmtSuite) TestEmitReport() { + code := ` + func main() { + rollup, err := libcmt.NewRollup() + if err != nil { for{} } + _ = rollup.EmitReport([]byte("Challengers")) + for {} + } + ` + suite.test(code, emulator.BreakReasonYieldedAutomatically) +} + +func (suite *LibcmtSuite) TestEmitException() { + code := ` + func main() { + rollup, err := libcmt.NewRollup() + if err != nil { for{} } + _ = rollup.EmitException([]byte("Monster")) + for {} + } + ` + suite.test(code, emulator.BreakReasonYieldedManually) +} diff --git a/test/snapshot/snapshot.go b/test/snapshot/snapshot.go new file mode 100644 index 000000000..4898c4626 --- /dev/null +++ b/test/snapshot/snapshot.go @@ -0,0 +1,207 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package snapshot + +import ( + "errors" + "fmt" + "log/slog" + "math" + "math/rand" + "os" + "os/exec" + "strconv" + "strings" + + "github.com/cartesi/rollups-node/pkg/emulator" +) + +const ( + ramLength = 64 << 20 +) + +var ( + ImagesPath = "/usr/share/cartesi-machine/images/" + RiscvCC = "riscv64-linux-gnu-gcc-12" +) + +type Snapshot struct { + pkg string + temp string + + // Directory where the snapshot was stored. + Dir string + + // Reason why the snapshot stopped running before being stored. + BreakReason emulator.BreakReason +} + +// FromScript creates a snapshot given an one-line command. +func FromScript(command string, cycles uint64) (*Snapshot, error) { + snapshot := &Snapshot{pkg: fmt.Sprintf("fromscript%d", rand.Int())} + + err := snapshot.createTempDir() + if err != nil { + return nil, err + } + + config := defaultMachineConfig() + config.Dtb.Entrypoint = command + + err = snapshot.createRunAndStore(config, cycles) + return snapshot, err +} + +// FromGoCode creates a snapshot from Go code. +func FromGoCode(cycles uint64, code string) (*Snapshot, error) { + pkg := fmt.Sprintf("fromgocode%d", rand.Int()) + + const perm = 0775 + + err := os.Mkdir(pkg, perm) + if err != nil { + return nil, err + } + + defer os.RemoveAll(pkg) + + err = os.WriteFile(pkg+"/"+pkg+".go", []byte(code), perm) + if err != nil { + return nil, err + } + + return FromGoProject(pkg, cycles) +} + +// FromGoProject creates a snapshot from a Go project. +// It expects the main function to be at pkg/pkg.go. +func FromGoProject(pkg string, cycles uint64) (*Snapshot, error) { + snapshot := &Snapshot{pkg: pkg} + + err := snapshot.createTempDir() + if err != nil { + return nil, err + } + + // Building the riscv64 binary from source. + sourceCode := pkg + "/" + pkg + ".go" + cmd := exec.Command("go", "build", "-o", snapshot.temp, sourceCode) + cmd.Env = append(os.Environ(), "CC="+RiscvCC, "CGO_ENABLED=1", "GOOS=linux", "GOARCH=riscv64") + err = run(cmd) + if err != nil { + return nil, snapshot.errAndClose(err) + } + + // Creating the .ext2 file. + const k = uint64(10) + blocks := strconv.FormatUint(k*1024, 10) + ext2 := pkg + ".ext2" + err = run(exec.Command("xgenext2fs", "-f", "-b", blocks, "-d", snapshot.temp, ext2)) + if err != nil { + return nil, snapshot.errAndClose(err) + } + err = run(exec.Command("mv", ext2, snapshot.temp)) + if err != nil { + return nil, snapshot.errAndClose(err) + } + + // Modifying 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`, + pkg, pkg, pkg) + config.Dtb.Entrypoint = fmt.Sprintf("CMT_DEBUG=yes /mnt/%s/%s", pkg, pkg) + config.FlashDrive = append(config.FlashDrive, emulator.MemoryRangeConfig{ + Start: 0x90000000000000, //nolint:mnd + Length: math.MaxUint64, + ImageFilename: snapshot.temp + "/" + ext2, + }) + + err = snapshot.createRunAndStore(config, cycles) + if err != nil { + return nil, err + } + + return snapshot, nil +} + +// Close deletes the temporary directory created to hold the snapshot files. +func (snapshot *Snapshot) Close() error { + return os.RemoveAll(snapshot.temp) +} + +// ------------------------------------------------------------------------------------------------ + +func defaultMachineConfig() *emulator.MachineConfig { + config := emulator.NewDefaultMachineConfig() + config.Ram.Length = ramLength + 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, //nolint:mnd + Length: math.MaxUint64, + ImageFilename: ImagesPath + "rootfs.ext2", + }} + return config +} + +func (snapshot *Snapshot) createTempDir() error { + temp, err := os.MkdirTemp("", snapshot.pkg+"-*") + if err != nil { + return err + } + snapshot.temp = temp + snapshot.Dir = snapshot.temp + "/snapshot" + return nil +} + +func (snapshot *Snapshot) createRunAndStore(config *emulator.MachineConfig, cycles uint64) error { + // Creates the (local) machine. + machine, err := emulator.NewMachine(config, &emulator.MachineRuntimeConfig{}) + if err != nil { + return snapshot.errAndClose(err) + } + defer machine.Delete() + + // Runs the machine. + snapshot.BreakReason, err = machine.Run(cycles) + if err != nil { + return snapshot.errAndClose(err) + } + + // Stores the machine. + err = machine.Store(snapshot.Dir) + if err != nil { + return snapshot.errAndClose(err) + } + + return nil +} + +func (snapshot Snapshot) errAndClose(err error) error { + return errors.Join(err, snapshot.Close()) +} + +func run(cmd *exec.Cmd) error { + 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()) + } + return err +}