diff --git a/Makefile b/Makefile index 5ee243aae..32d4ea2f0 100644 --- a/Makefile +++ b/Makefile @@ -64,7 +64,7 @@ GO_ARTIFACTS := $(addprefix cartesi-rollups-,node cli evm-reader advancer valida # These artifacts embed the machine runtime and therefore require libcartesi. # Keep this list explicit: every other artifact is built with CGO_ENABLED=0, so # the normal build fails if a C dependency leaks into a pure service or tool. -MACHINE_GO_ARTIFACTS := cartesi-rollups-node cartesi-rollups-advancer +MACHINE_GO_ARTIFACTS := cartesi-rollups-node cartesi-rollups-advancer cartesi-rollups-machine-tool PURE_GO_ARTIFACTS := $(filter-out $(MACHINE_GO_ARTIFACTS),$(GO_ARTIFACTS)) # fixme(vfusco): path on all oses diff --git a/cmd/cartesi-rollups-machine-tool/main.go b/cmd/cartesi-rollups-machine-tool/main.go index 56d603468..be33790e0 100644 --- a/cmd/cartesi-rollups-machine-tool/main.go +++ b/cmd/cartesi-rollups-machine-tool/main.go @@ -4,22 +4,22 @@ package main import ( - "bytes" "context" - "encoding/base64" "encoding/json" "errors" "fmt" + "log/slog" "os" - "os/exec" "path/filepath" "strings" "github.com/cartesi/rollups-node/cmd/cartesi-rollups-machine-tool/accountdrive" "github.com/cartesi/rollups-node/internal/config" - "github.com/cartesi/rollups-node/internal/model" + "github.com/cartesi/rollups-node/internal/manager" + "github.com/cartesi/rollups-node/internal/replay" "github.com/cartesi/rollups-node/internal/repository" "github.com/cartesi/rollups-node/internal/repository/factory" + "github.com/cartesi/rollups-node/pkg/machine" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/spf13/cobra" @@ -29,63 +29,55 @@ const defaultInputPageSize = uint64(500) func main() { config.SetDefaults() - if err := newRootCommand().ExecuteContext(context.Background()); err != nil { + logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) + if err := newRootCommand(logger).ExecuteContext(context.Background()); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } -func newRootCommand() *cobra.Command { +func newRootCommand(logger *slog.Logger) *cobra.Command { root := &cobra.Command{ Use: "cartesi-rollups-machine-tool", Short: "Rollups-aware helper for replaying machines and generating withdrawal proofs", } - root.AddCommand(newReplayCommand()) - root.AddCommand(newProveCommand()) + root.AddCommand(newReplayCommand(logger)) + root.AddCommand(newProveCommand(logger)) return root } -func newReplayCommand() *cobra.Command { +func newReplayCommand(logger *slog.Logger) *cobra.Command { var opts replayOptions cmd := &cobra.Command{ Use: "replay", - Short: "Replay accepted inputs from the node database into a machine template", + Short: "Replay completed inputs from the node database into a machine template", RunE: func(cmd *cobra.Command, _ []string) error { opts.HasToEpoch = cmd.Flags().Changed("to-epoch") opts.HasToInputIndex = cmd.Flags().Changed("to-input-index") - return runReplay(cmd.Context(), opts) + return runReplay(cmd.Context(), logger, opts) }, } - cmd.Flags().StringVar(&opts.Template, "template", "", "Stored machine template path") cmd.Flags().StringVar(&opts.Application, "application", "", "Application name or address") cmd.Flags().StringVar(&opts.DatabaseConnection, "database-connection", "", "Database connection string") cmd.Flags().StringVar(&opts.Store, "store", "", "Output stored machine path") - cmd.Flags().StringVar(&opts.CartesiMachine, "cartesi-machine", "cartesi-machine", "cartesi-machine executable") - cmd.Flags().StringVar(&opts.Lua, "lua", "lua5.4", "Lua executable used by Dave/PRT replay") - cmd.Flags().StringVar(&opts.CartesiSDKRoot, "cartesi-sdk-root", "", "Cartesi SDK root used to resolve Lua modules") - cmd.Flags().Uint64Var(&opts.ToEpoch, "to-epoch", 0, "Replay accepted inputs in epochs up to this epoch") - cmd.Flags().Uint64Var(&opts.ToInputIndex, "to-input-index", 0, "Replay accepted inputs up to this input index") - cobra.CheckErr(cmd.MarkFlagRequired("template")) + cmd.Flags().Uint64Var(&opts.ToEpoch, "to-epoch", 0, "Replay completed inputs in epochs up to this epoch") + cmd.Flags().Uint64Var(&opts.ToInputIndex, "to-input-index", 0, "Replay completed inputs up to this input index") cobra.CheckErr(cmd.MarkFlagRequired("application")) cobra.CheckErr(cmd.MarkFlagRequired("store")) return cmd } type replayOptions struct { - Template string Application string DatabaseConnection string Store string - CartesiMachine string - Lua string - CartesiSDKRoot string ToEpoch uint64 ToInputIndex uint64 HasToEpoch bool HasToInputIndex bool } -func runReplay(ctx context.Context, opts replayOptions) error { +func runReplay(ctx context.Context, logger *slog.Logger, opts replayOptions) error { if opts.DatabaseConnection == "" { dsn, err := config.GetDatabaseConnection() if err != nil { @@ -111,124 +103,103 @@ func runReplay(ctx context.Context, opts replayOptions) error { return fmt.Errorf("application %q not found", opts.Application) } - inputs, lastInputIndex, err := collectReplayInputs(ctx, repo, opts) + toInputExclusive, err := resolveReplayUpperBound(ctx, repo, app.Name, opts) if err != nil { return err } - tmp, err := os.MkdirTemp("", "cartesi-rollups-machine-tool-replay-*") + lastInput, err := repo.GetLastProcessedInput(ctx, opts.Application) if err != nil { - return fmt.Errorf("create temp dir: %w", err) + return fmt.Errorf("get last processed input: %w", err) + } + if lastInput != nil && lastInput.Status.IsTerminal() && toInputExclusive > lastInput.Index { + return fmt.Errorf( + "cannot snapshot: replay range reaches the application's terminal input 0x%x (status %s). Replay up to input 0x%x instead", + lastInput.Index, lastInput.Status, lastInput.Index, + ) } - defer os.RemoveAll(tmp) //nolint:errcheck - if err := replayInputs(ctx, opts, tmp, app, inputs); err != nil { - return err + instance, err := manager.NewMachineInstance(ctx, app, logger, true) + if err != nil { + return fmt.Errorf("create machine instance: %w", err) } + defer instance.Close() - root, err := readStoredMachineRoot(ctx, opts.CartesiMachine, opts.Store) + result, err := replay.Run(ctx, repo, instance, replay.Options{ + Application: app, + FromInput: 0, + ToInputExclusive: toInputExclusive, + BatchSize: defaultInputPageSize, + Verification: repository.ReplayVerificationCanonical, + }) if err != nil { - return err + return fmt.Errorf("replay: %w", err) + } + if err := instance.CreateSnapshot(ctx, instance.ProcessedInputs(), opts.Store); err != nil { + return fmt.Errorf("store machine: %w", err) + } + + root, err := instance.Hash(ctx) + if err != nil { + return fmt.Errorf("read machine root: %w", err) } summary := struct { - ProcessedInputs int `json:"processed_inputs"` + ProcessedInputs uint64 `json:"processed_inputs"` LastInputIndex string `json:"last_input_index,omitempty"` MachineRoot string `json:"machine_root"` Store string `json:"store"` }{ - ProcessedInputs: len(inputs), - MachineRoot: root, + ProcessedInputs: result.ReplayedInputs, + MachineRoot: common.BytesToHash(root[:]).Hex(), Store: opts.Store, } - if lastInputIndex != nil { - summary.LastInputIndex = fmt.Sprintf("0x%x", *lastInputIndex) + if result.ReplayedInputs > 0 { + summary.LastInputIndex = fmt.Sprintf("0x%x", toInputExclusive-1) } return json.NewEncoder(os.Stdout).Encode(summary) } -func replayInputs( - ctx context.Context, - opts replayOptions, - tmp string, - app *model.Application, - inputs []*model.Input, -) error { - if app.IsDaveConsensus() && len(inputs) > 0 { - return replayDaveInputs(ctx, opts, tmp, inputs) - } - return replayInputsBatch(ctx, opts, tmp, inputs) -} - -func replayInputsBatch(ctx context.Context, opts replayOptions, tmp string, inputs []*model.Input) error { - if _, err := writeReplayInputFiles(tmp, inputs); err != nil { - return err - } - - args := []string{ - "--quiet", - "--no-revert", - "--load=" + opts.Template, - fmt.Sprintf("--cmio-advance-state=input:%s,input_index_begin:0,input_index_end:%d", - filepath.Join(tmp, "input-%i.bin"), len(inputs)), - "--store=" + opts.Store, - } - return runCommand(ctx, opts.CartesiMachine, args...) -} - -func collectReplayInputs( +// resolveReplayUpperBound resolves the exclusive upper input bound of the +// replay range: the end of the requested epoch, or one past the requested +// input index. +func resolveReplayUpperBound( ctx context.Context, repo repository.Repository, + application string, opts replayOptions, -) ([]*model.Input, *uint64, error) { - status := model.InputCompletionStatus_Accepted - filter := repository.InputFilter{Status: &status} - var offset uint64 - var result []*model.Input - var lastInputIndex *uint64 - for { - inputs, _, err := repo.ListInputs(ctx, opts.Application, filter, - repository.Pagination{Limit: defaultInputPageSize, Offset: offset}, false) +) (uint64, error) { + if opts.HasToEpoch { + epoch, err := repo.GetEpoch(ctx, application, opts.ToEpoch) if err != nil { - return nil, nil, fmt.Errorf("list inputs: %w", err) - } - if len(inputs) == 0 { - break + return 0, fmt.Errorf("get epoch %d: %w", opts.ToEpoch, err) } - for _, input := range inputs { - if opts.HasToEpoch && input.EpochIndex > opts.ToEpoch { - return result, lastInputIndex, nil - } - if opts.HasToInputIndex && input.Index > opts.ToInputIndex { - return result, lastInputIndex, nil - } - result = append(result, input) - idx := input.Index - lastInputIndex = &idx + if epoch == nil { + return 0, fmt.Errorf("epoch %d not found for application %q", opts.ToEpoch, application) } - if uint64(len(inputs)) < defaultInputPageSize { - break - } - offset += uint64(len(inputs)) + return epoch.InputIndexUpperBound, nil + } + if opts.ToInputIndex == ^uint64(0) { + return 0, fmt.Errorf("to-input-index overflow") } - return result, lastInputIndex, nil + return opts.ToInputIndex + 1, nil } -func newProveCommand() *cobra.Command { +func newProveCommand(logger *slog.Logger) *cobra.Command { prove := &cobra.Command{ Use: "prove", Short: "Generate Rollups proof files from a stored machine", } - prove.AddCommand(newProveAccountsDriveCommand()) + prove.AddCommand(newProveAccountsDriveCommand(logger)) return prove } -func newProveAccountsDriveCommand() *cobra.Command { +func newProveAccountsDriveCommand(logger *slog.Logger) *cobra.Command { var opts proveAccountsDriveOptions cmd := &cobra.Command{ Use: "accounts-drive", Short: "Generate accounts-drive root and account withdrawal proofs", RunE: func(cmd *cobra.Command, _ []string) error { - return runProveAccountsDrive(cmd.Context(), opts) + return runProveAccountsDrive(cmd.Context(), logger, opts) }, } cmd.Flags().StringVar(&opts.Snapshot, "snapshot", "", "Stored machine snapshot path") @@ -239,7 +210,6 @@ func newProveAccountsDriveCommand() *cobra.Command { cmd.Flags().Uint8Var(&opts.Log2LeavesPerAccount, "log2-leaves-per-account", 0, "Log2 of leaves per account") cmd.Flags().StringVar(&opts.OutDriveRootProof, "out-drive-root-proof", "", "Output JSON for prove-drive-root") cmd.Flags().StringVar(&opts.OutWithdrawProof, "out-withdraw-proof", "", "Output JSON for withdraw") - cmd.Flags().StringVar(&opts.CartesiMachine, "cartesi-machine", "cartesi-machine", "cartesi-machine executable") cobra.CheckErr(cmd.MarkFlagRequired("snapshot")) cobra.CheckErr(cmd.MarkFlagRequired("account")) cobra.CheckErr(cmd.MarkFlagRequired("out-drive-root-proof")) @@ -255,10 +225,9 @@ type proveAccountsDriveOptions struct { Log2LeavesPerAccount uint8 OutDriveRootProof string OutWithdrawProof string - CartesiMachine string } -func runProveAccountsDrive(ctx context.Context, opts proveAccountsDriveOptions) error { +func runProveAccountsDrive(ctx context.Context, logger *slog.Logger, opts proveAccountsDriveOptions) error { if !common.IsHexAddress(opts.Account) { return fmt.Errorf("invalid account address %q", opts.Account) } @@ -286,13 +255,20 @@ func runProveAccountsDrive(ctx context.Context, opts proveAccountsDriveOptions) if err != nil { return err } - machineProof, err := generateMachineProof(ctx, opts.CartesiMachine, opts.Snapshot, driveStart, log2DriveSize) + + m, err := machine.Load(ctx, logger, machine.DefaultConfig(opts.Snapshot)) if err != nil { - return err + return fmt.Errorf("load stored machine: %w", err) } - if !strings.EqualFold(strip0x(machineProof.TargetHash), accountProof.DriveRoot.Hex()[2:]) { + defer m.Close() + + machineProof, err := m.GetProof(ctx, driveStart, int32(log2DriveSize), 64) //nolint:mnd // full machine memory + if err != nil { + return fmt.Errorf("get accounts-drive proof: %w", err) + } + if common.Hash(machineProof.TargetHash) != accountProof.DriveRoot { return fmt.Errorf("accounts-drive root mismatch: machine proof has %s, local drive has %s", - ensure0x(machineProof.TargetHash), accountProof.DriveRoot.Hex()) + common.Hash(machineProof.TargetHash).Hex(), accountProof.DriveRoot.Hex()) } if err := writeDriveRootProof(opts.OutDriveRootProof, machineProof); err != nil { @@ -313,7 +289,7 @@ func runProveAccountsDrive(ctx context.Context, opts proveAccountsDriveOptions) Account: account, AccountIndex: fmt.Sprintf("0x%x", accountProof.AccountIndex), AccountsDriveMerkleRoot: accountProof.DriveRoot.Hex(), - MachineRoot: ensure0x(machineProof.RootHash), + MachineRoot: common.BytesToHash(machineProof.RootHash[:]).Hex(), DriveRootProofFile: opts.OutDriveRootProof, WithdrawProofFile: opts.OutWithdrawProof, } @@ -349,80 +325,16 @@ func findStoredDrive(snapshot string, start uint64, length uint64) (string, erro return "", fmt.Errorf("accounts drive not found in stored machine: start=0x%x length=0x%x", start, length) } -type cartesiMachineProof struct { - TargetAddress uint64 `json:"target_address"` - Log2TargetSize uint8 `json:"log2_target_size"` - Log2RootSize uint8 `json:"log2_root_size"` - TargetHash string `json:"target_hash"` - SiblingHashes []string `json:"sibling_hashes"` - RootHash string `json:"root_hash"` -} - -func generateMachineProof( - ctx context.Context, - cartesiMachine string, - snapshot string, - address uint64, - log2Size uint8, -) (*cartesiMachineProof, error) { - tmp, err := os.CreateTemp("", "cartesi-rollups-machine-proof-*.json") - if err != nil { - return nil, fmt.Errorf("create proof temp file: %w", err) - } - tmpPath := tmp.Name() - tmp.Close() - defer os.Remove(tmpPath) //nolint:errcheck - - args := []string{ - "--quiet", - "--no-revert", - "--load=" + snapshot, - fmt.Sprintf("--initial-proof=address:0x%x,log2_size:%d,filename:%s", address, log2Size, tmpPath), - "--", - "/bin/true", - } - if err := runCommand(ctx, cartesiMachine, args...); err != nil { - return nil, err - } - - raw, err := os.ReadFile(tmpPath) //nolint:gosec - if err != nil { - return nil, fmt.Errorf("read machine proof: %w", err) - } - var proof cartesiMachineProof - if err := json.Unmarshal(raw, &proof); err != nil { - return nil, fmt.Errorf("parse machine proof: %w", err) - } - - // convert the file hashes from base64 to hex - proof.RootHash, err = base64ToHex(proof.RootHash) - if err != nil { - return nil, err - } - proof.TargetHash, err = base64ToHex(proof.TargetHash) - if err != nil { - return nil, err - } - for i := range proof.SiblingHashes { - proof.SiblingHashes[i], err = base64ToHex(proof.SiblingHashes[i]) - if err != nil { - return nil, err - } - } - - return &proof, nil -} - -func writeDriveRootProof(path string, proof *cartesiMachineProof) error { +func writeDriveRootProof(path string, proof *machine.MemoryProof) error { out := struct { AccountsDriveMerkleRoot string `json:"accounts_drive_merkle_root"` Proof []string `json:"proof"` }{ - AccountsDriveMerkleRoot: ensure0x(proof.TargetHash), - Proof: make([]string, len(proof.SiblingHashes)), + AccountsDriveMerkleRoot: common.BytesToHash(proof.TargetHash[:]).Hex(), + Proof: make([]string, len(proof.Siblings)), } - for i, sibling := range proof.SiblingHashes { - out.Proof[i] = ensure0x(sibling) + for i, sibling := range proof.Siblings { + out.Proof[i] = common.BytesToHash(sibling[:]).Hex() } return writeJSON(path, out) } @@ -454,56 +366,3 @@ func writeJSON(path string, value any) error { } return nil } - -func runCommand(ctx context.Context, name string, args ...string) error { - return runCommandWithEnv(ctx, name, nil, args...) -} - -func runCommandWithEnv(ctx context.Context, name string, env []string, args ...string) error { - cmd := exec.CommandContext(ctx, name, args...) //nolint:gosec - if len(env) > 0 { - cmd.Env = append(os.Environ(), env...) - } - var stderr bytes.Buffer - cmd.Stderr = &stderr - cmd.Stdout = ioDiscardUnlessDebug() - if err := cmd.Run(); err != nil { - return fmt.Errorf("%s %s failed: %w\n%s", name, strings.Join(args, " "), err, stderr.String()) - } - return nil -} - -func ioDiscardUnlessDebug() *bytes.Buffer { - return &bytes.Buffer{} -} - -func readStoredMachineRoot(ctx context.Context, cartesiMachine string, store string) (string, error) { - const minProofLog2Size = 5 - proof, err := generateMachineProof(ctx, cartesiMachine, store, 0, minProofLog2Size) - if err != nil { - return "", fmt.Errorf("read stored machine root: %w", err) - } - return ensure0x(proof.RootHash), nil -} - -func ensure0x(s string) string { - if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") { - return "0x" + s[2:] - } - return "0x" + s -} - -func strip0x(s string) string { - return strings.TrimPrefix(strings.TrimPrefix(s, "0x"), "0X") -} - -func base64ToHex(s string) (string, error) { - raw, err := base64.StdEncoding.DecodeString(s) - if err != nil { - return "", fmt.Errorf("expected %q to be a hash in base64. %w", s, err) - } - if len(raw) != common.HashLength { - return "", fmt.Errorf("expected %q to decode to %d bytes, got %d", s, common.HashLength, len(raw)) - } - return common.BytesToHash(raw).Hex(), nil -} diff --git a/cmd/cartesi-rollups-machine-tool/replay_dave.go b/cmd/cartesi-rollups-machine-tool/replay_dave.go deleted file mode 100644 index f2003a68c..000000000 --- a/cmd/cartesi-rollups-machine-tool/replay_dave.go +++ /dev/null @@ -1,155 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -package main - -import ( - "context" - _ "embed" - "fmt" - "os" - "os/exec" - "path/filepath" - "regexp" - "strings" - - "github.com/cartesi/rollups-node/internal/model" -) - -//go:embed replay_dave.lua -var replayDaveLua string - -func replayDaveInputs(ctx context.Context, opts replayOptions, tmp string, inputs []*model.Input) error { - manifest, err := writeReplayInputManifest(tmp, inputs) - if err != nil { - return err - } - script := filepath.Join(tmp, "replay-dave.lua") - if err := os.WriteFile(script, []byte(replayDaveLua), 0600); err != nil { - return fmt.Errorf("write Dave replay Lua script: %w", err) - } - - args := []string{ - script, - "--template", opts.Template, - "--store", opts.Store, - "--inputs-manifest", manifest, - } - return runCommandWithEnv(ctx, opts.Lua, replayDaveLuaEnv(opts), args...) -} - -func writeReplayInputManifest(tmp string, inputs []*model.Input) (string, error) { - paths, err := writeReplayInputFiles(tmp, inputs) - if err != nil { - return "", err - } - manifest := filepath.Join(tmp, "inputs.txt") - var contents strings.Builder - for _, path := range paths { - contents.WriteString(path) - contents.WriteByte('\n') - } - if err := os.WriteFile(manifest, []byte(contents.String()), 0600); err != nil { - return "", fmt.Errorf("write Dave replay input manifest: %w", err) - } - return manifest, nil -} - -func writeReplayInputFiles(tmp string, inputs []*model.Input) ([]string, error) { - paths := make([]string, len(inputs)) - for i, input := range inputs { - path := filepath.Join(tmp, fmt.Sprintf("input-%d.bin", i)) - if err := os.WriteFile(path, input.RawData, 0600); err != nil { - return nil, fmt.Errorf("write replay input %d: %w", i, err) - } - paths[i] = path - } - return paths, nil -} - -func replayDaveLuaEnv(opts replayOptions) []string { - sdkRoot := opts.CartesiSDKRoot - if sdkRoot == "" { - sdkRoot = detectCartesiSDKRoot(opts.CartesiMachine) - } - if sdkRoot == "" { - return nil - } - - luaPath := filepath.Join(sdkRoot, "share", "lua", "5.4", "?.lua") - luaCPath := filepath.Join(sdkRoot, "lib", "lua", "5.4", "?.so") - env := []string{ - "LUA_PATH_5_4=" + prependLuaSearchPath(os.Getenv("LUA_PATH_5_4"), luaPath), - "LUA_CPATH_5_4=" + prependLuaSearchPath(os.Getenv("LUA_CPATH_5_4"), luaCPath), - "LUA_PATH=" + prependLuaSearchPath(os.Getenv("LUA_PATH"), luaPath), - "LUA_CPATH=" + prependLuaSearchPath(os.Getenv("LUA_CPATH"), luaCPath), - } - if os.Getenv("CARTESI_IMAGES_PATH") == "" { - env = append(env, "CARTESI_IMAGES_PATH="+filepath.Join(sdkRoot, "share", "cartesi-machine", "images")) - } - return env -} - -func prependLuaSearchPath(current string, entry string) string { - if current == "" { - return entry + ";;" - } - if luaSearchPathContains(current, entry) { - return current - } - return entry + ";" + current -} - -func luaSearchPathContains(path string, entry string) bool { - for _, part := range strings.Split(path, ";") { - if part == entry { - return true - } - } - return false -} - -func detectCartesiSDKRoot(cartesiMachine string) string { - path, err := exec.LookPath(cartesiMachine) - if err != nil { - path = cartesiMachine - } - if resolved, err := filepath.EvalSymlinks(path); err == nil { - path = resolved - } - - if root := detectCartesiSDKRootFromFile(path); root != "" { - return root - } - for _, root := range []string{"/opt/cartesi-sdk21", "/opt/cartesi"} { - if fileExists(filepath.Join(root, "share", "lua", "5.4", "cartesi.lua")) { - return root - } - } - return "" -} - -var ( - cartesiMachineLuaPattern = regexp.MustCompile(`["']([^"']*/share/lua/5\.4/cartesi-machine\.lua)["']`) - cartesiLuaPathPattern = regexp.MustCompile(`["']?([^"'\s;]*/share/lua/5\.4)/\?\.lua`) -) - -func detectCartesiSDKRootFromFile(path string) string { - raw, err := os.ReadFile(path) //nolint:gosec - if err != nil { - return "" - } - text := string(raw) - if match := cartesiMachineLuaPattern.FindStringSubmatch(text); len(match) == 2 { - return strings.TrimSuffix(match[1], "/share/lua/5.4/cartesi-machine.lua") - } - if match := cartesiLuaPathPattern.FindStringSubmatch(text); len(match) == 2 { - return strings.TrimSuffix(match[1], "/share/lua/5.4") - } - return "" -} - -func fileExists(path string) bool { - info, err := os.Stat(path) - return err == nil && !info.IsDir() -} diff --git a/cmd/cartesi-rollups-machine-tool/replay_dave.lua b/cmd/cartesi-rollups-machine-tool/replay_dave.lua deleted file mode 100644 index dc957a628..000000000 --- a/cmd/cartesi-rollups-machine-tool/replay_dave.lua +++ /dev/null @@ -1,100 +0,0 @@ --- (c) Cartesi and individual authors (see AUTHORS) --- SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -local cartesi = require("cartesi") - -local function usage(message) - if message then io.stderr:write(message, "\n") end - io.stderr:write( - "usage: replay_dave.lua --template --store --inputs-manifest \n" - ) - os.exit(2) -end - -local function parse_args(args) - local opts = {} - local i = 1 - while i <= #args do - local key = args[i] - local value = args[i + 1] - if key == "--template" then - opts.template = value - elseif key == "--store" then - opts.store = value - elseif key == "--inputs-manifest" then - opts.inputs_manifest = value - else - usage("unknown argument: " .. tostring(key)) - end - if not value then usage("missing value for " .. tostring(key)) end - i = i + 2 - end - if not opts.template then usage("missing --template") end - if not opts.store then usage("missing --store") end - if not opts.inputs_manifest then usage("missing --inputs-manifest") end - return opts -end - -local function read_all(path) - local file = assert(io.open(path, "rb")) - return assert(file:read("*a")) -end - -local function read_input_paths(path) - local inputs = {} - for line in io.lines(path) do - if line ~= "" then inputs[#inputs + 1] = line end - end - return inputs -end - -local function run_until_manual_yield(machine) - while true do - local break_reason = machine:run(math.maxinteger) - if break_reason == cartesi.BREAK_REASON_YIELDED_MANUALLY then - return - elseif break_reason == cartesi.BREAK_REASON_YIELDED_AUTOMATICALLY then - machine:receive_cmio_request() - elseif break_reason == cartesi.BREAK_REASON_HALTED then - error("machine halted before a manual yield") - elseif break_reason == cartesi.BREAK_REASON_FAILED then - error("machine failed before a manual yield") - elseif break_reason ~= cartesi.BREAK_REASON_YIELDED_SOFTLY then - error("unexpected machine break reason: " .. tostring(break_reason)) - end - end -end - -local function ensure_manual_yield(machine) - if machine:read_reg("iflags_Y") == 0 then - run_until_manual_yield(machine) - end -end - -local function advance_one(machine, input_path, input_number) - local checkpoint = machine:get_root_hash() - machine:send_cmio_response(cartesi.HTIF_YIELD_REASON_ADVANCE_STATE, read_all(input_path), checkpoint) - run_until_manual_yield(machine) - - local _, reason, data = machine:receive_cmio_request() - if reason == cartesi.HTIF_YIELD_MANUAL_REASON_RX_REJECTED then - error(string.format("Dave replay input %d was rejected", input_number)) - elseif reason == cartesi.HTIF_YIELD_MANUAL_REASON_TX_EXCEPTION then - error(string.format("Dave replay input %d raised an exception", input_number)) - elseif reason ~= cartesi.HTIF_YIELD_MANUAL_REASON_RX_ACCEPTED then - error(string.format("Dave replay input %d ended with unexpected yield reason %s", input_number, tostring(reason))) - end - if #data ~= 32 then - error(string.format("Dave replay input %d returned an invalid outputs hash length: %d", input_number, #data)) - end -end - -local opts = parse_args(arg) -local machine = cartesi.machine(opts.template) -ensure_manual_yield(machine) - -for index, input_path in ipairs(read_input_paths(opts.inputs_manifest)) do - advance_one(machine, input_path, index - 1) -end - -machine:store(opts.store) diff --git a/cmd/cartesi-rollups-machine-tool/replay_dave_test.go b/cmd/cartesi-rollups-machine-tool/replay_dave_test.go deleted file mode 100644 index bfc923d33..000000000 --- a/cmd/cartesi-rollups-machine-tool/replay_dave_test.go +++ /dev/null @@ -1,110 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -package main - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "github.com/cartesi/rollups-node/internal/model" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestDetectCartesiSDKRootFromFile_WrapperWithCartesiMachineLua_ReturnsRoot(t *testing.T) { - t.Parallel() - - sdkRoot := filepath.Join(t.TempDir(), "cartesi-sdk") - luaDir := filepath.Join(sdkRoot, "share", "lua", "5.4") - wrapper := filepath.Join(t.TempDir(), "cartesi-machine") - contents := `#!/bin/sh -export LUA_PATH_5_4="` + luaDir + `/?.lua;${LUA_PATH_5_4:-;}" -lua5.4 "` + filepath.Join(luaDir, "cartesi-machine.lua") + `" "$@" -` - require.NoError(t, os.WriteFile(wrapper, []byte(contents), 0600)) - - assert.Equal(t, sdkRoot, detectCartesiSDKRootFromFile(wrapper)) -} - -func TestPrependLuaSearchPath_EmptyPath_KeepsLuaDefaultFallback(t *testing.T) { - t.Parallel() - - assert.Equal(t, "/opt/cartesi-sdk21/share/lua/5.4/?.lua;;", - prependLuaSearchPath("", "/opt/cartesi-sdk21/share/lua/5.4/?.lua")) -} - -func TestPrependLuaSearchPath_ExistingPath_PrependsWithoutDroppingExisting(t *testing.T) { - t.Parallel() - - got := prependLuaSearchPath("/custom/?.lua", "/opt/cartesi-sdk21/share/lua/5.4/?.lua") - - assert.Equal(t, "/opt/cartesi-sdk21/share/lua/5.4/?.lua;/custom/?.lua", got) -} - -func TestPrependLuaSearchPath_AlreadyPresent_DoesNotDuplicate(t *testing.T) { - t.Parallel() - - path := "/opt/cartesi-sdk21/share/lua/5.4/?.lua;/custom/?.lua" - - assert.Equal(t, path, prependLuaSearchPath(path, "/opt/cartesi-sdk21/share/lua/5.4/?.lua")) -} - -func TestReplayDaveLuaEnv_AutoDetectsSDKRootFromCartesiMachineWrapper(t *testing.T) { - sdkRoot := filepath.Join(t.TempDir(), "cartesi-sdk") - luaDir := filepath.Join(sdkRoot, "share", "lua", "5.4") - wrapper := filepath.Join(t.TempDir(), "cartesi-machine") - contents := `#!/bin/sh -export LUA_PATH_5_4="` + luaDir + `/?.lua;${LUA_PATH_5_4:-;}" -export LUA_CPATH_5_4="` + filepath.Join(sdkRoot, "lib", "lua", "5.4") + `/?.so;${LUA_CPATH_5_4:-;}" -lua5.4 "` + filepath.Join(luaDir, "cartesi-machine.lua") + `" "$@" -` - require.NoError(t, os.WriteFile(wrapper, []byte(contents), 0600)) - t.Setenv("LUA_PATH_5_4", "/custom/?.lua") - t.Setenv("LUA_CPATH_5_4", "") - t.Setenv("LUA_PATH", "") - t.Setenv("LUA_CPATH", "") - t.Setenv("CARTESI_IMAGES_PATH", "") - - env := replayDaveLuaEnv(replayOptions{CartesiMachine: wrapper}) - envByKey := envMap(env) - - assert.Equal(t, filepath.Join(luaDir, "?.lua")+";/custom/?.lua", envByKey["LUA_PATH_5_4"]) - assert.Equal(t, filepath.Join(sdkRoot, "lib", "lua", "5.4", "?.so")+";;", envByKey["LUA_CPATH_5_4"]) - assert.Equal(t, filepath.Join(luaDir, "?.lua")+";;", envByKey["LUA_PATH"]) - assert.Equal(t, filepath.Join(sdkRoot, "share", "cartesi-machine", "images"), envByKey["CARTESI_IMAGES_PATH"]) -} - -func TestWriteReplayInputManifest_WritesPayloadsAndManifest(t *testing.T) { - t.Parallel() - - tmp := t.TempDir() - inputs := []*model.Input{ - {RawData: []byte{0x01, 0x02}}, - {RawData: []byte("cartesi")}, - } - - manifest, err := writeReplayInputManifest(tmp, inputs) - require.NoError(t, err) - - raw, err := os.ReadFile(manifest) - require.NoError(t, err) - paths := strings.Split(strings.TrimSpace(string(raw)), "\n") - require.Len(t, paths, len(inputs)) - for i, path := range paths { - payload, err := os.ReadFile(path) - require.NoError(t, err) - assert.Equal(t, inputs[i].RawData, payload) - } -} - -func envMap(env []string) map[string]string { - result := make(map[string]string, len(env)) - for _, item := range env { - key, value, _ := strings.Cut(item, "=") - result[key] = value - } - return result -} diff --git a/internal/advancer/determinism_test.go b/internal/advancer/determinism_test.go index f7effe6ea..2e25de457 100644 --- a/internal/advancer/determinism_test.go +++ b/internal/advancer/determinism_test.go @@ -767,6 +767,12 @@ func (m *determinismRuntime) StateProof(ctx context.Context) (*machine.StateProo }, nil } +func (m *determinismRuntime) GetProof(ctx context.Context, _ uint64, _, _ int32) (*machine.MemoryProof, error) { + m.mu.Lock() + defer m.mu.Unlock() + return nil, m.checkOpenLocked(ctx) +} + func (m *determinismRuntime) Advance( ctx context.Context, input []byte, diff --git a/internal/manager/instance_test.go b/internal/manager/instance_test.go index abfb22982..0c6e46146 100644 --- a/internal/manager/instance_test.go +++ b/internal/manager/instance_test.go @@ -1449,6 +1449,8 @@ type MockRollupsMachine struct { StateProofReturn *machine.StateProof StateProofError error StateProofFunc func(context.Context) (*machine.StateProof, error) + GetProofReturn *machine.MemoryProof + GetProofError error AdvanceError error LastAdvanceComputeHashes bool @@ -1480,6 +1482,10 @@ func (m *MockRollupsMachine) StateProof(ctx context.Context) (*machine.StateProo return m.StateProofReturn, m.StateProofError } +func (m *MockRollupsMachine) GetProof(_ context.Context, _ uint64, _, _ int32) (*machine.MemoryProof, error) { + return m.GetProofReturn, m.GetProofError +} + func (m *MockRollupsMachine) Advance(_ context.Context, _ []byte, _ machine.Hash, computeHashes bool) (*machine.AdvanceResponse, error) { m.LastAdvanceComputeHashes = computeHashes if m.AdvanceError != nil { diff --git a/pkg/machine/implementation.go b/pkg/machine/implementation.go index 1fddb5e5d..43ac7e91f 100644 --- a/pkg/machine/implementation.go +++ b/pkg/machine/implementation.go @@ -194,6 +194,19 @@ func (m *machineImpl) StateProof(ctx context.Context) (*StateProof, error) { }, nil } +// GetProof returns a Merkle proof for the memory span starting at address +// with log2TargetSize, rooted at the machine memory tree of log2RootSize. +func (m *machineImpl) GetProof(ctx context.Context, address uint64, log2TargetSize, log2RootSize int32) (*MemoryProof, error) { + if err := checkContext(ctx); err != nil { + return nil, err + } + proof, err := m.backend.GetProof(address, log2TargetSize, log2RootSize, m.params.LoadDeadline) + if err != nil { + return nil, errors.Join(ErrMachineInternal, fmt.Errorf("could not get memory proof: %w", err)) + } + return &proof, nil +} + // ValidateAcceptedState checks the state semantics required when an epoch // is published through the released v3 contracts. StateProof itself remains // generic so the exact post-run proof can also be persisted for terminal diff --git a/pkg/machine/machine.go b/pkg/machine/machine.go index 793d78473..32db795ac 100644 --- a/pkg/machine/machine.go +++ b/pkg/machine/machine.go @@ -148,6 +148,10 @@ type Machine interface { // machine root and the three state leaves used by the rollups contracts. StateProof(ctx context.Context) (*StateProof, error) + // GetProof returns a Merkle proof for the memory span starting at address + // with log2TargetSize, rooted at the machine memory tree of log2RootSize. + GetProof(ctx context.Context, address uint64, log2TargetSize, log2RootSize int32) (*MemoryProof, error) + // Advance sends an input to the machine. // The checkpointHash is the machine's root hash before processing the input, // sent along with the request so the machine can revert to it if needed. diff --git a/pkg/machine/machine_test.go b/pkg/machine/machine_test.go index a8469ee8e..bc5fc51dd 100644 --- a/pkg/machine/machine_test.go +++ b/pkg/machine/machine_test.go @@ -5,15 +5,19 @@ package machine import ( "context" + "encoding/binary" + "encoding/json" "errors" "fmt" "io" "log/slog" + "path/filepath" "testing" "time" "github.com/cartesi/rollups-node/internal/model" "github.com/cartesi/rollups-node/pkg/emulator" + "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/suite" ) @@ -193,6 +197,90 @@ func (s *MachineSuite) TestLoad() { mockBackend.AssertExpectations(s.T()) } +// getProofYieldProgram writes the prepared manual yield request to HTIF +// tohost, leaving the machine at the accepted manual yield that Load requires. +var getProofYieldProgram = []uint32{ + 0x400082b7, // lui t0,0x40008: load the HTIF base address + 0x0002b423, // sd zero,8(t0): clear fromhost + 0x0132b023, // sd x19,0(t0): write the prepared request to tohost +} + +const ( + getProofRAMStart = uint64(0x80000000) // RISC-V RAM base for the test guest + getProofRAMLength = 4096 +) + +// TestGetProof proves a 32-byte memory span of a real emulator machine and +// verifies the proof against the machine root and the span's content. +func (s *MachineSuite) TestGetProof() { + require := s.Require() + ctx := context.Background() + + config, err := json.Marshal(map[string]any{ + "processor": map[string]any{"registers": map[string]any{"pc": getProofRAMStart}}, + "ram": map[string]any{"length": getProofRAMLength}, + "cmio": map[string]any{ + "rx_buffer": map[string]any{}, + "tx_buffer": map[string]any{}, + }, + }) + require.NoError(err) + + emu, err := emulator.CreateMachine(string(config), "", "") + require.NoError(err) + defer emu.Delete() + defer func() { _ = emu.Destroy() }() + + program := make([]byte, 4*len(getProofYieldProgram)) + for i, instruction := range getProofYieldProgram { + binary.LittleEndian.PutUint32(program[4*i:], instruction) + } + require.NoError(emu.WriteMemory(getProofRAMStart, program)) + + // The pre-set registers place the machine at the accepted manual yield + // that Load requires, without running the guest. + request := htifDeviceYield<