Skip to content

Commit 61e3b3c

Browse files
authored
Add --experimental_src_map to ethier gen command (#35)
* Solidity source mapping from OpCodes at runtime! * Fix test failure introduced by upgrade of go-ethereum + bring new test code in line with implementation variable names. * Run prettier on Solidity test contract * Properly handle source-code resolution from EVM trace when executing libraries. * GitHub test Action is failing despite tests passing so attempting to change the node_modules cache key to clear it. * Revert GitHub Action cache key and change testing to non-verbose as it hides failures; also output `solc` + `abigen` versions. * Deliberate panic in ethier/gen.go to diagnose difference from local run to GitHub Actions * A further deliberate panic in ethier/gen.go to see full output; abigen differs and regexp is a bad idea here because it's too fragile * Update ethier/gen.go to match output of old and new versions of abigen after ethereum/go-ethereum#24835. See the TODO in ethier/gen.go re direct modification of the bind.Bind() template. * Simplify calculation of PUSH<N> instruction offset
1 parent 4411623 commit 61e3b3c

File tree

12 files changed

+812
-52
lines changed

12 files changed

+812
-52
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@ jobs:
4040
sudo apt-get install -y solc abigen
4141
4242
- name: Run tests
43-
run: npm run test:verbose
43+
run: npm run test

eth/signer_test.go

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/google/tink/go/keyset"
1515
"github.com/google/tink/go/prf"
1616
"github.com/google/tink/go/tink"
17+
"github.com/h-fam/errdiff"
1718

1819
// These tests require ethtest.SimulatedBackend but that would result in a
1920
// cyclical dependency. As this is limited to these tests and not the
@@ -161,19 +162,19 @@ func TestTransactorWithChainID(t *testing.T) {
161162
const gasLimit = 21000
162163
txFee := new(big.Int).Mul(gasPrice, big.NewInt(gasLimit))
163164

164-
sendEth := func(t *testing.T, opts *bind.TransactOpts, to common.Address, value *big.Int) {
165+
sendEth := func(t *testing.T, opts *bind.TransactOpts, to common.Address, value *big.Int, errDiffAgainst interface{}) {
165166
t.Helper()
166167
unsigned := types.NewTransaction(0, to, value, gasLimit, gasPrice, nil)
167168
tx, err := opts.Signer(opts.From, unsigned)
168169
if err != nil {
169170
t.Fatalf("%T.Signer(%+v) error %v", opts, unsigned, err)
170171
}
171-
if err := sim.SendTransaction(ctx, tx); err != nil {
172-
t.Fatalf("%T.SendTransaction() error %v", sim, err)
172+
if diff := errdiff.Check(sim.SendTransaction(ctx, tx), errDiffAgainst); diff != "" {
173+
t.Fatalf("%T.SendTransaction() %s", sim, diff)
173174
}
174175
}
175176

176-
sendEth(t, sim.Acc(0), signer.Address(), Ether(42))
177+
sendEth(t, sim.Acc(0), signer.Address(), Ether(42), nil)
177178
wantBalance(ctx, t, "faucet after sending 42", sim.Addr(0), new(big.Int).Sub(Ether(100-42), txFee))
178179
wantBalance(ctx, t, "signer after receiving 42", signer.Address(), Ether(42))
179180

@@ -183,32 +184,17 @@ func TestTransactorWithChainID(t *testing.T) {
183184
if err != nil {
184185
t.Fatalf("%T.TransactorWithChainID(%d) error %v", signer, chainID, err)
185186
}
186-
sendEth(t, opts, sim.Addr(0), Ether(21))
187+
sendEth(t, opts, sim.Addr(0), Ether(21), nil)
187188
wantBalance(ctx, t, "faucet after sending 42 and receiving 21", sim.Addr(0), new(big.Int).Sub(Ether(100-42+21), txFee))
188189
wantBalance(ctx, t, "signer after receiving 42 and sending 21", signer.Address(), new(big.Int).Sub(Ether(42-21), txFee))
189190
})
190191

191192
t.Run("incorrect chain ID", func(t *testing.T) {
192-
// The SimulatedBackend panics instead of returning an error when the
193-
// chain ID is incorrect. #java
194-
defer func() {
195-
const wantContains = "invalid chain id"
196-
r := recover()
197-
198-
if err, ok := r.(error); ok && strings.Contains(err.Error(), wantContains) {
199-
return
200-
}
201-
t.Errorf("%T.SendTransaction(%T.TransactorWithChainID(<incorrect ID>)) recovered %T(%v); want panic with error containing %q", sim, signer, r, r, wantContains)
202-
}()
203-
204193
chainID := new(big.Int).Add(sim.Blockchain().Config().ChainID, big.NewInt(1))
205194
opts, err := signer.TransactorWithChainID(chainID)
206195
if err != nil {
207196
t.Fatalf("%T.TransactorWithChainID(%d) error %v", signer, chainID, err)
208197
}
209-
sendEth(t, opts, sim.Addr(0), Ether(1))
210-
// We should never reach here because sendEth results in a panic inside
211-
// go-ethereum's SimulatedBackend.
212-
t.Errorf("%T.SendTransaction(%T.TransactorWithChainID(<incorrect ID>)) did not panic", sim, signer)
198+
sendEth(t, opts, sim.Addr(0), Ether(1), "invalid chain id")
213199
})
214200
}

ethier/gen.go

Lines changed: 168 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,33 @@
11
package main
22

33
import (
4+
"bytes"
5+
"encoding/json"
46
"errors"
57
"fmt"
8+
"go/format"
9+
"go/parser"
10+
"go/token"
611
"io"
712
"log"
813
"os"
914
"os/exec"
1015
"path/filepath"
16+
"regexp"
1117
"strings"
18+
"text/template"
1219

20+
"github.com/ethereum/go-ethereum/common/compiler"
1321
"github.com/spf13/cobra"
22+
"golang.org/x/tools/go/ast/astutil"
23+
24+
_ "embed"
1425
)
1526

27+
const srcMapFlag = "experimental_src_map"
28+
1629
func init() {
17-
rootCmd.AddCommand(&cobra.Command{
30+
cmd := &cobra.Command{
1831
Use: "gen",
1932
Short: "Compiles Solidity contracts to generate Go ABI bindings with go:generate",
2033
RunE: gen,
@@ -29,12 +42,16 @@ func init() {
2942
}
3043
return nil
3144
},
32-
})
45+
}
46+
47+
cmd.Flags().Bool(srcMapFlag, false, "Generate source maps to determine Solidity code location from EVM traces")
48+
49+
rootCmd.AddCommand(cmd)
3350
}
3451

3552
// gen runs `solc | abigen` on the Solidity source files passed as the args.
3653
// TODO: support wildcard / glob matching of files.
37-
func gen(_ *cobra.Command, args []string) (retErr error) {
54+
func gen(cmd *cobra.Command, args []string) (retErr error) {
3855
pwd, err := os.Getwd()
3956
if err != nil {
4057
return fmt.Errorf("os.Getwd(): %v", err)
@@ -62,7 +79,7 @@ func gen(_ *cobra.Command, args []string) (retErr error) {
6279
args,
6380
"--base-path", basePath,
6481
"--include-path", filepath.Join(basePath, "node_modules"),
65-
"--combined-json", "abi,bin",
82+
"--combined-json", "abi,bin,bin-runtime,hashes,srcmap-runtime",
6683
)
6784
solc := exec.Command("solc", args...)
6885
solc.Stderr = os.Stderr
@@ -75,13 +92,16 @@ func gen(_ *cobra.Command, args []string) (retErr error) {
7592
"abigen",
7693
"--combined-json", "/dev/stdin",
7794
"--pkg", pkg,
78-
"--out", "generated.go",
7995
)
8096
abigen.Stderr = os.Stderr
8197

8298
r, w := io.Pipe()
8399
solc.Stdout = w
84-
abigen.Stdin = r
100+
combinedJSON := bytes.NewBuffer(nil)
101+
abigen.Stdin = io.TeeReader(r, combinedJSON)
102+
103+
generated := bytes.NewBuffer(nil)
104+
abigen.Stdout = generated
85105

86106
if err := solc.Start(); err != nil {
87107
return fmt.Errorf("start `solc`: %v", err)
@@ -99,5 +119,146 @@ func gen(_ *cobra.Command, args []string) (retErr error) {
99119
if err := abigen.Wait(); err != nil {
100120
return fmt.Errorf("`abigen` returned: %v", err)
101121
}
102-
return r.Close()
122+
if err := r.Close(); err != nil {
123+
return fmt.Errorf("close read-half of pipe from solc to abigen: %v", err)
124+
}
125+
126+
extend, err := cmd.Flags().GetBool(srcMapFlag)
127+
if err != nil {
128+
return fmt.Errorf("%T.Flags().GetBool(%q): %v", cmd, srcMapFlag, err)
129+
}
130+
if !extend {
131+
return os.WriteFile("generated.go", generated.Bytes(), 0644)
132+
}
133+
134+
out, err := extendGeneratedCode(generated, combinedJSON)
135+
if err != nil {
136+
return err
137+
}
138+
return os.WriteFile("generated.go", out, 0644)
139+
}
140+
141+
var (
142+
//go:embed gen_extra.go.tmpl
143+
extraCode string
144+
145+
// extraTemplate is the template for use by extendGeneratedCode().
146+
extraTemplate = template.Must(
147+
template.New("extra").
148+
Funcs(template.FuncMap{
149+
"quote": func(s interface{}) string {
150+
return fmt.Sprintf("%q", s)
151+
},
152+
"stringSlice": func(strs []string) string {
153+
q := make([]string, len(strs))
154+
for i, s := range strs {
155+
q[i] = fmt.Sprintf("%q", s)
156+
}
157+
return fmt.Sprintf("[]string{%s}", strings.Join(q, ", "))
158+
},
159+
"contract": func(s string) (string, error) {
160+
parts := strings.Split(s, ".sol:")
161+
if len(parts) != 2 {
162+
return "", fmt.Errorf("invalid contract name %q must have format path/to/file.sol:ContractName", s)
163+
}
164+
return parts[1], nil
165+
},
166+
}).
167+
Parse(extraCode),
168+
)
169+
170+
// Regular expressions for modifying abigen-generated code to work with the
171+
// extraTemplate code above.
172+
deployedRegexp = regexp.MustCompile(`^\s*return address, tx, &(.+?)\{.*Transactor.*\}, nil\s*$`)
173+
// Note the option for matching strings.Replace or strings.ReplaceAll due to
174+
// a recent change in abigen.
175+
libReplacementRegexp = regexp.MustCompile(`^\s*(.+?)Bin = strings.Replace(?:All)?\(.+?, "__\$([0-9a-f]{34})\$__", (.+?)(?:, -1)?\)\s*$`)
176+
// TODO(aschlosberg) replace regular expressions with a more explicit
177+
// approach for modifying the output code. This likely requires a PR to the
178+
// go-ethereum repo to allow bind.Bind (+/- abigen) to accept an alternate
179+
// template.
180+
)
181+
182+
// extendGeneratedCode adds ethier-specific functionality to code generated by
183+
// abigen, allowing for interoperability with the ethier/solidity package for
184+
// source-map interpretation at runtime.
185+
func extendGeneratedCode(generated, combinedJSON *bytes.Buffer) ([]byte, error) {
186+
meta := struct {
187+
SourceList []string `json:"sourceList"`
188+
Version string `json:"version"`
189+
190+
Contracts map[string]*compiler.Contract
191+
CombinedJSON string
192+
}{CombinedJSON: combinedJSON.String()}
193+
194+
if err := json.Unmarshal(combinedJSON.Bytes(), &meta); err != nil {
195+
return nil, fmt.Errorf("json.Unmarshal([solc output], %T): %v", &meta, err)
196+
}
197+
198+
cs, err := compiler.ParseCombinedJSON(combinedJSON.Bytes(), "", "", meta.Version, "")
199+
if err != nil {
200+
return nil, fmt.Errorf("compiler.ParseCombinedJSON(): %v", err)
201+
}
202+
meta.Contracts = cs
203+
for k, c := range meta.Contracts {
204+
if c.RuntimeCode == "0x" {
205+
delete(meta.Contracts, k)
206+
}
207+
}
208+
209+
if err := extraTemplate.Execute(generated, meta); err != nil {
210+
return nil, fmt.Errorf("%T.Execute(): %v", extraTemplate, err)
211+
}
212+
213+
// When using vm.Config.Trace, the only contract-identifying information is
214+
// the address to which the transaction was sent. We must therefore modify
215+
// every DeployFoo() function to save the address(es) at which the contract
216+
// is deployed.
217+
lines := strings.Split(generated.String(), "\n")
218+
for i, l := range lines {
219+
matches := deployedRegexp.FindStringSubmatch(l)
220+
if len(matches) == 0 {
221+
continue
222+
}
223+
lines[i] = fmt.Sprintf(
224+
`deployedContracts[address] = %q // Added by ethier gen
225+
%s`,
226+
matches[1], l,
227+
)
228+
}
229+
230+
// Libraries have their addresses string-replaced directly into contract
231+
// code, which we need to mirror for the runtime code too.
232+
for i, l := range lines {
233+
matches := libReplacementRegexp.FindStringSubmatch(l)
234+
if len(matches) == 0 {
235+
continue
236+
}
237+
lines[i] = fmt.Sprintf(
238+
`%s
239+
RuntimeSourceMaps[%q].RuntimeCode = strings.Replace(RuntimeSourceMaps[%[2]q].RuntimeCode, "__$%s$__", %s, -1)`,
240+
l, matches[1], matches[2], matches[3],
241+
)
242+
}
243+
244+
// Effectively the same as running goimports on the (ugly) generated code.
245+
fset := token.NewFileSet()
246+
f, err := parser.ParseFile(fset, "generated.go", strings.Join(lines, "\n"), parser.ParseComments|parser.AllErrors)
247+
if err != nil {
248+
return nil, fmt.Errorf("parser.ParseFile(%T, …): %v", fset, err)
249+
}
250+
for _, pkg := range []string{
251+
"github.com/ethereum/go-ethereum/common/compiler",
252+
"github.com/divergencetech/ethier/solidity",
253+
} {
254+
if !astutil.AddImport(fset, f, pkg) {
255+
return nil, fmt.Errorf("add import %q to generated Go: %v", pkg, err)
256+
}
257+
}
258+
259+
buf := bytes.NewBuffer(nil)
260+
if err := format.Node(buf, fset, f); err != nil {
261+
return nil, fmt.Errorf("format.Node(%T, %T, %T): %v", buf, fset, f, err)
262+
}
263+
return buf.Bytes(), nil
103264
}

ethier/gen_extra.go.tmpl

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
*
3+
* Additional code added by ethier, beyond standard abigen output.
4+
*
5+
*/
6+
7+
const (
8+
// SolCVersion is the version of the Solidity compiler used to create this
9+
// file.
10+
SolCVersion = {{quote .Version}}
11+
12+
// CombinedJSON is the raw combined-JSON output of solc, passed to abigen to
13+
// create this file.
14+
CombinedJSON = {{quote .CombinedJSON}}
15+
)
16+
17+
var (
18+
// SourceList is the list of source files used by solc when compiling these
19+
// contracts. Their indices correspond to the file numbers in the source
20+
// maps.
21+
SourceList = {{stringSlice .SourceList}}
22+
23+
// RuntimeSourceMaps contains, for each compiled contract, the runtime
24+
// binary and its associated source map. With a program counter pointing to
25+
// an instruction in the runtime binary, this is sufficient to determine the
26+
// respective location in the Solidity code from which the binary was
27+
// compiled.
28+
RuntimeSourceMaps = map[string]*compiler.Contract{
29+
{{- range $src, $c := .Contracts }}
30+
{{quote (contract $src)}}: {
31+
RuntimeCode: {{quote $c.RuntimeCode}},
32+
Info: compiler.ContractInfo{
33+
SrcMapRuntime: {{quote $c.Info.SrcMapRuntime}},
34+
},
35+
},
36+
{{- end }}
37+
}
38+
)
39+
40+
// deployedContracts tracks which contract is deployed at each address. The
41+
// standard abigen Deploy<ContractName>() functions have been modified to set
42+
// the value of this map to <ContractName> before returning the deployment
43+
// address. This allows SourceMap() to function correctly.
44+
var deployedContracts = make(map[common.Address]string)
45+
46+
// SourceMap returns a new SourceMap, able to convert program counters to
47+
// Solidity source offsets. SourceMap() must be called after contracts are
48+
// deployed otherwise they won't be registered by contract address (only by
49+
// contract name).
50+
func SourceMap() (*solidity.SourceMap, error) {
51+
return solidity.NewSourceMap(SourceList, RuntimeSourceMaps, deployedContracts)
52+
}

0 commit comments

Comments
 (0)