Commit 26f508cc authored by Michael Amadi's avatar Michael Amadi Committed by GitHub

transition snapshots script to common framework (#13398)

parent b62e7740
package solc package solc
import ( import (
"encoding/json"
"fmt" "fmt"
"github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi"
) )
type AbiType struct {
Parsed abi.ABI
Raw interface{}
}
func (a *AbiType) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &a.Raw); err != nil {
return err
}
return json.Unmarshal(data, &a.Parsed)
}
type CompilerInput struct { type CompilerInput struct {
Language string `json:"language"` Language string `json:"language"`
Sources map[string]map[string]string `json:"sources"` Sources map[string]map[string]string `json:"sources"`
...@@ -39,7 +52,7 @@ type CompilerOutputContracts map[string]CompilerOutputContract ...@@ -39,7 +52,7 @@ type CompilerOutputContracts map[string]CompilerOutputContract
// CompilerOutputContract represents the solc compiler output for a contract. // CompilerOutputContract represents the solc compiler output for a contract.
// Ignoring some fields such as devdoc and userdoc. // Ignoring some fields such as devdoc and userdoc.
type CompilerOutputContract struct { type CompilerOutputContract struct {
Abi abi.ABI `json:"abi"` Abi AbiType `json:"abi"`
Evm CompilerOutputEvm `json:"evm"` Evm CompilerOutputEvm `json:"evm"`
Metadata string `json:"metadata"` Metadata string `json:"metadata"`
StorageLayout StorageLayout `json:"storageLayout"` StorageLayout StorageLayout `json:"storageLayout"`
...@@ -72,6 +85,14 @@ func (s *StorageLayout) GetStorageLayoutType(name string) (StorageLayoutType, er ...@@ -72,6 +85,14 @@ func (s *StorageLayout) GetStorageLayoutType(name string) (StorageLayoutType, er
return StorageLayoutType{}, fmt.Errorf("%s not found", name) return StorageLayoutType{}, fmt.Errorf("%s not found", name)
} }
type AbiSpecStorageLayoutEntry struct {
Bytes uint `json:"bytes,string"`
Label string `json:"label"`
Offset uint `json:"offset"`
Slot uint `json:"slot,string"`
Type string `json:"type"`
}
type StorageLayoutEntry struct { type StorageLayoutEntry struct {
AstId uint `json:"astId"` AstId uint `json:"astId"`
Contract string `json:"contract"` Contract string `json:"contract"`
...@@ -241,7 +262,7 @@ type Expression struct { ...@@ -241,7 +262,7 @@ type Expression struct {
} }
type ForgeArtifact struct { type ForgeArtifact struct {
Abi abi.ABI `json:"abi"` Abi AbiType `json:"abi"`
Bytecode CompilerOutputBytecode `json:"bytecode"` Bytecode CompilerOutputBytecode `json:"bytecode"`
DeployedBytecode CompilerOutputBytecode `json:"deployedBytecode"` DeployedBytecode CompilerOutputBytecode `json:"deployedBytecode"`
MethodIdentifiers map[string]string `json:"methodIdentifiers"` MethodIdentifiers map[string]string `json:"methodIdentifiers"`
...@@ -266,7 +287,7 @@ type ForgeCompilerInfo struct { ...@@ -266,7 +287,7 @@ type ForgeCompilerInfo struct {
} }
type ForgeMetadataOutput struct { type ForgeMetadataOutput struct {
Abi abi.ABI `json:"abi"` Abi AbiType `json:"abi"`
DevDoc ForgeDocObject `json:"devdoc"` DevDoc ForgeDocObject `json:"devdoc"`
UserDoc ForgeDocObject `json:"userdoc"` UserDoc ForgeDocObject `json:"userdoc"`
} }
......
package main package main
import ( import (
"bytes"
"encoding/json"
"flag"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort" "strings"
"github.com/ethereum-optimism/optimism/op-chain-ops/solc"
"github.com/ethereum-optimism/optimism/packages/contracts-bedrock/scripts/checks/common"
) )
type ForgeArtifact struct { const (
// ABI is a nested JSON data structure, including some objects/maps. storageLayoutDir = "snapshots/storageLayout"
// We declare it as interface, and not raw-message, such that Go decodes into map[string]interface{} abiDir = "snapshots/abi"
// where possible. The JSON-encoder will then sort the keys (default Go JSON behavior on maps), )
// to reproduce the sortKeys(abi) result of the legacy Typescript version of the snapshort-generator.
ABI interface{} `json:"abi"`
Ast *struct {
NodeType string `json:"nodeType"`
Nodes []struct {
NodeType string `json:"nodeType"`
Name string `json:"name"`
ContractKind string `json:"contractKind"`
Abstract bool `json:"abstract"`
} `json:"nodes"`
} `json:"ast"`
StorageLayout struct {
Storage []struct {
Type string `json:"type"`
Label json.RawMessage `json:"label"`
Offset json.RawMessage `json:"offset"`
Slot json.RawMessage `json:"slot"`
} `json:"storage"`
Types map[string]struct {
Label string `json:"label"`
NumberOfBytes json.RawMessage `json:"numberOfBytes"`
} `json:"types"`
} `json:"storageLayout"`
Bytecode struct {
Object string `json:"object"`
} `json:"bytecode"`
}
type AbiSpecStorageLayoutEntry struct { type SnapshotResult struct {
Bytes json.RawMessage `json:"bytes"` ContractName string
Label json.RawMessage `json:"label"` Abi interface{}
Offset json.RawMessage `json:"offset"` StorageLayout []solc.AbiSpecStorageLayoutEntry
Slot json.RawMessage `json:"slot"`
Type string `json:"type"`
} }
func main() { func main() {
flag.Parse() if err := resetDirectory(storageLayoutDir); err != nil {
if flag.NArg() != 1 { fmt.Printf("failed to reset storage layout directory: %v\n", err)
fmt.Println("Expected path of contracts-bedrock as CLI argument")
os.Exit(1) os.Exit(1)
} }
rootDir := flag.Arg(0) if err := resetDirectory(abiDir); err != nil {
err := generateSnapshots(rootDir) fmt.Printf("failed to reset abi directory: %v\n", err)
os.Exit(1)
}
results, err := common.ProcessFilesGlob(
[]string{"forge-artifacts/**/*.json"},
[]string{},
processFile,
)
if err != nil { if err != nil {
fmt.Printf("Failed to generate snapshots: %v\n", err) fmt.Printf("Failed to generate snapshots: %v\n", err)
os.Exit(1) os.Exit(1)
} }
}
func generateSnapshots(rootDir string) error { for _, result := range results {
if result == nil {
forgeArtifactsDir := filepath.Join(rootDir, "forge-artifacts") continue
srcDir := filepath.Join(rootDir, "src")
outDir := filepath.Join(rootDir, "snapshots")
storageLayoutDir := filepath.Join(outDir, "storageLayout")
abiDir := filepath.Join(outDir, "abi")
fmt.Printf("writing abi and storage layout snapshots to %s\n", outDir)
// Clean and recreate directories
if err := os.RemoveAll(storageLayoutDir); err != nil {
return fmt.Errorf("failed to remove storage layout dir: %w", err)
}
if err := os.RemoveAll(abiDir); err != nil {
return fmt.Errorf("failed to remove ABI dir: %w", err)
}
if err := os.MkdirAll(storageLayoutDir, os.ModePerm); err != nil {
return fmt.Errorf("failed to create storage layout dir: %w", err)
}
if err := os.MkdirAll(abiDir, os.ModePerm); err != nil {
return fmt.Errorf("failed to create ABI dir: %w", err)
} }
contractSources, err := getAllContractsSources(srcDir) err := common.WriteJSON(result.Abi, filepath.Join(abiDir, fmt.Sprintf("%s.json", result.ContractName)))
if err != nil { if err != nil {
return fmt.Errorf("failed to retrieve contract sources: %w", err) fmt.Printf("failed to write abi: %v\n", err)
os.Exit(1)
} }
knownAbis := make(map[string]interface{}) err = common.WriteJSON(result.StorageLayout, filepath.Join(storageLayoutDir, fmt.Sprintf("%s.json", result.ContractName)))
for _, contractFile := range contractSources {
contractArtifacts := filepath.Join(forgeArtifactsDir, contractFile)
files, err := os.ReadDir(contractArtifacts)
if err != nil { if err != nil {
return fmt.Errorf("failed to scan contract artifacts of %q: %w", contractFile, err) fmt.Printf("failed to write storage layout: %v\n", err)
os.Exit(1)
}
} }
}
for _, file := range files { func processFile(file string) (*SnapshotResult, []error) {
artifactPath := filepath.Join(contractArtifacts, file.Name()) artifact, err := common.ReadForgeArtifact(file)
data, err := os.ReadFile(artifactPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to read artifact %q: %w", artifactPath, err) return nil, []error{err}
}
var artifact ForgeArtifact
if err := json.Unmarshal(data, &artifact); err != nil {
return fmt.Errorf("failed to decode artifact %q: %w", artifactPath, err)
} }
contractName, err := parseArtifactName(file.Name()) contractName, err := parseArtifactName(file)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse artifact name %q: %w", file.Name(), err) return nil, []error{fmt.Errorf("failed to parse artifact name %q: %w", file, err)}
} }
// HACK: This is a hack to ignore libraries and abstract contracts. Not robust against changes to solc's internal ast repr // Skip anything that isn't in the src directory.
if artifact.Ast == nil { if !strings.HasPrefix(artifact.Ast.AbsolutePath, "src/") {
return fmt.Errorf("ast isn't present in forge-artifacts. Did you run forge build with `--ast`? Artifact: %s", artifactPath) return nil, nil
} }
// Check if the artifact is a contract
// Skip anything that isn't a proper contract.
isContract := false isContract := false
for _, node := range artifact.Ast.Nodes { for _, node := range artifact.Ast.Nodes {
if node.NodeType == "ContractDefinition" && if node.NodeType == "ContractDefinition" &&
...@@ -136,18 +89,19 @@ func generateSnapshots(rootDir string) error { ...@@ -136,18 +89,19 @@ func generateSnapshots(rootDir string) error {
} }
} }
if !isContract { if !isContract {
fmt.Printf("ignoring library/interface %s\n", contractName) return nil, nil
continue
} }
storageLayout := make([]AbiSpecStorageLayoutEntry, 0, len(artifact.StorageLayout.Storage)) storageLayout := make([]solc.AbiSpecStorageLayoutEntry, 0, len(artifact.StorageLayout.Storage))
for _, storageEntry := range artifact.StorageLayout.Storage { for _, storageEntry := range artifact.StorageLayout.Storage {
// convert ast-based type to solidity type // Convert ast-based type to Solidity type.
typ, ok := artifact.StorageLayout.Types[storageEntry.Type] typ, ok := artifact.StorageLayout.Types[storageEntry.Type]
if !ok { if !ok {
return fmt.Errorf("undefined type for %s:%s", contractName, storageEntry.Label) return nil, []error{fmt.Errorf("undefined type for %s:%s", contractName, storageEntry.Label)}
} }
storageLayout = append(storageLayout, AbiSpecStorageLayoutEntry{
// Convert to Solidity storage layout entry.
storageLayout = append(storageLayout, solc.AbiSpecStorageLayoutEntry{
Label: storageEntry.Label, Label: storageEntry.Label,
Bytes: typ.NumberOfBytes, Bytes: typ.NumberOfBytes,
Offset: storageEntry.Offset, Offset: storageEntry.Offset,
...@@ -156,96 +110,31 @@ func generateSnapshots(rootDir string) error { ...@@ -156,96 +110,31 @@ func generateSnapshots(rootDir string) error {
}) })
} }
if existingAbi, exists := knownAbis[contractName]; exists { return &SnapshotResult{
if !jsonEqual(existingAbi, artifact.ABI) { ContractName: contractName,
return fmt.Errorf("detected multiple artifact versions with different ABIs for %s", contractFile) Abi: artifact.Abi.Raw,
} else { StorageLayout: storageLayout,
fmt.Printf("detected multiple artifacts for %s\n", contractName) }, nil
}
} else {
knownAbis[contractName] = artifact.ABI
}
// Sort and write snapshots
if err := writeJSON(filepath.Join(abiDir, contractName+".json"), artifact.ABI); err != nil {
return fmt.Errorf("failed to write ABI snapshot JSON of %q: %w", contractName, err)
}
if err := writeJSON(filepath.Join(storageLayoutDir, contractName+".json"), storageLayout); err != nil {
return fmt.Errorf("failed to write storage layout snapshot JSON of %q: %w", contractName, err)
}
}
}
return nil
}
func getAllContractsSources(srcDir string) ([]string, error) {
var paths []string
if err := readFilesRecursively(srcDir, &paths); err != nil {
return nil, fmt.Errorf("failed to retrieve files: %w", err)
}
var solFiles []string
for _, p := range paths {
if filepath.Ext(p) == ".sol" {
solFiles = append(solFiles, filepath.Base(p))
}
}
sort.Strings(solFiles)
return solFiles, nil
}
func readFilesRecursively(dir string, paths *[]string) error {
files, err := os.ReadDir(dir)
if err != nil {
return err
}
for _, file := range files {
filePath := filepath.Join(dir, file.Name())
if file.IsDir() {
if err := readFilesRecursively(filePath, paths); err != nil {
return fmt.Errorf("failed to recurse into %q: %w", filePath, err)
}
} else {
*paths = append(*paths, filePath)
}
}
return nil
} }
// ContractName.0.9.8.json -> ContractName.sol // ContractName.0.9.8.json -> ContractName.sol
// ContractName.json -> ContractName.sol // ContractName.json -> ContractName.sol
func parseArtifactName(artifactVersionFile string) (string, error) { func parseArtifactName(artifactVersionFile string) (string, error) {
re := regexp.MustCompile(`(.*?)\.([0-9]+\.[0-9]+\.[0-9]+)?`) re := regexp.MustCompile(`(.*?)\.([0-9]+\.[0-9]+\.[0-9]+)?`)
match := re.FindStringSubmatch(artifactVersionFile) baseName := filepath.Base(artifactVersionFile)
match := re.FindStringSubmatch(baseName)
if len(match) < 2 { if len(match) < 2 {
return "", fmt.Errorf("invalid artifact file name: %q", artifactVersionFile) return "", fmt.Errorf("invalid artifact file name: %q", artifactVersionFile)
} }
return match[1], nil return match[1], nil
} }
func writeJSON(filename string, data interface{}) error { func resetDirectory(dir string) error {
var out bytes.Buffer if err := os.RemoveAll(dir); err != nil {
enc := json.NewEncoder(&out) return fmt.Errorf("failed to remove directory %q: %w", dir, err)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
err := enc.Encode(data)
if err != nil {
return fmt.Errorf("failed to encode data: %w", err)
} }
jsonData := out.Bytes() if err := os.MkdirAll(dir, os.ModePerm); err != nil {
if len(jsonData) > 0 && jsonData[len(jsonData)-1] == '\n' { // strip newline return fmt.Errorf("failed to create directory %q: %w", dir, err)
jsonData = jsonData[:len(jsonData)-1]
}
if err := os.WriteFile(filename, jsonData, 0644); err != nil {
return fmt.Errorf("failed to write file: %w", err)
} }
return nil return nil
} }
func jsonEqual(a, b interface{}) bool {
jsonA, errA := json.Marshal(a)
jsonB, errB := json.Marshal(b)
return errA == nil && errB == nil && string(jsonA) == string(jsonB)
}
package common package common
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
"path/filepath"
"runtime" "runtime"
"sync" "sync"
"sync/atomic" "sync/atomic"
...@@ -100,8 +100,7 @@ func FindFiles(includes, excludes []string) (map[string]string, error) { ...@@ -100,8 +100,7 @@ func FindFiles(includes, excludes []string) (map[string]string, error) {
return nil, fmt.Errorf("glob pattern error: %w", err) return nil, fmt.Errorf("glob pattern error: %w", err)
} }
for _, match := range matches { for _, match := range matches {
name := filepath.Base(match) included[match] = match
included[name] = match
} }
} }
...@@ -112,7 +111,7 @@ func FindFiles(includes, excludes []string) (map[string]string, error) { ...@@ -112,7 +111,7 @@ func FindFiles(includes, excludes []string) (map[string]string, error) {
return nil, fmt.Errorf("glob pattern error: %w", err) return nil, fmt.Errorf("glob pattern error: %w", err)
} }
for _, match := range matches { for _, match := range matches {
excluded[filepath.Base(match)] = struct{}{} excluded[match] = struct{}{}
} }
} }
...@@ -137,3 +136,22 @@ func ReadForgeArtifact(path string) (*solc.ForgeArtifact, error) { ...@@ -137,3 +136,22 @@ func ReadForgeArtifact(path string) (*solc.ForgeArtifact, error) {
return &artifact, nil return &artifact, nil
} }
func WriteJSON(data interface{}, path string) error {
var out bytes.Buffer
enc := json.NewEncoder(&out)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
err := enc.Encode(data)
if err != nil {
return fmt.Errorf("failed to encode data: %w", err)
}
jsonData := out.Bytes()
if len(jsonData) > 0 && jsonData[len(jsonData)-1] == '\n' { // strip newline
jsonData = jsonData[:len(jsonData)-1]
}
if err := os.WriteFile(path, jsonData, 0644); err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
return nil
}
...@@ -41,7 +41,7 @@ func processFile(path string) (*common.Void, []error) { ...@@ -41,7 +41,7 @@ func processFile(path string) (*common.Void, []error) {
func extractTestNames(artifact *solc.ForgeArtifact) []string { func extractTestNames(artifact *solc.ForgeArtifact) []string {
isTest := false isTest := false
for _, entry := range artifact.Abi.Methods { for _, entry := range artifact.Abi.Parsed.Methods {
if entry.Name == "IS_TEST" { if entry.Name == "IS_TEST" {
isTest = true isTest = true
break break
...@@ -52,7 +52,7 @@ func extractTestNames(artifact *solc.ForgeArtifact) []string { ...@@ -52,7 +52,7 @@ func extractTestNames(artifact *solc.ForgeArtifact) []string {
} }
names := []string{} names := []string{}
for _, entry := range artifact.Abi.Methods { for _, entry := range artifact.Abi.Parsed.Methods {
if !strings.HasPrefix(entry.Name, "test") { if !strings.HasPrefix(entry.Name, "test") {
continue continue
} }
......
...@@ -218,7 +218,8 @@ func TestExtractTestNames(t *testing.T) { ...@@ -218,7 +218,8 @@ func TestExtractTestNames(t *testing.T) {
{ {
name: "valid test contract", name: "valid test contract",
artifact: &solc.ForgeArtifact{ artifact: &solc.ForgeArtifact{
Abi: abi.ABI{ Abi: solc.AbiType{
Parsed: abi.ABI{
Methods: map[string]abi.Method{ Methods: map[string]abi.Method{
"IS_TEST": {Name: "IS_TEST"}, "IS_TEST": {Name: "IS_TEST"},
"test_something_succeeds": {Name: "test_something_succeeds"}, "test_something_succeeds": {Name: "test_something_succeeds"},
...@@ -228,6 +229,7 @@ func TestExtractTestNames(t *testing.T) { ...@@ -228,6 +229,7 @@ func TestExtractTestNames(t *testing.T) {
}, },
}, },
}, },
},
want: []string{ want: []string{
"test_something_succeeds", "test_something_succeeds",
"test_other_fails", "test_other_fails",
...@@ -237,28 +239,33 @@ func TestExtractTestNames(t *testing.T) { ...@@ -237,28 +239,33 @@ func TestExtractTestNames(t *testing.T) {
{ {
name: "non-test contract", name: "non-test contract",
artifact: &solc.ForgeArtifact{ artifact: &solc.ForgeArtifact{
Abi: abi.ABI{ Abi: solc.AbiType{
Parsed: abi.ABI{
Methods: map[string]abi.Method{ Methods: map[string]abi.Method{
"test_something_succeeds": {Name: "test_something_succeeds"}, "test_something_succeeds": {Name: "test_something_succeeds"},
"not_a_test": {Name: "not_a_test"}, "not_a_test": {Name: "not_a_test"},
}, },
}, },
}, },
},
want: nil, want: nil,
}, },
{ {
name: "empty contract", name: "empty contract",
artifact: &solc.ForgeArtifact{ artifact: &solc.ForgeArtifact{
Abi: abi.ABI{ Abi: solc.AbiType{
Parsed: abi.ABI{
Methods: map[string]abi.Method{}, Methods: map[string]abi.Method{},
}, },
}, },
},
want: nil, want: nil,
}, },
{ {
name: "test contract with no test methods", name: "test contract with no test methods",
artifact: &solc.ForgeArtifact{ artifact: &solc.ForgeArtifact{
Abi: abi.ABI{ Abi: solc.AbiType{
Parsed: abi.ABI{
Methods: map[string]abi.Method{ Methods: map[string]abi.Method{
"IS_TEST": {Name: "IS_TEST"}, "IS_TEST": {Name: "IS_TEST"},
"not_a_test": {Name: "not_a_test"}, "not_a_test": {Name: "not_a_test"},
...@@ -266,6 +273,7 @@ func TestExtractTestNames(t *testing.T) { ...@@ -266,6 +273,7 @@ func TestExtractTestNames(t *testing.T) {
}, },
}, },
}, },
},
want: []string{}, want: []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