diff --git a/.circleci/config.yml b/.circleci/config.yml index 5c6ac18f6f71..d876ba148a37 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1123,6 +1123,9 @@ workflows: - go-lint: name: op-proposer-lint module: op-proposer + - go-lint: + name: op-program-lint + module: op-program - go-lint: name: op-service-lint module: op-service @@ -1145,6 +1148,9 @@ workflows: - go-test: name: op-proposer-tests module: op-proposer + - go-test: + name: op-program-tests + module: op-program - go-test: name: op-service-tests module: op-service @@ -1164,12 +1170,14 @@ workflows: - op-e2e-lint - op-node-lint - op-proposer-lint + - op-program-lint - op-service-lint - op-batcher-tests - op-bindings-tests - op-chain-ops-tests - op-node-tests - op-proposer-tests + - op-program-tests - op-service-tests - op-e2e-WS-tests - op-e2e-HTTP-tests diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5b8a358e5230..d3fd36188bf8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -29,6 +29,7 @@ /op-node @ethereum-optimism/go-reviewers /op-node/rollup @protolambda @trianglesphere /op-proposer @ethereum-optimism/go-reviewers +/op-program @ethereum-optimism/go-reviewers /op-service @ethereum-optimism/go-reviewers # Ops diff --git a/Makefile b/Makefile index 30a1d14bd7a8..fb6ba7049db5 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,10 @@ op-proposer: make -C ./op-proposer op-proposer .PHONY: op-proposer +op-program: + make -C ./op-program op-program +.PHONY: op-program + mod-tidy: # Below GOPRIVATE line allows mod-tidy to be run immediately after # releasing new versions. This bypasses the Go modules proxy, which diff --git a/op-program/.gitignore b/op-program/.gitignore new file mode 100644 index 000000000000..ba077a4031ad --- /dev/null +++ b/op-program/.gitignore @@ -0,0 +1 @@ +bin diff --git a/op-program/LICENSE b/op-program/LICENSE new file mode 100644 index 000000000000..b7328e483ff7 --- /dev/null +++ b/op-program/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Optimism + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/op-program/Makefile b/op-program/Makefile new file mode 100644 index 000000000000..08eb62187205 --- /dev/null +++ b/op-program/Makefile @@ -0,0 +1,27 @@ +GITCOMMIT := $(shell git rev-parse HEAD) +GITDATE := $(shell git show -s --format='%ct') +VERSION := v0.0.0 + +LDFLAGSSTRING +=-X main.GitCommit=$(GITCOMMIT) +LDFLAGSSTRING +=-X main.GitDate=$(GITDATE) +LDFLAGSSTRING +=-X github.com/ethereum-optimism/optimism/op-program/version.Version=$(VERSION) +LDFLAGSSTRING +=-X github.com/ethereum-optimism/optimism/op-program/version.Meta=$(VERSION_META) +LDFLAGS := -ldflags "$(LDFLAGSSTRING)" + +op-program: + env GO111MODULE=on GOOS=$(TARGETOS) GOARCH=$(TARGETARCH) go build -v $(LDFLAGS) -o ./bin/op-program ./cmd/main.go + +clean: + rm -rf bin + +test: + go test -v ./... + +lint: + golangci-lint run -E goimports,sqlclosecheck,bodyclose,asciicheck,misspell,errorlint -e "errors.As" -e "errors.Is" + +.PHONY: \ + op-program \ + clean \ + test \ + lint diff --git a/op-program/README.md b/op-program/README.md new file mode 100644 index 000000000000..6a2283bac8b6 --- /dev/null +++ b/op-program/README.md @@ -0,0 +1,43 @@ +# op-program + +Implements a fault proof program that runs through the rollup state-transition to verify an L2 output from L1 inputs. +This verifiable output can then resolve a disputed output on L1. + +The program is designed such that it can be run in a deterministic way such that two invocations with the same input +data wil result in not only the same output, but the same program execution trace. This allows it to be run in an +on-chain VM as part of the dispute resolution process. + +## Compiling + +To build op-program, from within the `op-program` directory run: + +```shell +make op-program +``` + +This resulting executable will be in `./bin/op-program` + +## Testing + +To run op-program unit tests, from within the `op-program` directory run: + +```shell +make test +``` + +## Lint + +To run the linter, from within the `op-program` directory run: +```shell +make lint +``` + +This requires having `golangci-lint` installed. + +## Running + +From within the `op-program` directory, options can be reviewed with: + +```shell +./bin/op-program --help +``` diff --git a/op-program/cmd/main.go b/op-program/cmd/main.go new file mode 100644 index 000000000000..1fbccdec0917 --- /dev/null +++ b/op-program/cmd/main.go @@ -0,0 +1,90 @@ +package main + +import ( + "fmt" + "os" + + "github.com/ethereum-optimism/optimism/op-node/chaincfg" + "github.com/ethereum-optimism/optimism/op-program/config" + "github.com/ethereum-optimism/optimism/op-program/flags" + "github.com/ethereum-optimism/optimism/op-program/version" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + "github.com/ethereum/go-ethereum/log" + "github.com/urfave/cli" +) + +var ( + GitCommit = "" + GitDate = "" +) + +// VersionWithMeta holds the textual version string including the metadata. +var VersionWithMeta = func() string { + v := version.Version + if GitCommit != "" { + v += "-" + GitCommit[:8] + } + if GitDate != "" { + v += "-" + GitDate + } + if version.Meta != "" { + v += "-" + version.Meta + } + return v +}() + +func main() { + args := os.Args + err := run(args, FaultProofProgram) + if err != nil { + log.Crit("Application failed", "message", err) + } +} + +type ConfigAction func(log log.Logger, config *config.Config) error + +// run parses the supplied args to create a config.Config instance, sets up logging +// then calls the supplied ConfigAction. +// This allows testing the translation from CLI arguments to Config +func run(args []string, action ConfigAction) error { + // Set up logger with a default INFO level in case we fail to parse flags, + // otherwise the final critical log won't show what the parsing error was. + oplog.SetupDefaults() + + app := cli.NewApp() + app.Version = VersionWithMeta + app.Flags = flags.Flags + app.Name = "op-program" + app.Usage = "Optimism Fault Proof Program" + app.Description = "The Optimism Fault Proof Program fault proof program that runs through the rollup state-transition to verify an L2 output from L1 inputs." + app.Action = func(ctx *cli.Context) error { + logger, err := setupLogging(ctx) + if err != nil { + return err + } + logger.Info("Starting fault proof program", "version", VersionWithMeta) + + cfg, err := config.NewConfigFromCLI(ctx) + if err != nil { + return err + } + return action(logger, cfg) + } + + return app.Run(args) +} + +func setupLogging(ctx *cli.Context) (log.Logger, error) { + logCfg := oplog.ReadCLIConfig(ctx) + if err := logCfg.Check(); err != nil { + return nil, fmt.Errorf("log config error: %w", err) + } + logger := oplog.NewLogger(logCfg) + return logger, nil +} + +// FaultProofProgram is the programmatic entry-point for the fault proof program +func FaultProofProgram(log log.Logger, cfg *config.Config) error { + cfg.Rollup.LogDescription(log, chaincfg.L2ChainIDToNetworkName) + return nil +} diff --git a/op-program/cmd/main_test.go b/op-program/cmd/main_test.go new file mode 100644 index 000000000000..848f72110c09 --- /dev/null +++ b/op-program/cmd/main_test.go @@ -0,0 +1,125 @@ +package main + +import ( + "encoding/json" + "os" + "testing" + + "github.com/ethereum-optimism/optimism/op-node/chaincfg" + "github.com/ethereum-optimism/optimism/op-program/config" + "github.com/ethereum/go-ethereum/log" + "github.com/stretchr/testify/require" +) + +func TestLogLevel(t *testing.T) { + t.Run("RejectInvalid", func(t *testing.T) { + verifyArgsInvalid(t, "unknown level: foo", addRequiredArgs("--log.level=foo")) + }) + + for _, lvl := range []string{"trace", "debug", "info", "error", "crit"} { + lvl := lvl + t.Run("AcceptValid_"+lvl, func(t *testing.T) { + logger, _, err := runWithArgs(addRequiredArgs("--log.level", lvl)) + require.NoError(t, err) + require.NotNil(t, logger) + }) + } +} + +func TestDefaultCLIOptionsMatchDefaultConfig(t *testing.T) { + cfg := configForArgs(t, addRequiredArgs()) + require.Equal(t, config.NewConfig(&chaincfg.Goerli), cfg) +} + +func TestNetwork(t *testing.T) { + t.Run("Unknown", func(t *testing.T) { + verifyArgsInvalid(t, "invalid network bar", replaceRequiredArg("--network", "bar")) + }) + + t.Run("Required", func(t *testing.T) { + verifyArgsInvalid(t, "flag rollup.config or network is required", addRequiredArgsExcept("--network")) + }) + + t.Run("DisallowNetworkAndRollupConfig", func(t *testing.T) { + verifyArgsInvalid(t, "cannot specify both rollup.config and network", addRequiredArgs("--rollup.config=foo")) + }) + + t.Run("RollupConfig", func(t *testing.T) { + dir := t.TempDir() + configJson, err := json.Marshal(chaincfg.Goerli) + require.NoError(t, err) + configFile := dir + "/config.json" + err = os.WriteFile(configFile, configJson, os.ModePerm) + require.NoError(t, err) + + cfg := configForArgs(t, addRequiredArgsExcept("--network", "--rollup.config", configFile)) + require.Equal(t, chaincfg.Goerli, *cfg.Rollup) + }) + + for name, cfg := range chaincfg.NetworksByName { + name := name + expected := cfg + t.Run("Network_"+name, func(t *testing.T) { + cfg := configForArgs(t, replaceRequiredArg("--network", name)) + require.Equal(t, expected, *cfg.Rollup) + }) + } +} + +func verifyArgsInvalid(t *testing.T, messageContains string, cliArgs []string) { + _, _, err := runWithArgs(cliArgs) + require.ErrorContains(t, err, messageContains) +} + +func configForArgs(t *testing.T, cliArgs []string) *config.Config { + _, cfg, err := runWithArgs(cliArgs) + require.NoError(t, err) + return cfg +} + +func runWithArgs(cliArgs []string) (log.Logger, *config.Config, error) { + var cfg *config.Config + var logger log.Logger + fullArgs := append([]string{"op-program"}, cliArgs...) + err := run(fullArgs, func(log log.Logger, config *config.Config) error { + logger = log + cfg = config + return nil + }) + return logger, cfg, err +} + +func addRequiredArgs(args ...string) []string { + req := requiredArgs() + combined := toArgList(req) + return append(combined, args...) +} + +func addRequiredArgsExcept(name string, optionalArgs ...string) []string { + req := requiredArgs() + delete(req, name) + return append(toArgList(req), optionalArgs...) +} + +func replaceRequiredArg(name string, value string) []string { + req := requiredArgs() + req[name] = value + return toArgList(req) +} + +// requiredArgs returns map of argument names to values which are the minimal arguments required +// to create a valid Config +func requiredArgs() map[string]string { + return map[string]string{ + "--network": "goerli", + } +} + +func toArgList(req map[string]string) []string { + var combined []string + for name, value := range req { + combined = append(combined, name) + combined = append(combined, value) + } + return combined +} diff --git a/op-program/config/config.go b/op-program/config/config.go new file mode 100644 index 000000000000..5bc59a446130 --- /dev/null +++ b/op-program/config/config.go @@ -0,0 +1,48 @@ +package config + +import ( + "errors" + + opnode "github.com/ethereum-optimism/optimism/op-node" + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/ethereum-optimism/optimism/op-program/flags" + "github.com/urfave/cli" +) + +var ( + ErrMissingRollupConfig = errors.New("missing rollup config") +) + +type Config struct { + Rollup *rollup.Config +} + +func (c *Config) Check() error { + if c.Rollup == nil { + return ErrMissingRollupConfig + } + if err := c.Rollup.Check(); err != nil { + return err + } + return nil +} + +// NewConfig creates a Config with all optional values set to the CLI default value +func NewConfig(rollupCfg *rollup.Config) *Config { + return &Config{ + Rollup: rollupCfg, + } +} + +func NewConfigFromCLI(ctx *cli.Context) (*Config, error) { + if err := flags.CheckRequired(ctx); err != nil { + return nil, err + } + rollupCfg, err := opnode.NewRollupConfig(ctx) + if err != nil { + return nil, err + } + return &Config{ + Rollup: rollupCfg, + }, nil +} diff --git a/op-program/config/config_test.go b/op-program/config/config_test.go new file mode 100644 index 000000000000..7c0e17fba4a4 --- /dev/null +++ b/op-program/config/config_test.go @@ -0,0 +1,26 @@ +package config + +import ( + "testing" + + "github.com/ethereum-optimism/optimism/op-node/chaincfg" + "github.com/ethereum-optimism/optimism/op-node/rollup" + "github.com/stretchr/testify/require" +) + +func TestDefaultConfigIsValid(t *testing.T) { + err := NewConfig(&chaincfg.Goerli).Check() + require.NoError(t, err) +} + +func TestRollupConfig(t *testing.T) { + t.Run("Required", func(t *testing.T) { + err := NewConfig(nil).Check() + require.ErrorIs(t, err, ErrMissingRollupConfig) + }) + + t.Run("Valid", func(t *testing.T) { + err := NewConfig(&rollup.Config{}).Check() + require.ErrorIs(t, err, rollup.ErrBlockTimeZero) + }) +} diff --git a/op-program/flags/flags.go b/op-program/flags/flags.go new file mode 100644 index 000000000000..330bd1319bbf --- /dev/null +++ b/op-program/flags/flags.go @@ -0,0 +1,51 @@ +package flags + +import ( + "fmt" + "strings" + + "github.com/ethereum-optimism/optimism/op-node/chaincfg" + service "github.com/ethereum-optimism/optimism/op-service" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + "github.com/urfave/cli" +) + +const envVarPrefix = "OP_PROGRAM" + +var ( + RollupConfig = cli.StringFlag{ + Name: "rollup.config", + Usage: "Rollup chain parameters", + EnvVar: service.PrefixEnvVar(envVarPrefix, "ROLLUP_CONFIG"), + } + Network = cli.StringFlag{ + Name: "network", + Usage: fmt.Sprintf("Predefined network selection. Available networks: %s", strings.Join(chaincfg.AvailableNetworks(), ", ")), + EnvVar: service.PrefixEnvVar(envVarPrefix, "NETWORK"), + } +) + +// Flags contains the list of configuration options available to the binary. +var Flags []cli.Flag + +var programFlags = []cli.Flag{ + RollupConfig, + Network, +} + +func init() { + Flags = append(Flags, oplog.CLIFlags(envVarPrefix)...) + Flags = append(Flags, programFlags...) +} + +func CheckRequired(ctx *cli.Context) error { + rollupConfig := ctx.GlobalString(RollupConfig.Name) + network := ctx.GlobalString(Network.Name) + if rollupConfig == "" && network == "" { + return fmt.Errorf("flag %s or %s is required", RollupConfig.Name, Network.Name) + } + if rollupConfig != "" && network != "" { + return fmt.Errorf("cannot specify both %s and %s", RollupConfig.Name, Network.Name) + } + return nil +} diff --git a/op-program/flags/flags_test.go b/op-program/flags/flags_test.go new file mode 100644 index 000000000000..7c3b884fd1f8 --- /dev/null +++ b/op-program/flags/flags_test.go @@ -0,0 +1,38 @@ +package flags + +import ( + "reflect" + "strings" + "testing" +) + +// TestUniqueFlags asserts that all flag names are unique, to avoid accidental conflicts between the many flags. +func TestUniqueFlags(t *testing.T) { + seenCLI := make(map[string]struct{}) + for _, flag := range Flags { + name := flag.GetName() + if _, ok := seenCLI[name]; ok { + t.Errorf("duplicate flag %s", name) + continue + } + seenCLI[name] = struct{}{} + } +} + +func TestCorrectEnvVarPrefix(t *testing.T) { + for _, flag := range Flags { + values := reflect.ValueOf(flag) + envVarValue := values.FieldByName("EnvVar") + if envVarValue == (reflect.Value{}) { + t.Errorf("Failed to find EnvVar for flag %v", flag.GetName()) + continue + } + envVar := envVarValue.String() + if envVar[:len("OP_PROGRAM_")] != "OP_PROGRAM_" { + t.Errorf("Flag %v env var (%v) does not start with OP_PROGRAM_", flag.GetName(), envVar) + } + if strings.Contains(envVar, "__") { + t.Errorf("Flag %v env var (%v) has duplicate underscores", flag.GetName(), envVar) + } + } +} diff --git a/op-program/version/version.go b/op-program/version/version.go new file mode 100644 index 000000000000..327ee7b49727 --- /dev/null +++ b/op-program/version/version.go @@ -0,0 +1,6 @@ +package version + +var ( + Version = "v0.10.14" + Meta = "dev" +)