Commit 36279371 authored by protolambda's avatar protolambda Committed by GitHub

op-chain-ops: source maps fixes + FS (#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
parent 9cd71a51
......@@ -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
......
......@@ -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)
......
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)
}
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")
}
# 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.
{"id":"c79aa2c3b4578aee2dd8f02d20b1aeb6","source_id_to_path":{"0":"src/SimpleStorage.sol","1":"src/StorageLibrary.sol"},"language":"Solidity"}
\ No newline at end of file
{"_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
################################################################
# 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"]
#!/bin/sh
set -euo
# Don't include previous build outputs
forge clean
forge build
// 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);
}
}
// 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;
}
}
{"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
{"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
......@@ -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())
......
......@@ -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
}
......
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)
// }
// }
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment