Skip to content

Commit

Permalink
feat: common core for contracts Go check scripts
Browse files Browse the repository at this point in the history
Introduces a new common base framework for writing contracts
check scripts in Go. Many of the check scripts basically do the
exact same logic of somehow parsing either the interfaces or the
artifact files. Goal of this small project is to make the process
of writing new checks easier and more reliable.

We demonstrate this framework in action by porting the test-names
script to use this new framework.
  • Loading branch information
smartcontracts committed Nov 28, 2024
1 parent 601af96 commit a45baf2
Show file tree
Hide file tree
Showing 8 changed files with 830 additions and 116 deletions.
1 change: 1 addition & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1379,6 +1379,7 @@ workflows:
op-e2e/interop
op-e2e/actions
op-e2e/faultproofs
packages/contracts-bedrock/scripts/checks
requires:
- contracts-bedrock-build
- cannon-prestate
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ toolchain go1.22.7
require (
github.com/BurntSushi/toml v1.4.0
github.com/andybalholm/brotli v1.1.0
github.com/bmatcuk/doublestar/v4 v4.7.1
github.com/btcsuite/btcd v0.24.2
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
github.com/cockroachdb/pebble v1.1.2
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE=
github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bmatcuk/doublestar/v4 v4.7.1 h1:fdDeAqgT47acgwd9bd9HxJRDmc9UAmPpc+2m0CXv75Q=
github.com/bmatcuk/doublestar/v4 v4.7.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
Expand Down
156 changes: 154 additions & 2 deletions op-chain-ops/solc/types.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package solc

import (
"encoding/json"
"fmt"

"github.com/ethereum/go-ethereum/accounts/abi"
Expand Down Expand Up @@ -129,5 +128,158 @@ type Ast struct {
Id uint `json:"id"`
License string `json:"license"`
NodeType string `json:"nodeType"`
Nodes json.RawMessage `json:"nodes"`
Nodes []AstNode `json:"nodes"`
Src string `json:"src"`
}

type AstNode struct {
Id int `json:"id"`
NodeType string `json:"nodeType"`
Src string `json:"src"`
Nodes []AstNode `json:"nodes,omitempty"`
Abstract bool `json:"abstract,omitempty"`
BaseContracts []AstBaseContract `json:"baseContracts,omitempty"`
CanonicalName string `json:"canonicalName,omitempty"`
ContractDependencies []int `json:"contractDependencies,omitempty"`
ContractKind string `json:"contractKind,omitempty"`
Documentation interface{} `json:"documentation,omitempty"`
FullyImplemented bool `json:"fullyImplemented,omitempty"`
LinearizedBaseContracts []int `json:"linearizedBaseContracts,omitempty"`
Name string `json:"name,omitempty"`
NameLocation string `json:"nameLocation,omitempty"`
Scope int `json:"scope,omitempty"`
UsedErrors []int `json:"usedErrors,omitempty"`
UsedEvents []int `json:"usedEvents,omitempty"`

// Function specific
Body *AstBlock `json:"body,omitempty"`
Parameters *AstParameterList `json:"parameters,omitempty"`
ReturnParameters *AstParameterList `json:"returnParameters,omitempty"`
StateMutability string `json:"stateMutability,omitempty"`
Virtual bool `json:"virtual,omitempty"`
Visibility string `json:"visibility,omitempty"`

// Variable specific
Constant bool `json:"constant,omitempty"`
Mutability string `json:"mutability,omitempty"`
StateVariable bool `json:"stateVariable,omitempty"`
StorageLocation string `json:"storageLocation,omitempty"`
TypeDescriptions *AstTypeDescriptions `json:"typeDescriptions,omitempty"`
TypeName *AstTypeName `json:"typeName,omitempty"`

// Expression specific
Expression *Expression `json:"expression,omitempty"`
IsConstant bool `json:"isConstant,omitempty"`
IsLValue bool `json:"isLValue,omitempty"`
IsPure bool `json:"isPure,omitempty"`
LValueRequested bool `json:"lValueRequested,omitempty"`

// Literal specific
HexValue string `json:"hexValue,omitempty"`
Kind string `json:"kind,omitempty"`
Value interface{} `json:"value,omitempty"`

// Other fields
Arguments []Expression `json:"arguments,omitempty"`
Condition *Expression `json:"condition,omitempty"`
TrueBody *AstBlock `json:"trueBody,omitempty"`
FalseBody *AstBlock `json:"falseBody,omitempty"`
Operator string `json:"operator,omitempty"`
}

type AstBaseContract struct {
BaseName *AstTypeName `json:"baseName"`
Id int `json:"id"`
NodeType string `json:"nodeType"`
Src string `json:"src"`
}

type AstDocumentation struct {
Id int `json:"id"`
NodeType string `json:"nodeType"`
Src string `json:"src"`
Text string `json:"text"`
}

type AstBlock struct {
Id int `json:"id"`
NodeType string `json:"nodeType"`
Src string `json:"src"`
Statements []AstNode `json:"statements"`
}

type AstParameterList struct {
Id int `json:"id"`
NodeType string `json:"nodeType"`
Parameters []AstNode `json:"parameters"`
Src string `json:"src"`
}

type AstTypeDescriptions struct {
TypeIdentifier string `json:"typeIdentifier"`
TypeString string `json:"typeString"`
}

type AstTypeName struct {
Id int `json:"id"`
Name string `json:"name"`
NodeType string `json:"nodeType"`
Src string `json:"src"`
StateMutability string `json:"stateMutability,omitempty"`
TypeDescriptions *AstTypeDescriptions `json:"typeDescriptions,omitempty"`
}

type Expression struct {
Id int `json:"id"`
NodeType string `json:"nodeType"`
Src string `json:"src"`
TypeDescriptions *AstTypeDescriptions `json:"typeDescriptions,omitempty"`
Name string `json:"name,omitempty"`
OverloadedDeclarations []int `json:"overloadedDeclarations,omitempty"`
ReferencedDeclaration int `json:"referencedDeclaration,omitempty"`
ArgumentTypes []AstTypeDescriptions `json:"argumentTypes,omitempty"`
}

type ForgeArtifact struct {
Abi abi.ABI `json:"abi"`
Bytecode CompilerOutputBytecode `json:"bytecode"`
DeployedBytecode CompilerOutputBytecode `json:"deployedBytecode"`
MethodIdentifiers map[string]string `json:"methodIdentifiers"`
RawMetadata string `json:"rawMetadata"`
Metadata ForgeCompilerMetadata `json:"metadata"`
StorageLayout *StorageLayout `json:"storageLayout,omitempty"`
Ast Ast `json:"ast"`
Id int `json:"id"`
}

type ForgeCompilerMetadata struct {
Compiler ForgeCompilerInfo `json:"compiler"`
Language string `json:"language"`
Output ForgeMetadataOutput `json:"output"`
Settings CompilerSettings `json:"settings"`
Sources map[string]ForgeSourceInfo `json:"sources"`
Version int `json:"version"`
}

type ForgeCompilerInfo struct {
Version string `json:"version"`
}

type ForgeMetadataOutput struct {
Abi abi.ABI `json:"abi"`
DevDoc ForgeDocObject `json:"devdoc"`
UserDoc ForgeDocObject `json:"userdoc"`
}

type ForgeSourceInfo struct {
Keccak256 string `json:"keccak256"`
License string `json:"license"`
Urls []string `json:"urls"`
}

type ForgeDocObject struct {
Kind string `json:"kind"`
Methods map[string]interface{} `json:"methods"`
Notice string `json:"notice,omitempty"`
Version int `json:"version"`
}
124 changes: 124 additions & 0 deletions packages/contracts-bedrock/scripts/checks/common/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package common

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"sync"
"sync/atomic"

"github.com/bmatcuk/doublestar/v4"
"github.com/ethereum-optimism/optimism/op-chain-ops/solc"
"golang.org/x/sync/errgroup"
)

type ErrorReporter struct {
hasErr atomic.Bool
outMtx sync.Mutex
}

func NewErrorReporter() *ErrorReporter {
return &ErrorReporter{}
}

func (e *ErrorReporter) Fail(msg string, args ...any) {
e.outMtx.Lock()
// Useful for suppressing error reporting in tests
if os.Getenv("SUPPRESS_ERROR_REPORTER") == "" {
_, _ = fmt.Fprintf(os.Stderr, "❌ "+msg+"\n", args...)
}
e.outMtx.Unlock()
e.hasErr.Store(true)
}

func (e *ErrorReporter) HasError() bool {
return e.hasErr.Load()
}

type FileProcessor func(path string) []error

func ProcessFiles(files map[string]string, processor FileProcessor) error {
g := errgroup.Group{}
g.SetLimit(runtime.NumCPU())

reporter := NewErrorReporter()
for name, path := range files {
name, path := name, path // Capture loop variables
g.Go(func() error {
if errs := processor(path); len(errs) > 0 {
for _, err := range errs {
reporter.Fail("%s: %v", name, err)
}
}
return nil
})
}

err := g.Wait()
if err != nil {
return fmt.Errorf("processing failed: %w", err)
}
if reporter.HasError() {
return fmt.Errorf("processing failed")
}
return nil
}

func ProcessFilesGlob(includes, excludes []string, processor FileProcessor) error {
files, err := FindFiles(includes, excludes)
if err != nil {
return err
}
return ProcessFiles(files, processor)
}

func FindFiles(includes, excludes []string) (map[string]string, error) {
included := make(map[string]string)
excluded := make(map[string]struct{})

// Get all included files
for _, pattern := range includes {
matches, err := doublestar.Glob(os.DirFS("."), pattern)
if err != nil {
return nil, fmt.Errorf("glob pattern error: %w", err)
}
for _, match := range matches {
name := filepath.Base(match)
included[name] = match
}
}

// Get all excluded files
for _, pattern := range excludes {
matches, err := doublestar.Glob(os.DirFS("."), pattern)
if err != nil {
return nil, fmt.Errorf("glob pattern error: %w", err)
}
for _, match := range matches {
excluded[filepath.Base(match)] = struct{}{}
}
}

// Remove excluded files from result
for name := range excluded {
delete(included, name)
}

return included, nil
}

func ReadForgeArtifact(path string) (*solc.ForgeArtifact, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read artifact: %w", err)
}

var artifact solc.ForgeArtifact
if err := json.Unmarshal(data, &artifact); err != nil {
return nil, fmt.Errorf("failed to parse artifact: %w", err)
}

return &artifact, nil
}
Loading

0 comments on commit a45baf2

Please sign in to comment.