Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
21b5aea
test: add import compliance tests
JonathanOppenheimer Nov 21, 2025
7e33965
test: tweak violation wording
JonathanOppenheimer Nov 21, 2025
404acd5
test: add violation to show failure example
JonathanOppenheimer Nov 21, 2025
dec282e
test: remove violation
JonathanOppenheimer Nov 21, 2025
234a039
test: only enforce on vms/evm directory
JonathanOppenheimer Nov 21, 2025
9a39d6b
test: clarify boundaries
JonathanOppenheimer Nov 22, 2025
5017e1e
test: rename test
JonathanOppenheimer Nov 22, 2025
e490151
test: clarify comments
JonathanOppenheimer Nov 22, 2025
f411817
chore: lint
JonathanOppenheimer Nov 22, 2025
84fbb35
test: add license violation test
JonathanOppenheimer Nov 22, 2025
498915c
test: inline once used type
JonathanOppenheimer Nov 22, 2025
f4fbfac
tests: rename libevm test
JonathanOppenheimer Nov 22, 2025
69e8354
chore: fix typo
JonathanOppenheimer Nov 22, 2025
be5228a
test: remove licensing test (excessive)
JonathanOppenheimer Nov 24, 2025
d607b78
Merge branch 'master' into JonathanOppenheimer/import-testing
JonathanOppenheimer Nov 24, 2025
99ed2ce
test: check both evm code locations
JonathanOppenheimer Nov 24, 2025
e9cc48b
test: move import tests to evm directory
JonathanOppenheimer Nov 24, 2025
cbcd663
test: reconsider boundary tests
JonathanOppenheimer Nov 24, 2025
f482da4
Update vms/evm/imports_test.go
JonathanOppenheimer Dec 1, 2025
c4b31fc
test: use Arran's rewritten test
JonathanOppenheimer Dec 1, 2025
7d513d4
fix: add .go
JonathanOppenheimer Dec 1, 2025
40e5904
Merge branch 'master' into JonathanOppenheimer/import-testing
JonathanOppenheimer Dec 1, 2025
b6541d1
chore: lint
JonathanOppenheimer Dec 1, 2025
70f33bc
chore: move comment
JonathanOppenheimer Dec 1, 2025
57f083c
docs: document functions
JonathanOppenheimer Dec 1, 2025
63a88d9
Update vms/evm/imports_test.go
JonathanOppenheimer Dec 2, 2025
249e7cd
Update vms/evm/imports_test.go
JonathanOppenheimer Dec 2, 2025
fee0356
Update vms/evm/imports_test.go
JonathanOppenheimer Dec 2, 2025
d24963b
test: use absolute file paths
JonathanOppenheimer Dec 2, 2025
7578a3f
Merge branch 'master' into JonathanOppenheimer/import-testing
JonathanOppenheimer Dec 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions graft/scripts/libevm-allowed-packages.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Allowed libevm packages can be imported by EVM code
# Lines with a "!" prefix are forbidden.
# Lines without a "!" prefix are allowed (exceptions to the forbidden patterns).

"github.com/ava-labs/libevm/accounts"
"github.com/ava-labs/libevm/accounts/external"
"github.com/ava-labs/libevm/accounts/keystore"
"github.com/ava-labs/libevm/accounts/scwallet"
"github.com/ava-labs/libevm/common"
"github.com/ava-labs/libevm/common/bitutil"
"github.com/ava-labs/libevm/common/compiler"
"github.com/ava-labs/libevm/common/hexutil"
"github.com/ava-labs/libevm/common/lru"
"github.com/ava-labs/libevm/common/math"
"github.com/ava-labs/libevm/common/prque"
"github.com/ava-labs/libevm/consensus/misc/eip4844"
"github.com/ava-labs/libevm/core/asm"
"github.com/ava-labs/libevm/core/bloombits"
"github.com/ava-labs/libevm/core/rawdb"
"github.com/ava-labs/libevm/core/state"
"github.com/ava-labs/libevm/core/types"
"github.com/ava-labs/libevm/core/vm"
"github.com/ava-labs/libevm/crypto"
"github.com/ava-labs/libevm/crypto/blake2b"
"github.com/ava-labs/libevm/crypto/bls12381"
"github.com/ava-labs/libevm/crypto/bn256"
"github.com/ava-labs/libevm/crypto/kzg4844"
"github.com/ava-labs/libevm/eth/tracers/js"
"github.com/ava-labs/libevm/eth/tracers/logger"
"github.com/ava-labs/libevm/eth/tracers/native"
"github.com/ava-labs/libevm/ethdb"
"github.com/ava-labs/libevm/ethdb/dbtest"
"github.com/ava-labs/libevm/ethdb/leveldb"
"github.com/ava-labs/libevm/ethdb/memorydb"
"github.com/ava-labs/libevm/ethdb/pebble"
"github.com/ava-labs/libevm/event"
"github.com/ava-labs/libevm/libevm"
"github.com/ava-labs/libevm/libevm/legacy"
"github.com/ava-labs/libevm/libevm/options"
"github.com/ava-labs/libevm/libevm/stateconf"
"github.com/ava-labs/libevm/log"
"github.com/ava-labs/libevm/metrics"
"github.com/ava-labs/libevm/params"
"github.com/ava-labs/libevm/rlp"
"github.com/ava-labs/libevm/trie"
"github.com/ava-labs/libevm/trie/testutil"
"github.com/ava-labs/libevm/trie/trienode"
"github.com/ava-labs/libevm/trie/triestate"
"github.com/ava-labs/libevm/trie/utils"
"github.com/ava-labs/libevm/triedb"
"github.com/ava-labs/libevm/triedb/database"
249 changes: 249 additions & 0 deletions tests/imports_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package tests

import (
"bufio"
"fmt"
"go/ast"
"go/parser"
"go/token"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"testing"

"github.com/stretchr/testify/require"

"github.com/ava-labs/avalanchego/utils/set"
)

// TestDoNotImportFromGraft ensures proper import rules for graft packages:
// - graft/coreth can be imported anywhere EXCEPT vms/evm (but vms/evm/emulate is an exception)
// - graft/subnet-evm cannot be imported anywhere EXCEPT vms/evm/emulate
func TestEnforceGraftImportBoundaries(t *testing.T) {
graftRegex := regexp.MustCompile(`^github\.com/ava-labs/avalanchego/graft(/|$)`)

// Find all graft imports in the entire repository
foundImports, err := findImportsMatchingPattern("..", graftRegex, func(path string, importPath string, _ *ast.ImportSpec) bool {
// Skip generated files and test-specific files
filename := filepath.Base(path)
if strings.HasPrefix(filename, "gen_") ||
strings.Contains(path, "graft/*/core/main_test.go") ||
strings.Contains(path, "graft/*/tempextrastest/") {
return true
}

// Skip files in the graft directory itself - they can import from graft
if strings.Contains(path, "/graft/") {
return true
}

isInEmulate := strings.Contains(path, "/vms/evm/emulate/")
isInVmsEvm := strings.Contains(path, "/vms/evm/")
isCoreth := strings.Contains(importPath, "/graft/coreth")
isSubnetEVM := strings.Contains(importPath, "/graft/subnet-evm")

// graft/coreth: illegal in vms/evm except vms/evm/emulate
if isCoreth && isInVmsEvm && !isInEmulate {
return false
}

// graft/subnet-evm: illegal everywhere except vms/evm/emulate
if isSubnetEVM && !isInEmulate {
return false
}

return true
})
require.NoError(t, err, "Failed to find graft imports")

if len(foundImports) == 0 {
return // no violations found
}

header := "Graft import rules violated!\n" +
"Rules:\n" +
" - graft/coreth can be imported anywhere EXCEPT vms/evm (but vms/evm/emulate is an exception)\n" +
" - graft/subnet-evm cannot be imported anywhere EXCEPT vms/evm/emulate\n\n"
require.Fail(t, formatImportViolations(foundImports, header))
}

