Skip to content
This repository was archived by the owner on Mar 8, 2025. It is now read-only.

Commit 3627937

Browse files
authored
op-chain-ops: source maps fixes + FS (ethereum-optimism#11574)
* op-chain-ops: source maps fixes + FS * op-chain-ops/srcmap: add doc-comment, remove replaced test * op-chain-ops: address review comments * op-chain-ops: fix missing .sol extension * op-chain-ops: fix artifacts traversal; check extension again, just don't trim the extension
1 parent 9cd71a5 commit 3627937

File tree

16 files changed

+337
-84
lines changed

16 files changed

+337
-84
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ dist
1515
artifacts
1616
cache
1717

18+
!op-chain-ops/foundry/testdata/srcmaps/cache
19+
!op-chain-ops/foundry/testdata/srcmaps/artifacts
20+
1821
packages/contracts-bedrock/deployments/devnetL1
1922
packages/contracts-bedrock/deployments/anvil
2023

op-chain-ops/foundry/artifactsfs.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,29 @@ type ArtifactsFS struct {
3232
FS statDirFs
3333
}
3434

35+
// ListArtifacts lists the artifacts. Each artifact matches a source-file name.
36+
// This name includes the extension, e.g. ".sol"
37+
// (no other artifact-types are supported at this time).
3538
func (af *ArtifactsFS) ListArtifacts() ([]string, error) {
3639
entries, err := af.FS.ReadDir(".")
3740
if err != nil {
3841
return nil, fmt.Errorf("failed to list artifacts: %w", err)
3942
}
4043
out := make([]string, 0, len(entries))
4144
for _, d := range entries {
45+
// Some artifacts may be nested in directories not suffixed with ".sol"
46+
// Nested artifacts, and non-solidity artifacts, are not supported.
4247
if name := d.Name(); strings.HasSuffix(name, ".sol") {
43-
out = append(out, strings.TrimSuffix(name, ".sol"))
48+
out = append(out, d.Name())
4449
}
4550
}
4651
return out, nil
4752
}
4853

49-
// ListContracts lists the contracts of the named artifact.
50-
// E.g. "Owned" might list "Owned.0.8.15", "Owned.0.8.25", and "Owned".
54+
// ListContracts lists the contracts of the named artifact, including the file extension.
55+
// E.g. "Owned.sol" might list "Owned.0.8.15", "Owned.0.8.25", and "Owned".
5156
func (af *ArtifactsFS) ListContracts(name string) ([]string, error) {
52-
f, err := af.FS.Open(name + ".sol")
57+
f, err := af.FS.Open(name)
5358
if err != nil {
5459
return nil, fmt.Errorf("failed to open artifact %q: %w", name, err)
5560
}
@@ -73,8 +78,10 @@ func (af *ArtifactsFS) ListContracts(name string) ([]string, error) {
7378

7479
// ReadArtifact reads a specific JSON contract artifact from the FS.
7580
// The contract name may be suffixed by a solidity compiler version, e.g. "Owned.0.8.25".
81+
// The contract name does not include ".json", this is a detail internal to the artifacts.
82+
// The name of the artifact is the source-file name, this must include the suffix such as ".sol".
7683
func (af *ArtifactsFS) ReadArtifact(name string, contract string) (*Artifact, error) {
77-
artifactPath := path.Join(name+".sol", contract+".json")
84+
artifactPath := path.Join(name, contract+".json")
7885
f, err := af.FS.Open(artifactPath)
7986
if err != nil {
8087
return nil, fmt.Errorf("failed to open artifact %q: %w", artifactPath, err)

op-chain-ops/foundry/sourcefs.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package foundry
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"io/fs"
8+
"path"
9+
"path/filepath"
10+
"strings"
11+
12+
"golang.org/x/exp/maps"
13+
14+
"github.com/ethereum-optimism/optimism/op-chain-ops/srcmap"
15+
)
16+
17+
// SourceMapFS wraps an FS to provide source-maps.
18+
// This FS relies on the following file path assumptions:
19+
// - `/artifacts/build-info/X.json` (build-info path is read from the below file): build files, of foundry incremental builds.
20+
// - `/cache/solidity-files-cache.json`: a JSON file enumerating all files, and when the build last changed.
21+
// - `/` a root dir, relative to where the source files are located (as per the compilationTarget metadata in an artifact).
22+
type SourceMapFS struct {
23+
fs fs.FS
24+
}
25+
26+
// NewSourceMapFS creates a new SourceMapFS.
27+
// The source-map FS loads identifiers for srcmap.ParseSourceMap
28+
// and provides a util to retrieve a source-map for an Artifact.
29+
// The solidity source-files are lazy-loaded when using the produced sourcemap.
30+
func NewSourceMapFS(fs fs.FS) *SourceMapFS {
31+
return &SourceMapFS{fs: fs}
32+
}
33+
34+
// ForgeBuild represents the JSON content of a forge-build entry in the `artifacts/build-info` output.
35+
type ForgeBuild struct {
36+
ID string `json:"id"` // ID of the build itself
37+
SourceIDToPath map[srcmap.SourceID]string `json:"source_id_to_path"` // srcmap ID to source filepath
38+
}
39+
40+
func (s *SourceMapFS) readBuild(buildInfoPath string, id string) (*ForgeBuild, error) {
41+
buildPath := path.Join(buildInfoPath, id+".json")
42+
f, err := s.fs.Open(buildPath)
43+
if err != nil {
44+
return nil, fmt.Errorf("failed to open build: %w", err)
45+
}
46+
defer f.Close()
47+
var build ForgeBuild
48+
if err := json.NewDecoder(f).Decode(&build); err != nil {
49+
return nil, fmt.Errorf("failed to read build: %w", err)
50+
}
51+
return &build, nil
52+
}
53+
54+
// ForgeBuildEntry represents a JSON entry that links the build job of a contract source file.
55+
type ForgeBuildEntry struct {
56+
Path string `json:"path"`
57+
BuildID string `json:"build_id"`
58+
}
59+
60+
// ForgeBuildInfo represents a JSON entry that enumerates the latest builds per contract per compiler version.
61+
type ForgeBuildInfo struct {
62+
// contract name -> solidity version -> build entry
63+
Artifacts map[string]map[string]ForgeBuildEntry `json:"artifacts"`
64+
}
65+
66+
// ForgeBuildCache rep
67+
type ForgeBuildCache struct {
68+
Paths struct {
69+
BuildInfos string `json:"build_infos"`
70+
} `json:"paths"`
71+
Files map[string]ForgeBuildInfo `json:"files"`
72+
}
73+
74+
func (s *SourceMapFS) readBuildCache() (*ForgeBuildCache, error) {
75+
cachePath := path.Join("cache", "solidity-files-cache.json")
76+
f, err := s.fs.Open(cachePath)
77+
if err != nil {
78+
return nil, fmt.Errorf("failed to open build cache: %w", err)
79+
}
80+
defer f.Close()
81+
var buildCache ForgeBuildCache
82+
if err := json.NewDecoder(f).Decode(&buildCache); err != nil {
83+
return nil, fmt.Errorf("failed to read build cache: %w", err)
84+
}
85+
return &buildCache, nil
86+
}
87+
88+
// ReadSourceIDs reads the source-identifier to source file-path mapping that is needed to translate a source-map
89+
// of the given contract, the given compiler version, and within the given source file path.
90+
func (s *SourceMapFS) ReadSourceIDs(path string, contract string, compilerVersion string) (map[srcmap.SourceID]string, error) {
91+
buildCache, err := s.readBuildCache()
92+
if err != nil {
93+
return nil, err
94+
}
95+
artifactBuilds, ok := buildCache.Files[path]
96+
if !ok {
97+
return nil, fmt.Errorf("no known builds for path %q", path)
98+
}
99+
byCompilerVersion, ok := artifactBuilds.Artifacts[contract]
100+
if !ok {
101+
return nil, fmt.Errorf("contract not found in artifact: %q", contract)
102+
}
103+
var buildEntry ForgeBuildEntry
104+
if compilerVersion != "" {
105+
entry, ok := byCompilerVersion[compilerVersion]
106+
if !ok {
107+
return nil, fmt.Errorf("no known build for compiler version: %q", compilerVersion)
108+
}
109+
buildEntry = entry
110+
} else {
111+
if len(byCompilerVersion) == 0 {
112+
return nil, errors.New("no known build, unspecified compiler version")
113+
}
114+
if len(byCompilerVersion) > 1 {
115+
return nil, fmt.Errorf("no compiler version specified, and more than one option: %s", strings.Join(maps.Keys(byCompilerVersion), ", "))
116+
}
117+
for _, entry := range byCompilerVersion {
118+
buildEntry = entry
119+
}
120+
}
121+
build, err := s.readBuild(filepath.ToSlash(buildCache.Paths.BuildInfos), buildEntry.BuildID)
122+
if err != nil {
123+
return nil, fmt.Errorf("failed to read build %q of contract %q: %w", buildEntry.BuildID, contract, err)
124+
}
125+
return build.SourceIDToPath, nil
126+
}
127+
128+
// SourceMap retrieves a source-map for a given contract of a foundry Artifact.
129+
func (s *SourceMapFS) SourceMap(artifact *Artifact, contract string) (*srcmap.SourceMap, error) {
130+
srcPath := ""
131+
for path, name := range artifact.Metadata.Settings.CompilationTarget {
132+
if name == contract {
133+
srcPath = path
134+
break
135+
}
136+
}
137+
if srcPath == "" {
138+
return nil, fmt.Errorf("no known source path for contract %s in artifact", contract)
139+
}
140+
// The commit suffix is ignored, the core semver part is what is used in the resolution of builds.
141+
basicCompilerVersion := strings.SplitN(artifact.Metadata.Compiler.Version, "+", 2)[0]
142+
ids, err := s.ReadSourceIDs(srcPath, contract, basicCompilerVersion)
143+
if err != nil {
144+
return nil, fmt.Errorf("failed to read source IDs of %q: %w", srcPath, err)
145+
}
146+
return srcmap.ParseSourceMap(s.fs, ids, artifact.DeployedBytecode.Object, artifact.DeployedBytecode.SourceMap)
147+
}

op-chain-ops/foundry/sourcefs_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package foundry
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
//go:generate ./testdata/srcmaps/generate.sh
11+
12+
func TestSourceMapFS(t *testing.T) {
13+
artifactFS := OpenArtifactsDir("./testdata/srcmaps/test-artifacts")
14+
exampleArtifact, err := artifactFS.ReadArtifact("SimpleStorage.sol", "SimpleStorage")
15+
require.NoError(t, err)
16+
srcFS := NewSourceMapFS(os.DirFS("./testdata/srcmaps"))
17+
srcMap, err := srcFS.SourceMap(exampleArtifact, "SimpleStorage")
18+
require.NoError(t, err)
19+
seenInfo := make(map[string]struct{})
20+
for i := range exampleArtifact.DeployedBytecode.Object {
21+
seenInfo[srcMap.FormattedInfo(uint64(i))] = struct{}{}
22+
}
23+
require.Contains(t, seenInfo, "src/SimpleStorage.sol:11:5")
24+
require.Contains(t, seenInfo, "src/StorageLibrary.sol:8:9")
25+
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
# artifacts test data
1+
# source-map test data
22

3-
This is a small selection of `forge-artifacts` specifically for testing of Artifact decoding and the Artifacts-FS.
3+
Simple small multi-contract forge setup, to test Go forge map functionality against.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"id":"c79aa2c3b4578aee2dd8f02d20b1aeb6","source_id_to_path":{"0":"src/SimpleStorage.sol","1":"src/StorageLibrary.sol"},"language":"Solidity"}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"_format":"","paths":{"artifacts":"test-artifacts","build_infos":"artifacts/build-info","sources":"src","tests":"test","scripts":"scripts","libraries":["lib","node_modules"]},"files":{"src/SimpleStorage.sol":{"lastModificationDate":1724351550959,"contentHash":"25499c2e202ada22ebd26f8e886cc2e1","sourceName":"src/SimpleStorage.sol","compilerSettings":{"solc":{"optimizer":{"enabled":true,"runs":999999},"metadata":{"useLiteralContent":false,"bytecodeHash":"none","appendCBOR":true},"outputSelection":{"*":{"":["ast"],"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata","storageLayout","devdoc","userdoc"]}},"evmVersion":"cancun","viaIR":false,"libraries":{}},"vyper":{"evmVersion":"cancun","outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode"]}}}},"imports":["src/StorageLibrary.sol"],"versionRequirement":"=0.8.15","artifacts":{"SimpleStorage":{"0.8.15":{"path":"SimpleStorage.sol/SimpleStorage.json","build_id":"c79aa2c3b4578aee2dd8f02d20b1aeb6"}}},"seenByCompiler":true},"src/StorageLibrary.sol":{"lastModificationDate":1724351550967,"contentHash":"61545ea51326b6aa0e3bafaf3116b0a8","sourceName":"src/StorageLibrary.sol","compilerSettings":{"solc":{"optimizer":{"enabled":true,"runs":999999},"metadata":{"useLiteralContent":false,"bytecodeHash":"none","appendCBOR":true},"outputSelection":{"*":{"":["ast"],"*":["abi","evm.bytecode","evm.deployedBytecode","evm.methodIdentifiers","metadata","storageLayout","devdoc","userdoc"]}},"evmVersion":"cancun","viaIR":false,"libraries":{}},"vyper":{"evmVersion":"cancun","outputSelection":{"*":{"*":["abi","evm.bytecode","evm.deployedBytecode"]}}}},"imports":[],"versionRequirement":"=0.8.15","artifacts":{"StorageLibrary":{"0.8.15":{"path":"StorageLibrary.sol/StorageLibrary.json","build_id":"c79aa2c3b4578aee2dd8f02d20b1aeb6"}}},"seenByCompiler":true}},"builds":["c79aa2c3b4578aee2dd8f02d20b1aeb6"]}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
################################################################
2+
# PROFILE: DEFAULT (Local) #
3+
################################################################
4+
5+
[profile.default]
6+
7+
# Compilation settings
8+
src = 'src'
9+
out = 'test-artifacts'
10+
script = 'scripts'
11+
optimizer = true
12+
optimizer_runs = 999999
13+
remappings = []
14+
extra_output = ['devdoc', 'userdoc', 'metadata', 'storageLayout']
15+
bytecode_hash = 'none'
16+
build_info_path = 'artifacts/build-info'
17+
ast = true
18+
evm_version = "cancun"
19+
# 5159 error code is selfdestruct error code
20+
ignored_error_codes = ["transient-storage", "code-size", "init-code-size", 5159]
21+
22+
# We set the gas limit to max int64 to avoid running out of gas during testing, since the default
23+
# gas limit is 1B and some of our tests require more gas than that, such as `test_callWithMinGas_noLeakageLow_succeeds`.
24+
# We use this gas limit since it was the default gas limit prior to https://github.com/foundry-rs/foundry/pull/8274.
25+
# Due to toml-rs limitations, if you increase the gas limit above this value it must be a string.
26+
gas_limit = 9223372036854775807
27+
28+
# Test / Script Runner Settings
29+
ffi = false
30+
fs_permissions = []
31+
libs = ["node_modules", "lib"]
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/sh
2+
3+
set -euo
4+
5+
# Don't include previous build outputs
6+
forge clean
7+
8+
forge build
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity 0.8.15;
3+
4+
import {StorageLibrary} from "./StorageLibrary.sol";
5+
6+
// @notice SimpleStorage is a contract to test Go <> foundry integration.
7+
// @dev uses a dependency, to test source-mapping with multiple sources.
8+
contract SimpleStorage {
9+
10+
// @dev example getter
11+
function getExampleData() public pure returns (uint256) {
12+
return StorageLibrary.addData(42);
13+
}
14+
}

0 commit comments

Comments
 (0)