Skip to content

Commit ec8fc7e

Browse files
authored
op-chain-ops: artifacts FS, improve artifacts metadata (#11445)
* op-chain-ops: artifacts FS, improve artifacts metadata * ci: include artifacts as Go e2e test pre-requisite * op-chain-ops: move full artifacts test, update testdata * ci: fix artifacts workspace copy
1 parent d8807a5 commit ec8fc7e

File tree

15 files changed

+267
-10609
lines changed

15 files changed

+267
-10609
lines changed

.circleci/config.yml

+2-1
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,7 @@ jobs:
857857
- attach_workspace:
858858
at: /tmp/workspace
859859
- run:
860-
name: Load devnet-allocs
860+
name: Load devnet-allocs and artifacts
861861
command: |
862862
mkdir -p .devnet
863863
cp /tmp/workspace/.devnet<<parameters.variant>>/allocs-l2-delta.json .devnet/allocs-l2-delta.json
@@ -866,6 +866,7 @@ jobs:
866866
cp /tmp/workspace/.devnet<<parameters.variant>>/allocs-l2-granite.json .devnet/allocs-l2-granite.json
867867
cp /tmp/workspace/.devnet<<parameters.variant>>/allocs-l1.json .devnet/allocs-l1.json
868868
cp /tmp/workspace/.devnet<<parameters.variant>>/addresses.json .devnet/addresses.json
869+
cp -r /tmp/workspace/packages/contracts-bedrock/forge-artifacts packages/contracts-bedrock/forge-artifacts
869870
cp /tmp/workspace/packages/contracts-bedrock/deploy-config/devnetL1.json packages/contracts-bedrock/deploy-config/devnetL1.json
870871
cp -r /tmp/workspace/packages/contracts-bedrock/deployments/devnetL1 packages/contracts-bedrock/deployments/devnetL1
871872
- run:

cannon/mipsevm/testutil/evm.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package testutil
22

33
import (
4+
"bytes"
45
"encoding/binary"
56
"fmt"
67
"math/big"
@@ -12,7 +13,6 @@ import (
1213
"github.com/ethereum-optimism/optimism/op-chain-ops/foundry"
1314

1415
"github.com/ethereum/go-ethereum/common"
15-
"github.com/ethereum/go-ethereum/common/hexutil"
1616
"github.com/ethereum/go-ethereum/consensus"
1717
"github.com/ethereum/go-ethereum/consensus/ethash"
1818
"github.com/ethereum/go-ethereum/core"
@@ -78,7 +78,7 @@ func NewEVMEnv(artifacts *Artifacts, addrs *Addresses) (*vm.EVM, *state.StateDB)
7878

7979
var mipsCtorArgs [32]byte
8080
copy(mipsCtorArgs[12:], addrs.Oracle[:])
81-
mipsDeploy := append(hexutil.MustDecode(artifacts.MIPS.Bytecode.Object.String()), mipsCtorArgs[:]...)
81+
mipsDeploy := append(bytes.Clone(artifacts.MIPS.Bytecode.Object), mipsCtorArgs[:]...)
8282
startingGas := uint64(30_000_000)
8383
_, deployedMipsAddr, leftOverGas, err := env.Create(vm.AccountRef(addrs.Sender), mipsDeploy, startingGas, common.U2560)
8484
if err != nil {

op-chain-ops/foundry/artifact.go

+77-13
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package foundry
22

33
import (
4+
"bytes"
45
"encoding/json"
6+
"errors"
57
"fmt"
68
"os"
7-
"path/filepath"
89
"strings"
910

1011
"github.com/holiman/uint256"
@@ -26,6 +27,7 @@ type Artifact struct {
2627
StorageLayout solc.StorageLayout
2728
DeployedBytecode DeployedBytecode
2829
Bytecode Bytecode
30+
Metadata Metadata
2931
}
3032

3133
func (a *Artifact) UnmarshalJSON(data []byte) error {
@@ -42,6 +44,7 @@ func (a *Artifact) UnmarshalJSON(data []byte) error {
4244
a.StorageLayout = artifact.StorageLayout
4345
a.DeployedBytecode = artifact.DeployedBytecode
4446
a.Bytecode = artifact.Bytecode
47+
a.Metadata = artifact.Metadata
4548
return nil
4649
}
4750

@@ -51,6 +54,7 @@ func (a Artifact) MarshalJSON() ([]byte, error) {
5154
StorageLayout: a.StorageLayout,
5255
DeployedBytecode: a.DeployedBytecode,
5356
Bytecode: a.Bytecode,
57+
Metadata: a.Metadata,
5458
}
5559
return json.Marshal(artifact)
5660
}
@@ -62,22 +66,83 @@ type artifactMarshaling struct {
6266
StorageLayout solc.StorageLayout `json:"storageLayout"`
6367
DeployedBytecode DeployedBytecode `json:"deployedBytecode"`
6468
Bytecode Bytecode `json:"bytecode"`
69+
Metadata Metadata `json:"metadata"`
70+
}
71+
72+
// Metadata is the subset of metadata in a foundry contract artifact that we use in OP-Stack tooling.
73+
type Metadata struct {
74+
Compiler struct {
75+
Version string `json:"version"`
76+
} `json:"compiler"`
77+
78+
Language string `json:"language"`
79+
80+
Output json.RawMessage `json:"output"`
81+
82+
Settings struct {
83+
// Remappings of the contract imports
84+
Remappings json.RawMessage `json:"remappings"`
85+
// Optimizer settings affect the compiler output, but can be arbitrary.
86+
// We load them opaquely, to include it in the hash of what we run.
87+
Optimizer json.RawMessage `json:"optimizer"`
88+
// Metadata is loaded opaquely, similar to the Optimizer, to include in hashing.
89+
// E.g. the bytecode-hash contract suffix as setting is enabled/disabled in here.
90+
Metadata json.RawMessage `json:"metadata"`
91+
// Map of full contract path to compiled contract name.
92+
CompilationTarget map[string]string `json:"compilationTarget"`
93+
// EVM version affects output, and hence included.
94+
EVMVersion string `json:"evmVersion"`
95+
// Libraries data
96+
Libraries json.RawMessage `json:"libraries"`
97+
} `json:"settings"`
98+
99+
Sources map[string]ContractSource `json:"sources"`
100+
101+
Version int `json:"version"`
102+
}
103+
104+
// ContractSource represents a JSON value in the "sources" map of a contract metadata dump.
105+
// This uniquely identifies the source code of the contract.
106+
type ContractSource struct {
107+
Keccak256 common.Hash `json:"keccak256"`
108+
URLs []string `json:"urls"`
109+
License string `json:"license"`
110+
}
111+
112+
var ErrLinkingUnsupported = errors.New("cannot load bytecode with linking placeholders")
113+
114+
// LinkableBytecode is not purely hex, it returns an ErrLinkingUnsupported error when
115+
// input contains __$aaaaaaa$__ style linking placeholders.
116+
// See https://docs.soliditylang.org/en/latest/using-the-compiler.html#library-linking
117+
// In practice this is only used by test contracts to link in large test libraries.
118+
type LinkableBytecode []byte
119+
120+
func (lb *LinkableBytecode) UnmarshalJSON(data []byte) error {
121+
if bytes.Contains(data, []byte("__$")) {
122+
return ErrLinkingUnsupported
123+
}
124+
return (*hexutil.Bytes)(lb).UnmarshalJSON(data)
125+
}
126+
127+
func (lb LinkableBytecode) MarshalText() ([]byte, error) {
128+
return (hexutil.Bytes)(lb).MarshalText()
65129
}
66130

67131
// DeployedBytecode represents the deployed bytecode section of the solc compiler output.
68132
type DeployedBytecode struct {
69-
SourceMap string `json:"sourceMap"`
70-
Object hexutil.Bytes `json:"object"`
71-
LinkReferences json.RawMessage `json:"linkReferences"`
72-
ImmutableReferences json.RawMessage `json:"immutableReferences,omitempty"`
133+
SourceMap string `json:"sourceMap"`
134+
Object LinkableBytecode `json:"object"`
135+
LinkReferences json.RawMessage `json:"linkReferences"`
136+
ImmutableReferences json.RawMessage `json:"immutableReferences,omitempty"`
73137
}
74138

75139
// Bytecode represents the bytecode section of the solc compiler output.
76140
type Bytecode struct {
77-
SourceMap string `json:"sourceMap"`
78-
Object hexutil.Bytes `json:"object"`
79-
LinkReferences json.RawMessage `json:"linkReferences"`
80-
ImmutableReferences json.RawMessage `json:"immutableReferences,omitempty"`
141+
SourceMap string `json:"sourceMap"`
142+
// not purely hex, can contain __$aaaaaaa$__ style linking placeholders
143+
Object LinkableBytecode `json:"object"`
144+
LinkReferences json.RawMessage `json:"linkReferences"`
145+
ImmutableReferences json.RawMessage `json:"immutableReferences,omitempty"`
81146
}
82147

83148
// ReadArtifact will read an artifact from disk given a path.
@@ -130,15 +195,14 @@ func (d *ForgeAllocs) UnmarshalJSON(b []byte) error {
130195
}
131196

132197
func LoadForgeAllocs(allocsPath string) (*ForgeAllocs, error) {
133-
path := filepath.Join(allocsPath)
134-
f, err := os.OpenFile(path, os.O_RDONLY, 0644)
198+
f, err := os.OpenFile(allocsPath, os.O_RDONLY, 0644)
135199
if err != nil {
136-
return nil, fmt.Errorf("failed to open forge allocs %q: %w", path, err)
200+
return nil, fmt.Errorf("failed to open forge allocs %q: %w", allocsPath, err)
137201
}
138202
defer f.Close()
139203
var out ForgeAllocs
140204
if err := json.NewDecoder(f).Decode(&out); err != nil {
141-
return nil, fmt.Errorf("failed to json-decode forge allocs %q: %w", path, err)
205+
return nil, fmt.Errorf("failed to json-decode forge allocs %q: %w", allocsPath, err)
142206
}
143207
return &out, nil
144208
}

op-chain-ops/foundry/artifact_test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ import (
1010

1111
// TestArtifactJSON tests roundtrip serialization of a foundry artifact for commonly used fields.
1212
func TestArtifactJSON(t *testing.T) {
13-
artifact, err := ReadArtifact("testdata/OptimismPortal.json")
13+
artifact, err := ReadArtifact("testdata/forge-artifacts/Owned.sol/Owned.json")
1414
require.NoError(t, err)
1515

1616
data, err := json.Marshal(artifact)
1717
require.NoError(t, err)
1818

19-
file, err := os.ReadFile("testdata/OptimismPortal.json")
19+
file, err := os.ReadFile("testdata/forge-artifacts/Owned.sol/Owned.json")
2020
require.NoError(t, err)
2121

2222
got := unmarshalIntoMap(t, data)
@@ -26,6 +26,7 @@ func TestArtifactJSON(t *testing.T) {
2626
require.JSONEq(t, marshal(t, got["deployedBytecode"]), marshal(t, expected["deployedBytecode"]))
2727
require.JSONEq(t, marshal(t, got["abi"]), marshal(t, expected["abi"]))
2828
require.JSONEq(t, marshal(t, got["storageLayout"]), marshal(t, expected["storageLayout"]))
29+
require.JSONEq(t, marshal(t, got["metadata"]), marshal(t, expected["metadata"]))
2930
}
3031

3132
func unmarshalIntoMap(t *testing.T, file []byte) map[string]any {

op-chain-ops/foundry/artifactsfs.go

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package foundry
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io/fs"
7+
"os"
8+
"path"
9+
"strings"
10+
)
11+
12+
type statDirFs interface {
13+
fs.StatFS
14+
fs.ReadDirFS
15+
}
16+
17+
func OpenArtifactsDir(dirPath string) *ArtifactsFS {
18+
dir := os.DirFS(dirPath)
19+
if d, ok := dir.(statDirFs); !ok {
20+
panic("Go DirFS guarantees changed")
21+
} else {
22+
return &ArtifactsFS{FS: d}
23+
}
24+
}
25+
26+
// ArtifactsFS wraps a filesystem (read-only access) of a forge-artifacts bundle.
27+
// The root contains directories for every artifact,
28+
// each containing one or more entries (one per solidity compiler version) for a solidity contract.
29+
// See OpenArtifactsDir for reading from a local directory.
30+
// Alternative FS systems, like a tarball, may be used too.
31+
type ArtifactsFS struct {
32+
FS statDirFs
33+
}
34+
35+
func (af *ArtifactsFS) ListArtifacts() ([]string, error) {
36+
entries, err := af.FS.ReadDir(".")
37+
if err != nil {
38+
return nil, fmt.Errorf("failed to list artifacts: %w", err)
39+
}
40+
out := make([]string, 0, len(entries))
41+
for _, d := range entries {
42+
if name := d.Name(); strings.HasSuffix(name, ".sol") {
43+
out = append(out, strings.TrimSuffix(name, ".sol"))
44+
}
45+
}
46+
return out, nil
47+
}
48+
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".
51+
func (af *ArtifactsFS) ListContracts(name string) ([]string, error) {
52+
f, err := af.FS.Open(name + ".sol")
53+
if err != nil {
54+
return nil, fmt.Errorf("failed to open artifact %q: %w", name, err)
55+
}
56+
defer f.Close()
57+
dirFile, ok := f.(fs.ReadDirFile)
58+
if !ok {
59+
return nil, fmt.Errorf("no dir for artifact %q, but got %T", name, f)
60+
}
61+
entries, err := dirFile.ReadDir(0)
62+
if err != nil {
63+
return nil, fmt.Errorf("failed to list artifact contents of %q: %w", name, err)
64+
}
65+
out := make([]string, 0, len(entries))
66+
for _, d := range entries {
67+
if name := d.Name(); strings.HasSuffix(name, ".json") {
68+
out = append(out, strings.TrimSuffix(name, ".json"))
69+
}
70+
}
71+
return out, nil
72+
}
73+
74+
// ReadArtifact reads a specific JSON contract artifact from the FS.
75+
// The contract name may be suffixed by a solidity compiler version, e.g. "Owned.0.8.25".
76+
func (af *ArtifactsFS) ReadArtifact(name string, contract string) (*Artifact, error) {
77+
artifactPath := path.Join(name+".sol", contract+".json")
78+
f, err := af.FS.Open(artifactPath)
79+
if err != nil {
80+
return nil, fmt.Errorf("failed to open artifact %q: %w", artifactPath, err)
81+
}
82+
defer f.Close()
83+
dec := json.NewDecoder(f)
84+
var out Artifact
85+
if err := dec.Decode(&out); err != nil {
86+
return nil, fmt.Errorf("failed to decode artifact %q: %w", name, err)
87+
}
88+
return &out, nil
89+
}
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package foundry
2+
3+
import (
4+
"errors"
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
"github.com/ethereum/go-ethereum/log"
10+
11+
"github.com/ethereum-optimism/optimism/op-service/testlog"
12+
)
13+
14+
func TestArtifacts(t *testing.T) {
15+
logger := testlog.Logger(t, log.LevelWarn) // lower this log level to get verbose test dump of all artifacts
16+
af := OpenArtifactsDir("./testdata/forge-artifacts")
17+
artifacts, err := af.ListArtifacts()
18+
require.NoError(t, err)
19+
require.NotEmpty(t, artifacts)
20+
for _, name := range artifacts {
21+
contracts, err := af.ListContracts(name)
22+
require.NoError(t, err, "failed to list %s", name)
23+
require.NotEmpty(t, contracts)
24+
for _, contract := range contracts {
25+
artifact, err := af.ReadArtifact(name, contract)
26+
if err != nil {
27+
if errors.Is(err, ErrLinkingUnsupported) {
28+
logger.Info("linking not supported", "name", name, "contract", contract, "err", err)
29+
continue
30+
}
31+
require.NoError(t, err, "failed to read artifact %s / %s", name, contract)
32+
}
33+
logger.Info("artifact",
34+
"name", name,
35+
"contract", contract,
36+
"compiler", artifact.Metadata.Compiler.Version,
37+
"sources", len(artifact.Metadata.Sources),
38+
"evmVersion", artifact.Metadata.Settings.EVMVersion,
39+
)
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)