Commit aa81f074 authored by Michael Amadi's avatar Michael Amadi Committed by GitHub

transition semver-lock script to common framework (#13399)

parent 0d0f69d5
......@@ -5,124 +5,112 @@ import (
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/scripts/checks/common"
"github.com/ethereum/go-ethereum/crypto"
)
const semverLockFile = "snapshots/semver-lock.json"
func main() {
if err := run(); err != nil {
panic(err)
}
type SemverLockOutput struct {
InitCodeHash string `json:"initCodeHash"`
SourceCodeHash string `json:"sourceCodeHash"`
}
type SemverLockResult struct {
SemverLockOutput
SourceFilePath string
}
func run() error {
// Find semver files
// Execute grep command to find files with @custom:semver
var cmd = exec.Command("bash", "-c", "grep -rl '@custom:semver' src | jq -Rs 'split(\"\n\") | map(select(length > 0))'")
cmdOutput, err := cmd.Output()
func main() {
results, err := common.ProcessFilesGlob(
[]string{"forge-artifacts/**/*.json"},
[]string{},
processFile,
)
if err != nil {
return err
fmt.Printf("error: %v\n", err)
os.Exit(1)
}
// Parse the JSON array of files
var files []string
if err := json.Unmarshal(cmdOutput, &files); err != nil {
return fmt.Errorf("failed to parse JSON output: %w", err)
// Create the output map
output := make(map[string]SemverLockOutput)
for _, result := range results {
if result == nil {
continue
}
output[result.SourceFilePath] = result.SemverLockOutput
}
// Hash and write to JSON file
// Map to store our JSON output
output := make(map[string]map[string]string)
// Get and sort the keys
keys := make([]string, 0, len(output))
for k := range output {
keys = append(keys, k)
}
sort.Strings(keys)
// regex to extract contract name from file path
re := regexp.MustCompile(`src/.*/(.+)\.sol`)
// Create a sorted map for output
sortedOutput := make(map[string]SemverLockOutput)
for _, k := range keys {
sortedOutput[k] = output[k]
}
// Get artifacts directory
cmd = exec.Command("forge", "config", "--json")
out, err := cmd.Output()
// Write to JSON file
jsonData, err := json.MarshalIndent(sortedOutput, "", " ")
if err != nil {
return fmt.Errorf("failed to get forge config: %w", err)
}
var config struct {
Out string `json:"out"`
panic(err)
}
if err := json.Unmarshal(out, &config); err != nil {
return fmt.Errorf("failed to parse forge config: %w", err)
if err := os.WriteFile(semverLockFile, jsonData, 0644); err != nil {
panic(err)
}
for _, file := range files {
// Read file contents
fileContents, err := os.ReadFile(file)
fmt.Printf("Wrote semver lock file to \"%s\".\n", semverLockFile)
}
func processFile(file string) (*SemverLockResult, []error) {
artifact, err := common.ReadForgeArtifact(file)
if err != nil {
return fmt.Errorf("failed to read file %s: %w", file, err)
return nil, []error{fmt.Errorf("failed to read artifact: %w", err)}
}
// Extract contract name from file path using regex
matches := re.FindStringSubmatch(file)
if len(matches) < 2 {
return fmt.Errorf("invalid file path format: %s", file)
// Only apply to files in the src directory.
sourceFilePath := artifact.Ast.AbsolutePath
if !strings.HasPrefix(sourceFilePath, "src/") {
return nil, nil
}
contractName := matches[1]
// Get artifact files
artifactDir := filepath.Join(config.Out, contractName+".sol")
files, err := os.ReadDir(artifactDir)
if err != nil {
return fmt.Errorf("failed to read artifact directory: %w", err)
}
if len(files) == 0 {
return fmt.Errorf("no artifacts found for %s", contractName)
// Check if the contract uses semver.
semverRegex := regexp.MustCompile(`custom:semver`)
semver := semverRegex.FindStringSubmatch(artifact.RawMetadata)
if len(semver) == 0 {
return nil, nil
}
// Read initcode from artifact
artifactPath := filepath.Join(artifactDir, files[0].Name())
artifact, err := os.ReadFile(artifactPath)
// Extract the init code from the artifact.
initCodeBytes, err := hex.DecodeString(strings.TrimPrefix(artifact.Bytecode.Object, "0x"))
if err != nil {
return fmt.Errorf("failed to read initcode: %w", err)
}
artifactJson := json.RawMessage(artifact)
var artifactObj struct {
Bytecode struct {
Object string `json:"object"`
} `json:"bytecode"`
}
if err := json.Unmarshal(artifactJson, &artifactObj); err != nil {
return fmt.Errorf("failed to parse artifact: %w", err)
return nil, []error{fmt.Errorf("failed to decode hex: %w", err)}
}
// convert the hex bytecode to a uint8 array / bytes
initCodeBytes, err := hex.DecodeString(strings.TrimPrefix(artifactObj.Bytecode.Object, "0x"))
// Extract the source contents from the AST.
sourceCode, err := os.ReadFile(sourceFilePath)
if err != nil {
return fmt.Errorf("failed to decode hex: %w", err)
return nil, []error{fmt.Errorf("failed to read source file: %w", err)}
}
// Calculate hashes using Keccak256
var sourceCode = []byte(strings.TrimSuffix(string(fileContents), "\n"))
trimmedSourceCode := []byte(strings.TrimSuffix(string(sourceCode), "\n"))
initCodeHash := fmt.Sprintf("0x%x", crypto.Keccak256Hash(initCodeBytes))
sourceCodeHash := fmt.Sprintf("0x%x", crypto.Keccak256Hash(sourceCode))
// Store in output map
output[file] = map[string]string{
"initCodeHash": initCodeHash,
"sourceCodeHash": sourceCodeHash,
}
}
// Write to JSON file
jsonData, err := json.MarshalIndent(output, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal JSON: %w", err)
}
if err := os.WriteFile(semverLockFile, jsonData, 0644); err != nil {
return fmt.Errorf("failed to write semver lock file: %w", err)
}
fmt.Printf("Wrote semver lock file to \"%s\".\n", semverLockFile)
return nil
sourceCodeHash := fmt.Sprintf("0x%x", crypto.Keccak256Hash(trimmedSourceCode))
return &SemverLockResult{
SourceFilePath: sourceFilePath,
SemverLockOutput: SemverLockOutput{
InitCodeHash: initCodeHash,
SourceCodeHash: sourceCodeHash,
},
}, nil
}
......@@ -37,20 +37,27 @@ func (e *ErrorReporter) HasError() bool {
return e.hasErr.Load()
}
type FileProcessor func(path string) []error
type Void struct{}
func ProcessFiles(files map[string]string, processor FileProcessor) error {
type FileProcessor[T any] func(path string) (T, []error)
func ProcessFiles[T any](files map[string]string, processor FileProcessor[T]) (map[string]T, error) {
g := errgroup.Group{}
g.SetLimit(runtime.NumCPU())
reporter := NewErrorReporter()
results := sync.Map{}
for name, path := range files {
name, path := name, path // Capture loop variables
g.Go(func() error {
if errs := processor(path); len(errs) > 0 {
result, errs := processor(path)
if len(errs) > 0 {
for _, err := range errs {
reporter.Fail("%s: %v", name, err)
}
} else {
results.Store(path, result)
}
return nil
})
......@@ -58,18 +65,26 @@ func ProcessFiles(files map[string]string, processor FileProcessor) error {
err := g.Wait()
if err != nil {
return fmt.Errorf("processing failed: %w", err)
return nil, fmt.Errorf("processing failed: %w", err)
}
if reporter.HasError() {
return fmt.Errorf("processing failed")
return nil, fmt.Errorf("processing failed")
}
return nil
// Convert sync.Map to regular map
finalResults := make(map[string]T)
results.Range(func(key, value interface{}) bool {
finalResults[key.(string)] = value.(T)
return true
})
return finalResults, nil
}
func ProcessFilesGlob(includes, excludes []string, processor FileProcessor) error {
func ProcessFilesGlob[T any](includes, excludes []string, processor FileProcessor[T]) (map[string]T, error) {
files, err := FindFiles(includes, excludes)
if err != nil {
return err
return nil, err
}
return ProcessFiles(files, processor)
}
......
package common
import (
"fmt"
"os"
"path/filepath"
"sync"
......@@ -33,23 +34,55 @@ func TestProcessFiles(t *testing.T) {
"file2": "path2",
}
// Test successful processing
err := ProcessFiles(files, func(path string) []error {
return nil
// Test void processing (no results)
_, err := ProcessFiles(files, func(path string) (*Void, []error) {
return nil, nil
})
if err != nil {
t.Errorf("expected no error, got %v", err)
}
// Test error handling
err = ProcessFiles(files, func(path string) []error {
_, err = ProcessFiles(files, func(path string) (*Void, []error) {
var errors []error
errors = append(errors, os.ErrNotExist)
return errors
return nil, errors
})
if err == nil {
t.Error("expected error, got nil")
}
// Test successful processing with string results
results, err := ProcessFiles(files, func(path string) (string, []error) {
return "processed_" + path, nil
})
if err != nil {
t.Errorf("expected no error, got %v", err)
}
if len(results) != 2 {
t.Errorf("expected 2 results, got %d", len(results))
}
if results["path1"] != "processed_path1" {
t.Errorf("expected processed_path1, got %s", results["path1"])
}
// Test processing with struct results
type testResult struct {
Path string
Counter int
}
structResults, err := ProcessFiles(files, func(path string) (testResult, []error) {
return testResult{Path: path, Counter: len(path)}, nil
})
if err != nil {
t.Errorf("expected no error, got %v", err)
}
if len(structResults) != 2 {
t.Errorf("expected 2 results, got %d", len(structResults))
}
if structResults["path1"].Counter != 5 {
t.Errorf("expected counter 5, got %d", structResults["path1"].Counter)
}
}
func TestProcessFilesGlob(t *testing.T) {
......@@ -75,24 +108,24 @@ func TestProcessFilesGlob(t *testing.T) {
}
}
// Test processing with includes and excludes
includes := []string{"*.txt"}
excludes := []string{"skip.txt"}
// Test void processing (no results)
processedFiles := make(map[string]bool)
var mtx sync.Mutex
err := ProcessFilesGlob(includes, excludes, func(path string) []error {
_, err := ProcessFilesGlob(includes, excludes, func(path string) (*Void, []error) {
mtx.Lock()
processedFiles[filepath.Base(path)] = true
mtx.Unlock()
return nil
return nil, nil
})
if err != nil {
t.Errorf("ProcessFiles failed: %v", err)
}
// Verify results
// Verify void processing results
if len(processedFiles) != 2 {
t.Errorf("expected 2 processed files, got %d", len(processedFiles))
}
......@@ -105,6 +138,54 @@ func TestProcessFilesGlob(t *testing.T) {
if processedFiles["skip.txt"] {
t.Error("skip.txt should have been excluded")
}
// Test processing with struct results
type fileInfo struct {
Size int64
Content string
}
results, err := ProcessFilesGlob(includes, excludes, func(path string) (fileInfo, []error) {
content, err := os.ReadFile(path)
if err != nil {
return fileInfo{}, []error{err}
}
info, err := os.Stat(path)
if err != nil {
return fileInfo{}, []error{err}
}
return fileInfo{
Size: info.Size(),
Content: string(content),
}, nil
})
if err != nil {
t.Errorf("ProcessFilesGlob failed: %v", err)
}
// Verify struct results
if len(results) != 2 {
t.Errorf("expected 2 results, got %d", len(results))
}
if result, exists := results["test1.txt"]; !exists {
t.Error("expected result for test1.txt")
} else {
if result.Content != "content1" {
t.Errorf("expected content1, got %s", result.Content)
}
if result.Size != 8 {
t.Errorf("expected size 8, got %d", result.Size)
}
}
// Test error handling
_, err = ProcessFilesGlob(includes, excludes, func(path string) (fileInfo, []error) {
return fileInfo{}, []error{fmt.Errorf("test error")}
})
if err == nil {
t.Error("expected error, got nil")
}
}
func TestFindFiles(t *testing.T) {
......
......@@ -50,7 +50,7 @@ type Artifact struct {
}
func main() {
if err := common.ProcessFilesGlob(
if _, err := common.ProcessFilesGlob(
[]string{"forge-artifacts/**/*.json"},
[]string{},
processFile,
......@@ -60,56 +60,56 @@ func main() {
}
}
func processFile(artifactPath string) []error {
func processFile(artifactPath string) (*common.Void, []error) {
cwd, err := os.Getwd()
if err != nil {
return []error{fmt.Errorf("failed to get current working directory: %w", err)}
return nil, []error{fmt.Errorf("failed to get current working directory: %w", err)}
}
artifactsDir := filepath.Join(cwd, "forge-artifacts")
contractName := strings.Split(filepath.Base(artifactPath), ".")[0]
if isExcluded(contractName) {
return nil
return nil, nil
}
artifact, err := readArtifact(artifactPath)
if err != nil {
return []error{fmt.Errorf("failed to read artifact: %w", err)}
return nil, []error{fmt.Errorf("failed to read artifact: %w", err)}
}
contractDef := getContractDefinition(artifact, contractName)
if contractDef == nil {
return nil // Skip processing if contract definition is not found
return nil, nil // Skip processing if contract definition is not found
}
if contractDef.ContractKind != "interface" {
return nil
return nil, nil
}
if !strings.HasPrefix(contractName, "I") {
return []error{fmt.Errorf("%s: Interface does not start with 'I'", contractName)}
return nil, []error{fmt.Errorf("%s: Interface does not start with 'I'", contractName)}
}
semver, err := getContractSemver(artifact)
if err != nil {
return []error{fmt.Errorf("failed to get contract semver: %w", err)}
return nil, []error{fmt.Errorf("failed to get contract semver: %w", err)}
}
if semver != "solidity^0.8.0" {
return []error{fmt.Errorf("%s: Interface does not have correct compiler version (MUST be exactly solidity ^0.8.0)", contractName)}
return nil, []error{fmt.Errorf("%s: Interface does not have correct compiler version (MUST be exactly solidity ^0.8.0)", contractName)}
}
contractBasename := contractName[1:]
correspondingContractFile := filepath.Join(artifactsDir, contractBasename+".sol", contractBasename+".json")
if _, err := os.Stat(correspondingContractFile); errors.Is(err, os.ErrNotExist) {
return nil
return nil, nil
}
contractArtifact, err := readArtifact(correspondingContractFile)
if err != nil {
return []error{fmt.Errorf("failed to read corresponding contract artifact: %w", err)}
return nil, []error{fmt.Errorf("failed to read corresponding contract artifact: %w", err)}
}
interfaceABI := artifact.ABI
......@@ -117,23 +117,23 @@ func processFile(artifactPath string) []error {
normalizedInterfaceABI, err := normalizeABI(interfaceABI)
if err != nil {
return []error{fmt.Errorf("failed to normalize interface ABI: %w", err)}
return nil, []error{fmt.Errorf("failed to normalize interface ABI: %w", err)}
}
normalizedContractABI, err := normalizeABI(contractABI)
if err != nil {
return []error{fmt.Errorf("failed to normalize contract ABI: %w", err)}
return nil, []error{fmt.Errorf("failed to normalize contract ABI: %w", err)}
}
match, err := compareABIs(normalizedInterfaceABI, normalizedContractABI)
if err != nil {
return []error{fmt.Errorf("failed to compare ABIs: %w", err)}
return nil, []error{fmt.Errorf("failed to compare ABIs: %w", err)}
}
if !match {
return []error{fmt.Errorf("%s: Differences found in ABI between interface and actual contract", contractName)}
return nil, []error{fmt.Errorf("%s: Differences found in ABI between interface and actual contract", contractName)}
}
return nil
return nil, nil
}
func readArtifact(path string) (*Artifact, error) {
......
......@@ -89,14 +89,14 @@ func validateSpacer(variable solc.StorageLayoutEntry, types map[string]solc.Stor
return errors
}
func processFile(path string) []error {
func processFile(path string) (*common.Void, []error) {
artifact, err := common.ReadForgeArtifact(path)
if err != nil {
return []error{err}
return nil, []error{err}
}
if artifact.StorageLayout == nil {
return nil
return nil, nil
}
var errors []error
......@@ -109,11 +109,11 @@ func processFile(path string) []error {
}
}
return errors
return nil, errors
}
func main() {
if err := common.ProcessFilesGlob(
if _, err := common.ProcessFilesGlob(
[]string{"forge-artifacts/**/*.json"},
[]string{"forge-artifacts/**/CrossDomainMessengerLegacySpacer{0,1}.json"},
processFile,
......
......@@ -12,7 +12,7 @@ import (
)
func main() {
if err := common.ProcessFilesGlob(
if _, err := common.ProcessFilesGlob(
[]string{"forge-artifacts/**/*.json"},
[]string{},
processFile,
......@@ -22,10 +22,10 @@ func main() {
}
}
func processFile(path string) []error {
func processFile(path string) (*common.Void, []error) {
artifact, err := common.ReadForgeArtifact(path)
if err != nil {
return []error{err}
return nil, []error{err}
}
var errors []error
......@@ -36,7 +36,7 @@ func processFile(path string) []error {
}
}
return errors
return nil, errors
}
func extractTestNames(artifact *solc.ForgeArtifact) []string {
......
......@@ -14,7 +14,7 @@ var importPattern = regexp.MustCompile(`import\s*{([^}]+)}`)
var asPattern = regexp.MustCompile(`(\S+)\s+as\s+(\S+)`)
func main() {
if err := common.ProcessFilesGlob(
if _, err := common.ProcessFilesGlob(
[]string{"src/**/*.sol", "scripts/**/*.sol", "test/**/*.sol"},
[]string{},
processFile,
......@@ -24,10 +24,10 @@ func main() {
}
}
func processFile(filePath string) []error {
func processFile(filePath string) (*common.Void, []error) {
content, err := os.ReadFile(filePath)
if err != nil {
return []error{fmt.Errorf("%s: failed to read file: %w", filePath, err)}
return nil, []error{fmt.Errorf("%s: failed to read file: %w", filePath, err)}
}
imports := findImports(string(content))
......@@ -43,10 +43,10 @@ func processFile(filePath string) []error {
for _, unused := range unusedImports {
errors = append(errors, fmt.Errorf("%s", unused))
}
return errors
return nil, errors
}
return nil
return nil, nil
}
func findImports(content string) []string {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment