diff --git a/.circleci/config.yml b/.circleci/config.yml index 758d62b53e956..1649da56d2f5b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1422,6 +1422,19 @@ jobs: paths: - "/home/circleci/.cache/golangci-lint" + check-op-geth-version: + docker: + - image: <> + resource_class: small + steps: + - utils/checkout-with-mise: + checkout-method: blobless + enable-mise-cache: true + - run: + name: check op-geth version + command: | + make check-op-geth-version + go-tests: parameters: notify: @@ -2824,6 +2837,9 @@ workflows: - go-lint: context: - circleci-repo-readonly-authenticated-github-token + - check-op-geth-version: + context: + - circleci-repo-readonly-authenticated-github-token - fuzz-golang: name: fuzz-golang-<> on_changes: <> diff --git a/Makefile b/Makefile index a0ada6a77d8a9..403f0e7a3f407 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,10 @@ lint-go-fix: ## Lints Go code with specific linters and fixes reported issues golangci-lint run ./... --fix .PHONY: lint-go-fix +check-op-geth-version: ## Checks that op-geth version in go.mod is valid + go run ./ops/scripts/check-op-geth-version +.PHONY: check-op-geth-version + golang-docker: ## Builds Docker images for Go components using buildx # We don't use a buildx builder here, and just load directly into regular docker, for convenience. GIT_COMMIT=$$(git rev-parse HEAD) \ diff --git a/op-service/util.go b/op-service/util.go index 84d8e0d864c40..0c0ec79e09d1a 100644 --- a/op-service/util.go +++ b/op-service/util.go @@ -13,6 +13,7 @@ import ( "time" "github.com/ethereum/go-ethereum/common/hexutil" + "golang.org/x/mod/modfile" "github.com/ethereum-optimism/optimism/op-service/ctxinterrupt" @@ -115,17 +116,23 @@ func CloseAction(ctx context.Context, fn func(ctx context.Context) error) error } } -// FindMonorepoRoot will recursively search upwards for a go.mod file. -// This depends on the structure of the monorepo having a go.mod file at the root. +// FindMonorepoRoot will recursively search upwards for the monorepo's go.mod file. +// It verifies that the go.mod file belongs to the optimism monorepo by checking the module path. func FindMonorepoRoot(startDir string) (string, error) { dir, err := filepath.Abs(startDir) if err != nil { return "", err } for { - modulePath := filepath.Join(dir, "go.mod") - if _, err := os.Stat(modulePath); err == nil { - return dir, nil + goModPath := filepath.Join(dir, "go.mod") + if content, err := os.ReadFile(goModPath); err == nil { + modFile, err := modfile.Parse(goModPath, content, nil) + if err != nil { + return "", fmt.Errorf("parsing go.mod: %w", err) + } + if modFile.Module.Mod.Path == "github.com/ethereum-optimism/optimism" { + return dir, nil + } } parentDir := filepath.Dir(dir) // Check if we reached the filesystem root diff --git a/ops/scripts/check-op-geth-version/main.go b/ops/scripts/check-op-geth-version/main.go new file mode 100644 index 0000000000000..6e253762ad401 --- /dev/null +++ b/ops/scripts/check-op-geth-version/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + + "golang.org/x/mod/modfile" + + opservice "github.com/ethereum-optimism/optimism/op-service" +) + +const ( + goEthereumPath = "github.com/ethereum/go-ethereum" + opGethPath = "github.com/ethereum-optimism/op-geth" +) + +// The op-geth minor version encodes the upstream geth version as exactly 6 digits: +// - 2 digits for geth major version (zero-padded) +// - 2 digits for geth minor version (zero-padded) +// - 2 digits for geth patch version (zero-padded) +// +// Examples: +// - v1.101407.0 -> geth v1.14.7 +// - v1.101605.0-rc.2 -> geth v1.16.5 +var opGethVersionPattern = regexp.MustCompile(`^v\d+\.\d{6}\.\d+(-rc\.\d+)?$`) + +func main() { + if err := run("."); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} + +func run(dir string) error { + root, err := opservice.FindMonorepoRoot(dir) + if err != nil { + return fmt.Errorf("finding monorepo root: %w", err) + } + + goModPath := filepath.Join(root, "go.mod") + content, err := os.ReadFile(goModPath) + if err != nil { + return fmt.Errorf("reading go.mod: %w", err) + } + + modFile, err := modfile.Parse(goModPath, content, nil) + if err != nil { + return fmt.Errorf("parsing go.mod: %w", err) + } + + // Find the replace directive for go-ethereum -> op-geth + var opGethVersion string + for _, rep := range modFile.Replace { + if rep.Old.Path == goEthereumPath { + if rep.New.Path != opGethPath { + return fmt.Errorf("go-ethereum replacement must point to %s, got %s", opGethPath, rep.New.Path) + } + opGethVersion = rep.New.Version + break + } + } + + if opGethVersion == "" { + return fmt.Errorf("no replace directive found for %s", goEthereumPath) + } + + if !opGethVersionPattern.MatchString(opGethVersion) { + return fmt.Errorf("invalid op-geth version %q", opGethVersion) + } + + return nil +} diff --git a/ops/scripts/check-op-geth-version/main_test.go b/ops/scripts/check-op-geth-version/main_test.go new file mode 100644 index 0000000000000..de4b25115401e --- /dev/null +++ b/ops/scripts/check-op-geth-version/main_test.go @@ -0,0 +1,31 @@ +package main + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRun(t *testing.T) { + tests := map[string]string{ + "valid": "", + "valid-rc": "", + "invalid-version": "invalid op-geth version", + "invalid-geth-encoding": "invalid op-geth version", + "wrong-replacement": "must point to github.com/ethereum-optimism/op-geth", + "no-replacement": "no replace directive found", + } + for name, errContains := range tests { + t.Run(name, func(t *testing.T) { + testDir, err := filepath.Abs(filepath.Join("testdata", name)) + require.NoError(t, err) + if err = run(testDir); errContains == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + require.ErrorContains(t, err, errContains) + } + }) + } +} diff --git a/ops/scripts/check-op-geth-version/testdata/invalid-geth-encoding/go.mod b/ops/scripts/check-op-geth-version/testdata/invalid-geth-encoding/go.mod new file mode 100644 index 0000000000000..0967eda22dda0 --- /dev/null +++ b/ops/scripts/check-op-geth-version/testdata/invalid-geth-encoding/go.mod @@ -0,0 +1,5 @@ +module github.com/ethereum-optimism/optimism + +go 1.22.0 + +replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.11407.0 diff --git a/ops/scripts/check-op-geth-version/testdata/invalid-version/go.mod b/ops/scripts/check-op-geth-version/testdata/invalid-version/go.mod new file mode 100644 index 0000000000000..53c3f6cc6cfa0 --- /dev/null +++ b/ops/scripts/check-op-geth-version/testdata/invalid-version/go.mod @@ -0,0 +1,5 @@ +module github.com/ethereum-optimism/optimism + +go 1.22.0 + +replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101605.0-beta.1 diff --git a/ops/scripts/check-op-geth-version/testdata/no-replacement/go.mod b/ops/scripts/check-op-geth-version/testdata/no-replacement/go.mod new file mode 100644 index 0000000000000..e4ccc4695c4ed --- /dev/null +++ b/ops/scripts/check-op-geth-version/testdata/no-replacement/go.mod @@ -0,0 +1,3 @@ +module github.com/ethereum-optimism/optimism + +go 1.22.0 diff --git a/ops/scripts/check-op-geth-version/testdata/pseudoversion/go.mod b/ops/scripts/check-op-geth-version/testdata/pseudoversion/go.mod new file mode 100644 index 0000000000000..9edbf55c6a6b6 --- /dev/null +++ b/ops/scripts/check-op-geth-version/testdata/pseudoversion/go.mod @@ -0,0 +1,5 @@ +module github.com/ethereum-optimism/optimism + +go 1.22.0 + +replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101605.1-0.20260113212524-7f3b615ab10d diff --git a/ops/scripts/check-op-geth-version/testdata/valid-rc/go.mod b/ops/scripts/check-op-geth-version/testdata/valid-rc/go.mod new file mode 100644 index 0000000000000..b75fce3ea7cbe --- /dev/null +++ b/ops/scripts/check-op-geth-version/testdata/valid-rc/go.mod @@ -0,0 +1,5 @@ +module github.com/ethereum-optimism/optimism + +go 1.22.0 + +replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101605.0-rc.2 diff --git a/ops/scripts/check-op-geth-version/testdata/valid/go.mod b/ops/scripts/check-op-geth-version/testdata/valid/go.mod new file mode 100644 index 0000000000000..5b4abab3f0c4f --- /dev/null +++ b/ops/scripts/check-op-geth-version/testdata/valid/go.mod @@ -0,0 +1,5 @@ +module github.com/ethereum-optimism/optimism + +go 1.22.0 + +replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101605.0 diff --git a/ops/scripts/check-op-geth-version/testdata/wrong-replacement/go.mod b/ops/scripts/check-op-geth-version/testdata/wrong-replacement/go.mod new file mode 100644 index 0000000000000..ffd3752f63314 --- /dev/null +++ b/ops/scripts/check-op-geth-version/testdata/wrong-replacement/go.mod @@ -0,0 +1,5 @@ +module github.com/ethereum-optimism/optimism + +go 1.22.0 + +replace github.com/ethereum/go-ethereum => github.com/some-other/fork v1.0.0