-
Notifications
You must be signed in to change notification settings - Fork 2
/
solcover.go
423 lines (377 loc) · 15.1 KB
/
solcover.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
// Package solcover provides trace-based Solidity coverage analysis by mapping
// from EVM-trace program counters to original Solidity source code.
//
// This package doesn't typically need to be used directly, and is automatically
// supported by adding the source-map flag to `ethier gen` of the
// github.com/divergencetech/ethier/ethier binary for generating Go bindings.
//
// See https://docs.soliditylang.org/en/v0.8.14/internals/source_mappings.html
// for more information.
package solcover
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"math"
"regexp"
"strconv"
"strings"
"github.com/bazelbuild/tools_jvm_autodeps/thirdparty/golang/parsers/util/offset"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/compiler"
"github.com/ethereum/go-ethereum/core/vm"
)
// A Location is an offset-based location in a Solidity file. Using notation
// described in https://docs.soliditylang.org/en/v0.8.14/internals/source_mappings.html,
// s = Start, l = Length, f = FileIdx, j = Jump, and m = ModifierDepth.
//
// Note that two Locations may have the same offset within the same file but
// their OpCode and instruction number will differ.
type Location struct {
// InstructionNumber is the index of the instruction, within the runtime
// byte code, that was compiled from this Solidity Location. Note that this
// is different to the regular byte-code index (i.e. the program counter) as
// PUSH<N> instructions use 1+N bytes. InstructionNumber is therefore
// equivalent to a regular slice index after stripping the N pushed bytes
// for each PUSH<N>.
InstructionNumber int
// OpCode is the instruction found at InstructionNumber.
OpCode vm.OpCode
// FileIdx refers to the index of the source file in the inputs to solc, as
// returned in solc output, but can generally be ignored in favour of the
// Source, which is determined from the SourceList parameter passed to
// RegisterContract.
FileIdx int
// Source is the relative file path to the source that solc used to compile
// this particular instruction from the location.
Source string
// Start and Length are byte offsets into Source, describing the specific
// code from which this (and other) InstructionNumber locations were
// compiled.
Start, Length int
// Line and Col are both 1-indexed as this is typical behaviour of IDEs and
// coverage reports.
Line, Col int
// EndLine and EndCol are Length bytes after Line and Col, also 1-indexed.
EndLine, EndCol int
// Jump and ModifierDepth are the j and m elements, respectively, as
// described in the Solidity source_mappings documentation above.
Jump JumpType
ModifierDepth int
}
// A JumpType describes the action of a JUMP instruction.
type JumpType string
// Possible JumpType values.
const (
FunctionIn JumpType = `i`
FunctionOut JumpType = `o`
RegularJump JumpType = `-`
)
// A compiledContract couples a *compiler.Contract with the fully qualified name
// of the Solidity contract it represents, and the solc source-list of files
// that were used to compile it. A fully qualified name is the concatenation of
// the source file name, a colon :, and the name of the contract.
type compiledContract struct {
*compiler.Contract
name string
sourceList []string
// locations[instructions[pc]] provides an indirect index from pc to loc.
locations []*Location
instructions pcToInstruction
}
// location converts the program counter into an instruction number, and returns
// the corresponding Location and true. If the pc was not in the runtime source
// map of the contract, or if cc == nil, then location returns (nil, false).
func (cc *compiledContract) location(pc uint64) (*Location, bool) {
if cc == nil {
// This allows direct calls on values returned from maps when the key
// didn't exist, avoiding an extra check of the ok.
return nil, false
}
i, ok := cc.instructions[pc]
if !ok {
return nil, false
}
return cc.locations[i], true
}
// A contractMatcher allows matching of deployed contracts against solc output
// that includes libraries, allowing the library address to change.
type contractMatcher struct {
re *regexp.Regexp
*compiledContract
}
// A sourceFile holds the input to solc for a particular file in a compilation's
// source list.
type sourceFile struct {
contents string
mapper *offset.Mapper
// isExternal flags that the code comes from an external source such as a
// Node module. This is merely for accounting as we don't need coverage
// reports for these files, and is propagated from RegisterSourceCode()
isExternal bool
}
var (
sourceCode = make(map[string]*sourceFile)
// contractsByName is keyed by the fully qualified name of the contract, its
// source file and Solditiy name; e.g. path/to/file.sol:MyContract.
contractsByName = make(map[string]*compiledContract)
// contractsByHash identifies contracts by the SHA256 hash of their byte
// code (creation, not runtime). The sha256 package is convenient because it
// returns a fixed-size array instead of a slice (like crypto.Keccak256),
// which can be used as a map key.
contractsByHash = make(map[[sha256.Size]byte]*compiledContract)
// contractMatchers are stored by the hash of their regex pattern to avoid
// duplication. The pattern is effectively the entire runtime code with
// modifications for library addresses, so the hash saves space.
contractMatchers = make(map[[sha256.Size]byte]contractMatcher)
// deployedContracts maps contracts by their deployed addresses iff the
// contract has already been registered.
deployedContracts = make(map[common.Address]*compiledContract)
)
// RegisterSourceCode registers the contents of source files passed in
// sourceList arguments to RegisterContract(). This allows op codes in
// contracts, deployed or otherwise, to be mapped back to the specific Solidity
// code that resulted in their compilation.
//
// RegisterSourceCode SHOULD be called before all calls to RegisterContract that
// include fileName in the sourceList otherwise Location values will not contain
// line and column numbers. External source code, e.g. OpenZeppelin contracts,
// won't be monitored in coverage collection, but will be available for
// Etherscan verification.
func RegisterSourceCode(fileName, contents string, isExternal bool) {
sourceCode[fileName] = &sourceFile{
contents: contents,
mapper: offset.NewMapper(contents),
isExternal: isExternal,
}
}
// RegisterContract registers a compiled contract by its fully qualified name.
// This allows inspection of EVM traces to watch for contract deployments that
// are matched against already-registered contracts, and then for mapping each
// step in the trace back to original source code via the program counter. See
// SourceByName() documentation re fully qualified names.
//
// The order of the sourceList MUST match the solc output from which the
// *Contract was parsed. The Contract's Info.SrcMapRuntime represents files as
// indices into this slice so a change in order will result in invalid results.
// All files included in sourceList SHOULD be registered via RegisterSourceCode
// before calling RegisterContract.
//
// RegisterContract MUST be called before deployment otherwise
// RegisterDeployedContract will fail to match the byte code. This is typically
// done as part on an init() function, and `ethier gen` generated code performs
// this step automatically.
func RegisterContract(name string, c *compiler.Contract, sourceList []string) {
instructions, opCodes, err := parseCode(c)
if err != nil {
panic(fmt.Sprintf("Parsing RuntimeCode of %q: %v", name, err))
}
locations, err := parseSrcMap(c, sourceList)
if err != nil {
panic(fmt.Sprintf("Parsing SrcMap of %q: %v", name, err))
}
for i, l := range locations {
l.OpCode = opCodes[i]
}
cc := &compiledContract{
Contract: c,
name: name,
sourceList: sourceList,
instructions: instructions,
locations: locations,
}
contractsByName[name] = cc
if libraryPlaceholder.MatchString(c.Code) {
registerContractByRegexp(cc)
} else {
registerContractByHash(cc)
}
}
func registerContractByRegexp(cc *compiledContract) {
pattern := libraryPlaceholder.ReplaceAllString(
strings.TrimPrefix(cc.Code, "0x"),
"73[[:xdigit:]]{40}",
)
contractMatchers[sha256.Sum256([]byte(pattern))] = contractMatcher{
re: regexp.MustCompile(pattern),
compiledContract: cc,
}
}
func registerContractByHash(cc *compiledContract) {
code := strings.TrimPrefix(cc.Code, "0x")
bin, err := hex.DecodeString(code)
if err != nil {
// panic is used instead of returning an error because the expected
// usage of RegisterContract() is in init() functions of generated code,
// so it's not possible to propagate an error. log.Fatal() wouldn't
// provide enough context but panic gives the code location.
panic(fmt.Sprintf("solidity.RegisterContract(): hex.DecodeString(%q): %v", code, err))
}
contractsByHash[sha256.Sum256(bin)] = cc
}
// RegisterDeployedContract matches the code against contracts registered with
// RegisterContract, allowing future calls to Source(addr, …) to return
// data pertaining to the correct contract.
//
// RegisterDeployedContract should be called by EVMLogger.CaptureStart when the
// create flag is true, passing the deployment address and the input code bytes.
func RegisterDeployedContract(addr common.Address, code []byte) {
c, ok := contractsByHash[sha256.Sum256(code)]
if ok {
deployedContracts[addr] = c
return
}
for _, m := range contractMatchers {
if m.re.MatchString(hex.EncodeToString(code)) {
deployedContracts[addr] = m.compiledContract
return
}
}
}
// Source returns the code location that was compiled into the instruction at
// the specific program counter in the deployed contract. The contract's address
// MUST have been registered with RegisterDeployedContract().
func Source(contract common.Address, pc uint64) (*Location, bool) {
return deployedContracts[contract].location(pc)
}
// SourceByName functions identically to Source but doesn't require that the
// contract has been deployed. The contract MUST have been registered with
// RegisterContract(). The contractName is fully qualified, including both the
// source file and the name, e.g. path/to/file.sol:ContractName.
//
// NOTE that there isn't a one-to-one mapping between runtime byte code (i.e.
// program counter) and instruction number because the PUSH* instructions
// require additional bytes as documented in:
// https://docs.soliditylang.org/en/v0.8.14/internals/source_mappings.html.
func SourceByName(contractName string, pc uint64) (*Location, bool) {
return contractsByName[contractName].location(pc)
}
var (
// libraryPlaceHolder finds all places in which bind.Bind has inserted a
// string identifying a library address to be pushed (PUSH20 == 0x73). In
// actual deployment this value is replaced, by the generated code, with the
// deployed library's address, but for source mapping it can be ignored
// because the PUSH20 means that parseCode() will skip the 20 bytes. We can
// therefore replace it with a push of anything, so use the zero address for
// simplicity.
libraryPlaceholder = regexp.MustCompile(`73__\$[[:xdigit:]]{34}\$__`)
pushZeroAddress = fmt.Sprintf("73%x", common.Address{})
)
// pcToInstruction maps a program counter to an instruction number. As PUSH<N>
// op codes use N+1 bytes, the program counter within a specific contract's
// runtime code does not match to the instruction number. A pcToInstruction map
// is therefore contract-specific.
type pcToInstruction map[uint64]int
// parseCode converts a Contract's runtime byte code into a mapping from
// program counter (position in byte code) to instruction number because the
// PUSH* OpCodes require additional byte code but the source-map is based on
// instruction number. It also returns byte code as a slice of OpCodes,
// effectively stripping the additional bytes included by PUSH* ops.
func parseCode(c *compiler.Contract) (pcToInstruction, []vm.OpCode, error) {
rawCode := strings.TrimPrefix(c.RuntimeCode, "0x")
rawCode = libraryPlaceholder.ReplaceAllString(rawCode, pushZeroAddress)
code, err := hex.DecodeString(rawCode)
if err != nil {
return nil, nil, fmt.Errorf("hex.DecodeString(%T.RuntimeCode): %v", c, err)
}
var (
instruction int
opCodes []vm.OpCode
)
instructions := make(pcToInstruction)
for pc, n := uint64(0), uint64(len(code)); pc < n; pc++ {
instructions[pc] = instruction
instruction++
c := vm.OpCode(code[int(pc)])
opCodes = append(opCodes, c)
if c.IsPush() {
pc += uint64(c - vm.PUSH0)
}
}
return instructions, opCodes, nil
}
// nMappingFields is the number of fields in the solc source mapping: s,l,f,j,m.
const nMappingFields = 5
// srcMapNode captures the s:l:f:j:m pattern of solc source mapping. If
// compressed, empty values mean that the previous value from the last node must
// be copied across.
type srcMapNode [nMappingFields]string
// parseSrcMap decompresses the Contract's runtime source map, returning a slice
// of Locations, indexed by instruction number; i.e. the indices in the slice
// correspond to the values a pcToInstruction map.
func parseSrcMap(c *compiler.Contract, sourceList []string) ([]*Location, error) {
instructions := strings.Split(c.Info.SrcMapRuntime, ";")
locations := make([]*Location, len(instructions))
var last, curr srcMapNode
for i, instr := range instructions {
copy(curr[:], strings.Split(instr, ":"))
for j, n := 0, len(curr); j < nMappingFields; j++ {
if j < n && curr[j] != "" {
last[j] = curr[j]
} else {
curr[j] = last[j]
}
}
loc, err := locationFromNode(curr, sourceList)
if err != nil {
return nil, err
}
loc.InstructionNumber = i
locations[i] = loc
}
return locations, nil
}
// locationFromNode parses an s:l:f:j:m node from a solc source map, returning
// the corresponding Location.
func locationFromNode(node srcMapNode, sourceList []string) (*Location, error) {
start, err := strconv.Atoi(node[0])
if err != nil {
return nil, fmt.Errorf("parse `s`: %v", err)
}
length, err := strconv.Atoi(node[1])
if err != nil {
return nil, fmt.Errorf("parse `l`: %v", err)
}
fileIdx, err := strconv.Atoi(node[2])
if err != nil {
return nil, fmt.Errorf("parse `f`: %v", err)
}
modDepth, err := strconv.Atoi(node[4])
if err != nil {
return nil, fmt.Errorf("parse `m`: %v", err)
}
loc := &Location{
Start: start,
Length: length,
FileIdx: fileIdx,
Jump: JumpType(node[3]),
ModifierDepth: modDepth,
}
if start < 0 || length < 0 || fileIdx < 0 {
// TODO(aschlosberg) investigate the meaning of -1 values in the
// compressed output. For now, just make it an impossible file index.
fileIdx = math.MaxInt64
loc.FileIdx = fileIdx
}
if fileIdx >= len(sourceList) {
return loc, nil
}
loc.Source = sourceList[fileIdx]
c := sourceCode[loc.Source]
m := c.mapper
if m.Len() == 0 {
return loc, nil
}
for offset, set := range map[int][2]*int{
start: {&loc.Line, &loc.Col},
start + length: {&loc.EndLine, &loc.EndCol},
} {
l, c, err := m.LineAndColumn(offset)
if err != nil {
return nil, fmt.Errorf("%T.LineAndColumn(%d) of %q: %v", m, offset, sourceList[fileIdx], err)
}
*set[0] = l + 1
*set[1] = c + 1
}
return loc, nil
}