diff --git a/.gitignore b/.gitignore index dfb1a31af823..9751cc608985 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ dist artifacts cache +!op-chain-ops/foundry/testdata/srcmaps/cache +!op-chain-ops/foundry/testdata/srcmaps/artifacts + packages/contracts-bedrock/deployments/devnetL1 packages/contracts-bedrock/deployments/anvil diff --git a/op-chain-ops/foundry/artifactsfs.go b/op-chain-ops/foundry/artifactsfs.go index 278404bcbcfe..2b5599c85c20 100644 --- a/op-chain-ops/foundry/artifactsfs.go +++ b/op-chain-ops/foundry/artifactsfs.go @@ -32,6 +32,9 @@ type ArtifactsFS struct { FS statDirFs } +// ListArtifacts lists the artifacts. Each artifact matches a source-file name. +// This name includes the extension, e.g. ".sol" +// (no other artifact-types are supported at this time). func (af *ArtifactsFS) ListArtifacts() ([]string, error) { entries, err := af.FS.ReadDir(".") if err != nil { @@ -39,17 +42,19 @@ func (af *ArtifactsFS) ListArtifacts() ([]string, error) { } out := make([]string, 0, len(entries)) for _, d := range entries { + // Some artifacts may be nested in directories not suffixed with ".sol" + // Nested artifacts, and non-solidity artifacts, are not supported. if name := d.Name(); strings.HasSuffix(name, ".sol") { - out = append(out, strings.TrimSuffix(name, ".sol")) + out = append(out, d.Name()) } } return out, nil } -// ListContracts lists the contracts of the named artifact. -// E.g. "Owned" might list "Owned.0.8.15", "Owned.0.8.25", and "Owned". +// ListContracts lists the contracts of the named artifact, including the file extension. +// E.g. "Owned.sol" might list "Owned.0.8.15", "Owned.0.8.25", and "Owned". func (af *ArtifactsFS) ListContracts(name string) ([]string, error) { - f, err := af.FS.Open(name + ".sol") + f, err := af.FS.Open(name) if err != nil { return nil, fmt.Errorf("failed to open artifact %q: %w", name, err) } @@ -73,8 +78,10 @@ func (af *ArtifactsFS) ListContracts(name string) ([]string, error) { // ReadArtifact reads a specific JSON contract artifact from the FS. // The contract name may be suffixed by a solidity compiler version, e.g. "Owned.0.8.25". +// The contract name does not include ".json", this is a detail internal to the artifacts. +// The name of the artifact is the source-file name, this must include the suffix such as ".sol". func (af *ArtifactsFS) ReadArtifact(name string, contract string) (*Artifact, error) { - artifactPath := path.Join(name+".sol", contract+".json") + artifactPath := path.Join(name, contract+".json") f, err := af.FS.Open(artifactPath) if err != nil { return nil, fmt.Errorf("failed to open artifact %q: %w", artifactPath, err) diff --git a/op-chain-ops/foundry/sourcefs.go b/op-chain-ops/foundry/sourcefs.go new file mode 100644 index 000000000000..964794e5fd65 --- /dev/null +++ b/op-chain-ops/foundry/sourcefs.go @@ -0,0 +1,147 @@ +package foundry + +import ( + "encoding/json" + "errors" + "fmt" + "io/fs" + "path" + "path/filepath" + "strings" + + "golang.org/x/exp/maps" + + "github.com/ethereum-optimism/optimism/op-chain-ops/srcmap" +) + +// SourceMapFS wraps an FS to provide source-maps. +// This FS relies on the following file path assumptions: +// - `/artifacts/build-info/X.json` (build-info path is read from the below file): build files, of foundry incremental builds. +// - `/cache/solidity-files-cache.json`: a JSON file enumerating all files, and when the build last changed. +// - `/` a root dir, relative to where the source files are located (as per the compilationTarget metadata in an artifact). +type SourceMapFS struct { + fs fs.FS +} + +// NewSourceMapFS creates a new SourceMapFS. +// The source-map FS loads identifiers for srcmap.ParseSourceMap +// and provides a util to retrieve a source-map for an Artifact. +// The solidity source-files are lazy-loaded when using the produced sourcemap. +func NewSourceMapFS(fs fs.FS) *SourceMapFS { + return &SourceMapFS{fs: fs} +} + +// ForgeBuild represents the JSON content of a forge-build entry in the `artifacts/build-info` output. +type ForgeBuild struct { + ID string `json:"id"` // ID of the build itself + SourceIDToPath map[srcmap.SourceID]string `json:"source_id_to_path"` // srcmap ID to source filepath +} + +func (s *SourceMapFS) readBuild(buildInfoPath string, id string) (*ForgeBuild, error) { + buildPath := path.Join(buildInfoPath, id+".json") + f, err := s.fs.Open(buildPath) + if err != nil { + return nil, fmt.Errorf("failed to open build: %w", err) + } + defer f.Close() + var build ForgeBuild + if err := json.NewDecoder(f).Decode(&build); err != nil { + return nil, fmt.Errorf("failed to read build: %w", err) + } + return &build, nil +} + +// ForgeBuildEntry represents a JSON entry that links the build job of a contract source file. +type ForgeBuildEntry struct { + Path string `json:"path"` + BuildID string `json:"build_id"` +} + +// ForgeBuildInfo represents a JSON entry that enumerates the latest builds per contract per compiler version. +type ForgeBuildInfo struct { + // contract name -> solidity version -> build entry + Artifacts map[string]map[string]ForgeBuildEntry `json:"artifacts"` +} + +// ForgeBuildCache rep +type ForgeBuildCache struct { + Paths struct { + BuildInfos string `json:"build_infos"` + } `json:"paths"` + Files map[string]ForgeBuildInfo `json:"files"` +} + +func (s *SourceMapFS) readBuildCache() (*ForgeBuildCache, error) { + cachePath := path.Join("cache", "solidity-files-cache.json") + f, err := s.fs.Open(cachePath) + if err != nil { + return nil, fmt.Errorf("failed to open build cache: %w", err) + } + defer f.Close() + var buildCache ForgeBuildCache + if err := json.NewDecoder(f).Decode(&buildCache); err != nil { + return nil, fmt.Errorf("failed to read build cache: %w", err) + } + return &buildCache, nil +} + +// ReadSourceIDs reads the source-identifier to source file-path mapping that is needed to translate a source-map +// of the given contract, the given compiler version, and within the given source file path. +func (s *SourceMapFS) ReadSourceIDs(path string, contract string, compilerVersion string) (map[srcmap.SourceID]string, error) { + buildCache, err := s.readBuildCache() + if err != nil { + return nil, err + } + artifactBuilds, ok := buildCache.Files[path] + if !ok { + return nil, fmt.Errorf("no known builds for path %q", path) + } + byCompilerVersion, ok := artifactBuilds.Artifacts[contract] + if !ok { + return nil, fmt.Errorf("contract not found in artifact: %q", contract) + } + var buildEntry ForgeBuildEntry + if compilerVersion != "" { + entry, ok := byCompilerVersion[compilerVersion] + if !ok { + return nil, fmt.Errorf("no known build for compiler version: %q", compilerVersion) + } + buildEntry = entry + } else { + if len(byCompilerVersion) == 0 { + return nil, errors.New("no known build, unspecified compiler version") + } + if len(byCompilerVersion) > 1 { + return nil, fmt.Errorf("no compiler version specified, and more than one option: %s", strings.Join(maps.Keys(byCompilerVersion), ", ")) + } + for _, entry := range byCompilerVersion { + buildEntry = entry + } + } + build, err := s.readBuild(filepath.ToSlash(buildCache.Paths.BuildInfos), buildEntry.BuildID) + if err != nil { + return nil, fmt.Errorf("failed to read build %q of contract %q: %w", buildEntry.BuildID, contract, err) + } + return build.SourceIDToPath, nil +} + +// SourceMap retrieves a source-map for a given contract of a foundry Artifact. +func (s *SourceMapFS) SourceMap(artifact *Artifact, contract string) (*srcmap.SourceMap, error) { + srcPath := "" + for path, name := range artifact.Metadata.Settings.CompilationTarget { + if name == contract { + srcPath = path + break + } + } + if srcPath == "" { + return nil, fmt.Errorf("no known source path for contract %s in artifact", contract) + } + // The commit suffix is ignored, the core semver part is what is used in the resolution of builds. + basicCompilerVersion := strings.SplitN(artifact.Metadata.Compiler.Version, "+", 2)[0] + ids, err := s.ReadSourceIDs(srcPath, contract, basicCompilerVersion) + if err != nil { + return nil, fmt.Errorf("failed to read source IDs of %q: %w", srcPath, err) + } + return srcmap.ParseSourceMap(s.fs, ids, artifact.DeployedBytecode.Object, artifact.DeployedBytecode.SourceMap) +} diff --git a/op-chain-ops/foundry/sourcefs_test.go b/op-chain-ops/foundry/sourcefs_test.go new file mode 100644 index 000000000000..bb30a10ad1a2 --- /dev/null +++ b/op-chain-ops/foundry/sourcefs_test.go @@ -0,0 +1,25 @@ +package foundry + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +//go:generate ./testdata/srcmaps/generate.sh + +func TestSourceMapFS(t *testing.T) { + artifactFS := OpenArtifactsDir("./testdata/srcmaps/test-artifacts") + exampleArtifact, err := artifactFS.ReadArtifact("SimpleStorage.sol", "SimpleStorage") + require.NoError(t, err) + srcFS := NewSourceMapFS(os.DirFS("./testdata/srcmaps")) + srcMap, err := srcFS.SourceMap(exampleArtifact, "SimpleStorage") + require.NoError(t, err) + seenInfo := make(map[string]struct{}) + for i := range exampleArtifact.DeployedBytecode.Object { + seenInfo[srcMap.FormattedInfo(uint64(i))] = struct{}{} + } + require.Contains(t, seenInfo, "src/SimpleStorage.sol:11:5") + require.Contains(t, seenInfo, "src/StorageLibrary.sol:8:9") +} diff --git a/op-chain-ops/foundry/testdata/README.md b/op-chain-ops/foundry/testdata/README.md index 5e7a67f7c484..549103b7de3f 100644 --- a/op-chain-ops/foundry/testdata/README.md +++ b/op-chain-ops/foundry/testdata/README.md @@ -1,3 +1,3 @@ -# artifacts test data +# source-map test data -This is a small selection of `forge-artifacts` specifically for testing of Artifact decoding and the Artifacts-FS. +Simple small multi-contract forge setup, to test Go forge map functionality against. diff --git a/op-chain-ops/foundry/testdata/srcmaps/artifacts/build-info/c79aa2c3b4578aee2dd8f02d20b1aeb6.json b/op-chain-ops/foundry/testdata/srcmaps/artifacts/build-info/c79aa2c3b4578aee2dd8f02d20b1aeb6.json new file mode 100644 index 000000000000..59cd6663b18f --- /dev/null +++ b/op-chain-ops/foundry/testdata/srcmaps/artifacts/build-info/c79aa2c3b4578aee2dd8f02d20b1aeb6.json @@ -0,0 +1 @@ +{"id":"c79aa2c3b4578aee2dd8f02d20b1aeb6","source_id_to_path":{"0":"src/SimpleStorage.sol","1":"src/StorageLibrary.sol"},"language":"Solidity"} \ No newline at end of file diff --git a/op-chain-ops/foundry/testdata/srcmaps/cache/solidity-files-cache.json b/op-chain-ops/foundry/testdata/srcmaps/cache/solidity-files-cache.json new file mode 100644 index 000000000000..47bdef8c6965 --- /dev/null +++ b/op-chain-ops/foundry/testdata/srcmaps/cache/solidity-files-cache.json @@ -0,0 +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"]} \ No newline at end of file diff --git a/op-chain-ops/foundry/testdata/srcmaps/foundry.toml b/op-chain-ops/foundry/testdata/srcmaps/foundry.toml new file mode 100644 index 000000000000..c321ef0af22a --- /dev/null +++ b/op-chain-ops/foundry/testdata/srcmaps/foundry.toml @@ -0,0 +1,31 @@ +################################################################ +# PROFILE: DEFAULT (Local) # +################################################################ + +[profile.default] + +# Compilation settings +src = 'src' +out = 'test-artifacts' +script = 'scripts' +optimizer = true +optimizer_runs = 999999 +remappings = [] +extra_output = ['devdoc', 'userdoc', 'metadata', 'storageLayout'] +bytecode_hash = 'none' +build_info_path = 'artifacts/build-info' +ast = true +evm_version = "cancun" +# 5159 error code is selfdestruct error code +ignored_error_codes = ["transient-storage", "code-size", "init-code-size", 5159] + +# We set the gas limit to max int64 to avoid running out of gas during testing, since the default +# gas limit is 1B and some of our tests require more gas than that, such as `test_callWithMinGas_noLeakageLow_succeeds`. +# We use this gas limit since it was the default gas limit prior to https://github.com/foundry-rs/foundry/pull/8274. +# Due to toml-rs limitations, if you increase the gas limit above this value it must be a string. +gas_limit = 9223372036854775807 + +# Test / Script Runner Settings +ffi = false +fs_permissions = [] +libs = ["node_modules", "lib"] diff --git a/op-chain-ops/foundry/testdata/srcmaps/generate.sh b/op-chain-ops/foundry/testdata/srcmaps/generate.sh new file mode 100755 index 000000000000..f63cb05ed60d --- /dev/null +++ b/op-chain-ops/foundry/testdata/srcmaps/generate.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +set -euo + +# Don't include previous build outputs +forge clean + +forge build diff --git a/op-chain-ops/foundry/testdata/srcmaps/src/SimpleStorage.sol b/op-chain-ops/foundry/testdata/srcmaps/src/SimpleStorage.sol new file mode 100644 index 000000000000..05477b6e747d --- /dev/null +++ b/op-chain-ops/foundry/testdata/srcmaps/src/SimpleStorage.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import {StorageLibrary} from "./StorageLibrary.sol"; + +// @notice SimpleStorage is a contract to test Go <> foundry integration. +// @dev uses a dependency, to test source-mapping with multiple sources. +contract SimpleStorage { + + // @dev example getter + function getExampleData() public pure returns (uint256) { + return StorageLibrary.addData(42); + } +} diff --git a/op-chain-ops/foundry/testdata/srcmaps/src/StorageLibrary.sol b/op-chain-ops/foundry/testdata/srcmaps/src/StorageLibrary.sol new file mode 100644 index 000000000000..049b03ac2f8e --- /dev/null +++ b/op-chain-ops/foundry/testdata/srcmaps/src/StorageLibrary.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +// @notice StorageLibrary is an example library used for integration testing. +library StorageLibrary { + + function addData(uint256 _data) internal pure returns (uint256) { + return _data + 123; + } + +} + diff --git a/op-chain-ops/foundry/testdata/srcmaps/test-artifacts/SimpleStorage.sol/SimpleStorage.json b/op-chain-ops/foundry/testdata/srcmaps/test-artifacts/SimpleStorage.sol/SimpleStorage.json new file mode 100644 index 000000000000..f6aed9db421f --- /dev/null +++ b/op-chain-ops/foundry/testdata/srcmaps/test-artifacts/SimpleStorage.sol/SimpleStorage.json @@ -0,0 +1 @@ +{"abi":[{"type":"function","name":"getExampleData","inputs":[],"outputs":[{"name":"","type":"uint256","internalType":"uint256"}],"stateMutability":"pure"}],"bytecode":{"object":"0x608060405234801561001057600080fd5b5060b08061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063b5bc337e14602d575b600080fd5b60336045565b60405190815260200160405180910390f35b6000604f602a6054565b905090565b6000605f82607b6065565b92915050565b60008219821115609e577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b50019056fea164736f6c634300080f000a","sourceMap":"258:165:0:-:0;;;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x6080604052348015600f57600080fd5b506004361060285760003560e01c8063b5bc337e14602d575b600080fd5b60336045565b60405190815260200160405180910390f35b6000604f602a6054565b905090565b6000605f82607b6065565b92915050565b60008219821115609e577f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b50019056fea164736f6c634300080f000a","sourceMap":"258:165:0:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;315:106;;;:::i;:::-;;;160:25:2;;;148:2;133:18;315:106:0;;;;;;;;362:7;388:26;411:2;388:22;:26::i;:::-;381:33;;315:106;:::o;165:99:1:-;220:7;246:11;:5;254:3;246:11;:::i;:::-;239:18;165:99;-1:-1:-1;;165:99:1:o;196:282:2:-;236:3;267:1;263:6;260:1;257:13;254:193;;;303:77;300:1;293:88;404:4;401:1;394:15;432:4;429:1;422:15;254:193;-1:-1:-1;463:9:2;;196:282::o","linkReferences":{}},"methodIdentifiers":{"getExampleData()":"b5bc337e"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.15+commit.e14f2714\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[],\"name\":\"getExampleData\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"\",\"type\":\"uint256\"}],\"stateMutability\":\"pure\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"src/SimpleStorage.sol\":\"SimpleStorage\"},\"evmVersion\":\"london\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":999999},\"remappings\":[]},\"sources\":{\"src/SimpleStorage.sol\":{\"keccak256\":\"0x72903094842a1afc1a226391c402411969dc9736b4aa71222a620bfa5a712e91\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://59876be2e0433af20a46cd8d00de4f461f1f75e53c79ff821787684329077812\",\"dweb:/ipfs/QmXzGm3ka8PoUoD7kyEv77babg49uo3TZFMfcWUYrA9QTJ\"]},\"src/StorageLibrary.sol\":{\"keccak256\":\"0x29bbbc60bf5a5f414ff1bf0198d06e007b193071767991a7ae92fd0683bf63b3\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://83ec033aadcafafc309c246e6fd83d75ceb77eef37658a4876d61b7436bd4a7d\",\"dweb:/ipfs/QmQg2wwT5xfm1yMetzioBccKg6nEv5bhRsrmZC69Z9QN8F\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.15+commit.e14f2714"},"language":"Solidity","output":{"abi":[{"inputs":[],"stateMutability":"pure","type":"function","name":"getExampleData","outputs":[{"internalType":"uint256","name":"","type":"uint256"}]}],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":[],"optimizer":{"enabled":true,"runs":999999},"metadata":{"bytecodeHash":"none"},"compilationTarget":{"src/SimpleStorage.sol":"SimpleStorage"},"evmVersion":"london","libraries":{}},"sources":{"src/SimpleStorage.sol":{"keccak256":"0x72903094842a1afc1a226391c402411969dc9736b4aa71222a620bfa5a712e91","urls":["bzz-raw://59876be2e0433af20a46cd8d00de4f461f1f75e53c79ff821787684329077812","dweb:/ipfs/QmXzGm3ka8PoUoD7kyEv77babg49uo3TZFMfcWUYrA9QTJ"],"license":"MIT"},"src/StorageLibrary.sol":{"keccak256":"0x29bbbc60bf5a5f414ff1bf0198d06e007b193071767991a7ae92fd0683bf63b3","urls":["bzz-raw://83ec033aadcafafc309c246e6fd83d75ceb77eef37658a4876d61b7436bd4a7d","dweb:/ipfs/QmQg2wwT5xfm1yMetzioBccKg6nEv5bhRsrmZC69Z9QN8F"],"license":"MIT"}},"version":1},"storageLayout":{"storage":[],"types":{}},"userdoc":{"version":1,"kind":"user"},"devdoc":{"version":1,"kind":"dev"},"ast":{"absolutePath":"src/SimpleStorage.sol","id":16,"exportedSymbols":{"SimpleStorage":[15],"StorageLibrary":[30]},"nodeType":"SourceUnit","src":"32:392:0","nodes":[{"id":1,"nodeType":"PragmaDirective","src":"32:23:0","nodes":[],"literals":["solidity","0.8",".15"]},{"id":3,"nodeType":"ImportDirective","src":"57:52:0","nodes":[],"absolutePath":"src/StorageLibrary.sol","file":"./StorageLibrary.sol","nameLocation":"-1:-1:-1","scope":16,"sourceUnit":31,"symbolAliases":[{"foreign":{"id":2,"name":"StorageLibrary","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":30,"src":"65:14:0","typeDescriptions":{}},"nameLocation":"-1:-1:-1"}],"unitAlias":""},{"id":15,"nodeType":"ContractDefinition","src":"258:165:0","nodes":[{"id":14,"nodeType":"FunctionDefinition","src":"315:106:0","nodes":[],"body":{"id":13,"nodeType":"Block","src":"371:50:0","nodes":[],"statements":[{"expression":{"arguments":[{"hexValue":"3432","id":10,"isConstant":false,"isLValue":false,"isPure":true,"kind":"number","lValueRequested":false,"nodeType":"Literal","src":"411:2:0","typeDescriptions":{"typeIdentifier":"t_rational_42_by_1","typeString":"int_const 42"},"value":"42"}],"expression":{"argumentTypes":[{"typeIdentifier":"t_rational_42_by_1","typeString":"int_const 42"}],"expression":{"id":8,"name":"StorageLibrary","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":30,"src":"388:14:0","typeDescriptions":{"typeIdentifier":"t_type$_t_contract$_StorageLibrary_$30_$","typeString":"type(library StorageLibrary)"}},"id":9,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"memberName":"addData","nodeType":"MemberAccess","referencedDeclaration":29,"src":"388:22:0","typeDescriptions":{"typeIdentifier":"t_function_internal_pure$_t_uint256_$returns$_t_uint256_$","typeString":"function (uint256) pure returns (uint256)"}},"id":11,"isConstant":false,"isLValue":false,"isPure":false,"kind":"functionCall","lValueRequested":false,"names":[],"nodeType":"FunctionCall","src":"388:26:0","tryCall":false,"typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"functionReturnParameters":7,"id":12,"nodeType":"Return","src":"381:33:0"}]},"functionSelector":"b5bc337e","implemented":true,"kind":"function","modifiers":[],"name":"getExampleData","nameLocation":"324:14:0","parameters":{"id":4,"nodeType":"ParameterList","parameters":[],"src":"338:2:0"},"returnParameters":{"id":7,"nodeType":"ParameterList","parameters":[{"constant":false,"id":6,"mutability":"mutable","name":"","nameLocation":"-1:-1:-1","nodeType":"VariableDeclaration","scope":14,"src":"362:7:0","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"},"typeName":{"id":5,"name":"uint256","nodeType":"ElementaryTypeName","src":"362:7:0","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"visibility":"internal"}],"src":"361:9:0"},"scope":15,"stateMutability":"pure","virtual":false,"visibility":"public"}],"abstract":false,"baseContracts":[],"canonicalName":"SimpleStorage","contractDependencies":[],"contractKind":"contract","fullyImplemented":true,"linearizedBaseContracts":[15],"name":"SimpleStorage","nameLocation":"267:13:0","scope":16,"usedErrors":[]}],"license":"MIT"},"id":0} \ No newline at end of file diff --git a/op-chain-ops/foundry/testdata/srcmaps/test-artifacts/StorageLibrary.sol/StorageLibrary.json b/op-chain-ops/foundry/testdata/srcmaps/test-artifacts/StorageLibrary.sol/StorageLibrary.json new file mode 100644 index 000000000000..c27a93db99ee --- /dev/null +++ b/op-chain-ops/foundry/testdata/srcmaps/test-artifacts/StorageLibrary.sol/StorageLibrary.json @@ -0,0 +1 @@ +{"abi":[],"bytecode":{"object":"0x602d6037600b82828239805160001a607314602a57634e487b7160e01b600052600060045260246000fd5b30600052607381538281f3fe73000000000000000000000000000000000000000030146080604052600080fdfea164736f6c634300080f000a","sourceMap":"135:132:1:-:0;;;;;;;;;;;;;;;-1:-1:-1;;;135:132:1;;;;;;;;;;;;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x73000000000000000000000000000000000000000030146080604052600080fdfea164736f6c634300080f000a","sourceMap":"135:132:1:-:0;;;;;;;;","linkReferences":{}},"methodIdentifiers":{},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.15+commit.e14f2714\"},\"language\":\"Solidity\",\"output\":{\"abi\":[],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"src/StorageLibrary.sol\":\"StorageLibrary\"},\"evmVersion\":\"london\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":999999},\"remappings\":[]},\"sources\":{\"src/StorageLibrary.sol\":{\"keccak256\":\"0x29bbbc60bf5a5f414ff1bf0198d06e007b193071767991a7ae92fd0683bf63b3\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://83ec033aadcafafc309c246e6fd83d75ceb77eef37658a4876d61b7436bd4a7d\",\"dweb:/ipfs/QmQg2wwT5xfm1yMetzioBccKg6nEv5bhRsrmZC69Z9QN8F\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.15+commit.e14f2714"},"language":"Solidity","output":{"abi":[],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":[],"optimizer":{"enabled":true,"runs":999999},"metadata":{"bytecodeHash":"none"},"compilationTarget":{"src/StorageLibrary.sol":"StorageLibrary"},"evmVersion":"london","libraries":{}},"sources":{"src/StorageLibrary.sol":{"keccak256":"0x29bbbc60bf5a5f414ff1bf0198d06e007b193071767991a7ae92fd0683bf63b3","urls":["bzz-raw://83ec033aadcafafc309c246e6fd83d75ceb77eef37658a4876d61b7436bd4a7d","dweb:/ipfs/QmQg2wwT5xfm1yMetzioBccKg6nEv5bhRsrmZC69Z9QN8F"],"license":"MIT"}},"version":1},"storageLayout":{"storage":[],"types":{}},"userdoc":{"version":1,"kind":"user"},"devdoc":{"version":1,"kind":"dev"},"ast":{"absolutePath":"src/StorageLibrary.sol","id":31,"exportedSymbols":{"StorageLibrary":[30]},"nodeType":"SourceUnit","src":"32:237:1","nodes":[{"id":17,"nodeType":"PragmaDirective","src":"32:23:1","nodes":[],"literals":["solidity","0.8",".15"]},{"id":30,"nodeType":"ContractDefinition","src":"135:132:1","nodes":[{"id":29,"nodeType":"FunctionDefinition","src":"165:99:1","nodes":[],"body":{"id":28,"nodeType":"Block","src":"229:35:1","nodes":[],"statements":[{"expression":{"commonType":{"typeIdentifier":"t_uint256","typeString":"uint256"},"id":26,"isConstant":false,"isLValue":false,"isPure":false,"lValueRequested":false,"leftExpression":{"id":24,"name":"_data","nodeType":"Identifier","overloadedDeclarations":[],"referencedDeclaration":19,"src":"246:5:1","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"nodeType":"BinaryOperation","operator":"+","rightExpression":{"hexValue":"313233","id":25,"isConstant":false,"isLValue":false,"isPure":true,"kind":"number","lValueRequested":false,"nodeType":"Literal","src":"254:3:1","typeDescriptions":{"typeIdentifier":"t_rational_123_by_1","typeString":"int_const 123"},"value":"123"},"src":"246:11:1","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"functionReturnParameters":23,"id":27,"nodeType":"Return","src":"239:18:1"}]},"implemented":true,"kind":"function","modifiers":[],"name":"addData","nameLocation":"174:7:1","parameters":{"id":20,"nodeType":"ParameterList","parameters":[{"constant":false,"id":19,"mutability":"mutable","name":"_data","nameLocation":"190:5:1","nodeType":"VariableDeclaration","scope":29,"src":"182:13:1","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"},"typeName":{"id":18,"name":"uint256","nodeType":"ElementaryTypeName","src":"182:7:1","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"visibility":"internal"}],"src":"181:15:1"},"returnParameters":{"id":23,"nodeType":"ParameterList","parameters":[{"constant":false,"id":22,"mutability":"mutable","name":"","nameLocation":"-1:-1:-1","nodeType":"VariableDeclaration","scope":29,"src":"220:7:1","stateVariable":false,"storageLocation":"default","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"},"typeName":{"id":21,"name":"uint256","nodeType":"ElementaryTypeName","src":"220:7:1","typeDescriptions":{"typeIdentifier":"t_uint256","typeString":"uint256"}},"visibility":"internal"}],"src":"219:9:1"},"scope":30,"stateMutability":"pure","virtual":false,"visibility":"internal"}],"abstract":false,"baseContracts":[],"canonicalName":"StorageLibrary","contractDependencies":[],"contractKind":"library","fullyImplemented":true,"linearizedBaseContracts":[30],"name":"StorageLibrary","nameLocation":"143:14:1","scope":31,"usedErrors":[]}],"license":"MIT"},"id":1} \ No newline at end of file diff --git a/op-chain-ops/script/script_test.go b/op-chain-ops/script/script_test.go index 8dcebf420d73..f9127411e2bf 100644 --- a/op-chain-ops/script/script_test.go +++ b/op-chain-ops/script/script_test.go @@ -20,7 +20,7 @@ func TestScript(t *testing.T) { scriptContext := DefaultContext h := NewHost(logger, af, scriptContext) - addr, err := h.LoadContract("ScriptExample.s", "ScriptExample") + addr, err := h.LoadContract("ScriptExample.s.sol", "ScriptExample") require.NoError(t, err) require.NoError(t, h.EnableCheats()) diff --git a/op-chain-ops/srcmap/solutil.go b/op-chain-ops/srcmap/solutil.go index 9944db558710..a5d573fa74a1 100644 --- a/op-chain-ops/srcmap/solutil.go +++ b/op-chain-ops/srcmap/solutil.go @@ -3,7 +3,7 @@ package srcmap import ( "fmt" "io" - "os" + "io/fs" "strconv" "strings" @@ -69,84 +69,113 @@ func parseInstrMapping(last InstrMapping, v string) (InstrMapping, error) { return out, err } +func loadLineColData(srcFs fs.FS, srcPath string) ([]LineCol, error) { + dat, err := fs.ReadFile(srcFs, srcPath) + if err != nil { + return nil, fmt.Errorf("failed to read source %q: %w", srcPath, err) + } + datStr := string(dat) + out := make([]LineCol, len(datStr)) + line := uint32(1) + lastLinePos := uint32(0) + for i, b := range datStr { // iterate the utf8 or the bytes? + col := uint32(i) - lastLinePos + out[i] = LineCol{Line: line, Col: col} + if b == '\n' { + lastLinePos = uint32(i) + line += 1 + } + } + return out, nil +} + +type SourceID uint64 + +func (id *SourceID) UnmarshalText(data []byte) error { + v, err := strconv.ParseUint(string(data), 10, 64) + if err != nil { + return err + } + *id = SourceID(v) + return nil +} + +// SourceMap is a util to map solidity deployed-bytecode positions +// to source-file, line and column position data. +// It is best used in combination with foundry.SourceMapFS to load the source-map. +// The source-map functionality is tested as part of the FS. type SourceMap struct { - // source names - Sources []string + srcFs fs.FS + srcIDToPath map[SourceID]string // per source, source offset -> line/col - PosData [][]LineCol + // This data is lazy-loaded. + PosData map[SourceID][]LineCol // per bytecode byte, byte index -> instr Instr []InstrMapping } -func (s *SourceMap) Info(pc uint64) (source string, line uint32, col uint32) { +// Info translates a program-counter (execution position in the EVM bytecode) +// into the source-code location that is being executed. +// This location is the source file-path, the line number, and column number. +// This may return an error, as the source-file is lazy-loaded to calculate the position data. +func (s *SourceMap) Info(pc uint64) (source string, line uint32, col uint32, err error) { instr := s.Instr[pc] - if instr.F < 0 { - return "generated", 0, 0 + if instr.F < 0 || instr == (InstrMapping{}) { + return "generated", 0, 0, nil } - if instr.F >= int32(len(s.Sources)) { + id := SourceID(instr.F) + if _, ok := s.srcIDToPath[id]; !ok { source = "unknown" return } - source = s.Sources[instr.F] + source = s.srcIDToPath[id] if instr.S < 0 { return } - if s.PosData[instr.F] == nil { // when the source file is known to be unavailable - return + posData, ok := s.PosData[id] + if !ok { + data, loadErr := loadLineColData(s.srcFs, source) + if loadErr != nil { + return source, 0, 0, loadErr + } + s.PosData[id] = data + posData = data } - if int(instr.S) >= len(s.PosData[instr.F]) { // possibly invalid / truncated source mapping + if int(instr.S) >= len(posData) { // possibly invalid / truncated source mapping return } - lc := s.PosData[instr.F][instr.S] + lc := posData[instr.S] line = lc.Line col = lc.Col return } +// FormattedInfo is a convenience method to run Info, and turn it into a formatted string. +// Any error is turned into a string also, to make this simple to plug into logging. func (s *SourceMap) FormattedInfo(pc uint64) string { - f, l, c := s.Info(pc) + f, l, c, err := s.Info(pc) + if err != nil { + return "srcmap err:" + err.Error() + } return fmt.Sprintf("%s:%d:%d", f, l, c) } // ParseSourceMap parses a solidity sourcemap: mapping bytecode indices to source references. // See https://docs.soliditylang.org/en/latest/internals/source_mappings.html // -// Sources is the list of source files, which will be read from the filesystem -// to transform token numbers into line/column numbers. -// The sources are as referenced in the source-map by index. -// Not all sources are necessary, some indices may be unknown. -func ParseSourceMap(sources []string, bytecode []byte, sourceMap string) (*SourceMap, error) { +// The srcIDToPath is the mapping of source files, which will be read from the filesystem +// to transform token numbers into line/column numbers. Source-files are lazy-loaded when needed. +// +// The source identifier mapping can be loaded through a foundry.SourceMapFS, +// also including a convenience util to load a source-map from an artifact. +func ParseSourceMap(srcFs fs.FS, srcIDToPath map[SourceID]string, bytecode []byte, sourceMap string) (*SourceMap, error) { instructions := strings.Split(sourceMap, ";") srcMap := &SourceMap{ - Sources: sources, - PosData: make([][]LineCol, 0, len(sources)), - Instr: make([]InstrMapping, 0, len(bytecode)), - } - // map source code position byte offsets to line/column pairs - for i, s := range sources { - if strings.HasPrefix(s, "~") { - srcMap.PosData = append(srcMap.PosData, nil) - continue - } - dat, err := os.ReadFile(s) - if err != nil { - return nil, fmt.Errorf("failed to read source %d %q: %w", i, s, err) - } - datStr := string(dat) - - out := make([]LineCol, len(datStr)) - line := uint32(1) - lastLinePos := uint32(0) - for i, b := range datStr { // iterate the utf8 or the bytes? - col := uint32(i) - lastLinePos - out[i] = LineCol{Line: line, Col: col} - if b == '\n' { - lastLinePos = uint32(i) - line += 1 - } - } - srcMap.PosData = append(srcMap.PosData, out) + srcFs: srcFs, + srcIDToPath: srcIDToPath, + PosData: make(map[SourceID][]LineCol), + Instr: make([]InstrMapping, 0, len(bytecode)), } instIndex := 0 @@ -168,6 +197,7 @@ func ParseSourceMap(sources []string, bytecode []byte, sourceMap string) (*Sourc } else { instMapping = instructions[instIndex] } + // the last instruction is used to de-dup data with in the source-map encoding. m, err := parseInstrMapping(lastInstr, instMapping) if err != nil { return nil, fmt.Errorf("failed to parse instr element in source map: %w", err) @@ -176,6 +206,7 @@ func ParseSourceMap(sources []string, bytecode []byte, sourceMap string) (*Sourc for j := 0; j < instLen; j++ { srcMap.Instr = append(srcMap.Instr, m) } + lastInstr = m i += instLen instIndex += 1 } diff --git a/op-chain-ops/srcmap/solutil_test.go b/op-chain-ops/srcmap/solutil_test.go deleted file mode 100644 index e607d7e4ddc9..000000000000 --- a/op-chain-ops/srcmap/solutil_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package srcmap - -import ( - "testing" -) - -func TestSourcemap(t *testing.T) { - t.Skip("TODO(clabby): This test is disabled until source IDs have been added to foundry artifacts.") - - // contractsDir := "../../packages/contracts-bedrock" - // sources := []string{path.Join(contractsDir, "src/cannon/MIPS.sol")} - // for i, source := range sources { - // sources[i] = path.Join(contractsDir, source) - // } - // - // deployedByteCode := hexutil.MustDecode(bindings.MIPSDeployedBin) - // srcMap, err := ParseSourceMap( - // sources, - // deployedByteCode, - // bindings.MIPSDeployedSourceMap) - // require.NoError(t, err) - // - // for i := 0; i < len(deployedByteCode); i++ { - // info := srcMap.FormattedInfo(uint64(i)) - // if strings.HasPrefix(info, "unknown") { - // t.Fatalf("unexpected info: %q", info) - // } - // } -}