// TestEnforceLibevmImportsAllowlist ensures that all libevm imports by EVM code
// are explicitly allowed via the libevm-allowed-packages.txt file.
func TestEnforceLibevmImportsAllowlist(t *testing.T) {
_, allowedPackages, err := loadPatternFile("../graft/scripts/libevm-allowed-packages.txt")
require.NoError(t, err, "Failed to load allowed packages")

// Convert allowed packages to a set for faster lookup
allowedSet := set.Set[string]{}
for _, pkg := range allowedPackages {
allowedSet.Add(pkg)
}

// Find all libevm imports in graft and vms/evm, excluding underscore and "eth*" named imports
libevmRegex := regexp.MustCompile(`^github\.com/ava-labs/libevm/`)
filterFunc := func(path string, _ string, imp *ast.ImportSpec) bool {
// Skip generated files and test-specific files
filename := filepath.Base(path)
if strings.HasPrefix(filename, "gen_") ||
strings.Contains(path, "graft/*/core/main_test.go") ||
strings.Contains(path, "graft/*/tempextrastest/") {
return true
}

// Skip underscore and "eth*" named imports
return imp.Name != nil && (imp.Name.Name == "_" || strings.HasPrefix(imp.Name.Name, "eth"))
}

// TODO(jonathanoppenheimer): remove when graft is removed
foundImports, err := findImportsMatchingPattern("../graft", libevmRegex, filterFunc)
require.NoError(t, err, "Failed to find libevm imports in graft")

evmImports, err := findImportsMatchingPattern("../vms/evm", libevmRegex, filterFunc)
require.NoError(t, err, "Failed to find libevm imports in vms/evm")

// merge libevm imports from graft and vms/evm
for importPath, files := range evmImports {
if existingFiles, exists := foundImports[importPath]; exists {
for file := range files {
existingFiles.Add(file)
}
} else {
foundImports[importPath] = files
}
}

violations := make(map[string]set.Set[string])
for importPath, files := range foundImports {
if !allowedSet.Contains(importPath) {
violations[importPath] = files
}
}

if len(violations) == 0 {
return // no violations found
}

header := "EVM files must not import forbidden libevm packages!\n" +
"If a package is safe to import, add it to graft/scripts/libevm-allowed-packages.txt.\n\n"
require.Fail(t, formatImportViolations(violations, header))
}

// formatImportViolations formats a map of import violations into an error message
func formatImportViolations(violations map[string]set.Set[string], header string) string {
sortedViolations := make([]string, 0, len(violations))
for importPath := range violations {
sortedViolations = append(sortedViolations, importPath)
}
slices.Sort(sortedViolations)

var errorMsg strings.Builder
errorMsg.WriteString(header)
errorMsg.WriteString("Violations:\n\n")

for _, importPath := range sortedViolations {
files := violations[importPath]
fileList := files.List()
slices.Sort(fileList)

errorMsg.WriteString(fmt.Sprintf("- %s\n", importPath))
errorMsg.WriteString(fmt.Sprintf(" Used in %d file(s):\n", len(fileList)))
for _, file := range fileList {
errorMsg.WriteString(fmt.Sprintf(" • %s\n", file))
}
errorMsg.WriteString("\n")
}

return errorMsg.String()
}

// loadPatternFile reads patterns from a file and separates them into forbidden and allowed.
// Lines with a ! prefix are forbidden patterns.
// Lines without a ! prefix are allowed patterns (exceptions).
// Returns: (forbidden patterns, allowed patterns, error)
func loadPatternFile(filename string) ([]string, []string, error) {
file, err := os.Open(filename)
if err != nil {
return nil, nil, fmt.Errorf("failed to open pattern file: %w", err)
}
defer file.Close()

var forbidden []string
var allowed []string

scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}

line = strings.Trim(line, `"`)
if strings.HasPrefix(line, "!") {
forbidden = append(forbidden, strings.TrimPrefix(line, "!"))
} else {
allowed = append(allowed, line)
}
}

if err := scanner.Err(); err != nil {
return nil, nil, fmt.Errorf("failed to read pattern file: %w", err)
}

return forbidden, allowed, nil
}

// findImportsMatchingPattern is a generalized function that finds all imports
// matching a given regex pattern in the specified directory.
// The filterFunc can be used to skip certain files or imports (return true to skip).
// Returns a map of import paths to the set of files that contain them.
func findImportsMatchingPattern(
rootDir string,
importRegex *regexp.Regexp,
filterFunc func(filePath string, importPath string, importSpec *ast.ImportSpec) bool,
) (map[string]set.Set[string], error) {
imports := make(map[string]set.Set[string])

err := filepath.Walk(rootDir, func(path string, _ os.FileInfo, err error) error {
if err != nil || !strings.HasSuffix(path, ".go") {
return err
}

node, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ParseComments)
if err != nil {
return fmt.Errorf("failed to parse %s: %w", path, err)
}

for _, imp := range node.Imports {
if imp.Path == nil {
continue
}

importPath := strings.Trim(imp.Path.Value, `"`)
if !importRegex.MatchString(importPath) {
continue
}

if filterFunc != nil && filterFunc(path, importPath, imp) {
continue
}

if _, exists := imports[importPath]; !exists {
imports[importPath] = set.Set[string]{}
}
fileSet := imports[importPath]
fileSet.Add(path)
imports[importPath] = fileSet
}
return nil
})
if err != nil {
return nil, err
}

return imports, nil
}
Loading