Skip to content

Commit

Permalink
Merge pull request #3 from chaintope/support_create_contract
Browse files Browse the repository at this point in the history
Support create contract
  • Loading branch information
nakajo2011 authored May 21, 2019
2 parents 1d7e0db + 19c6c4a commit 77e4687
Show file tree
Hide file tree
Showing 17 changed files with 1,782 additions and 53 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,14 @@ contract("VyperStorage", (accounts) => {
})
```

## CAUTION
if request method is eth_call, then this Lib re-request eth_sendTransaction with same params, for getting traceLogs.
so that, in using this coverage tool, more gas is consumes. may `out of gas` error a lot of happen.

## Demo
![](https://user-images.githubusercontent.com/1563840/57188087-e22b1400-6f33-11e9-8892-475a0454f056.png)
![](https://user-images.githubusercontent.com/1563840/58031554-9b583380-7b5b-11e9-80ee-a87cead6d210.png)

## Not yet support list
- `eth_call` opcodes trace.
- Solidity's Contract (maybe available, but not tested)

## Licence
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "contract-coverager",
"version": "0.2.0",
"version": "0.3.0",
"main": "src/index.js",
"scripts": {
"lint": "eslint src/**/*.js test/**/*.spec.js",
Expand Down
5 changes: 5 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const NEW_CONTRACT = 'NEW_CONTRACT'

module.exports = {
NEW_CONTRACT
}
86 changes: 69 additions & 17 deletions src/coverage_subprovider.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ const inherits = require('util').inherits
const promisify = require('util').promisify
const { separateTraceLogs } = require('./tracelog_utils')
const Subprovider = require('web3-provider-engine/subproviders/subprovider')
const TraceCollector = require('./trace_collector')
const { TraceCollector, TRACE_LOG_TYPE } = require('./trace_collector')
const TruffleArtifactResolver = require('./truffle_artifacts_resolver')
const Coverager = require('./coverager')
const { NEW_CONTRACT } = require('./constants')

const NonEmitterProvider = require('./non_emitter_provider')
const Web3ProviderEngine = require('web3-provider-engine')
Expand All @@ -28,8 +29,7 @@ function CoverageSubprovider(provider, jsonGlob = null) {
provider.sendAsync = provider.send
}
this.provider = provider
this.resolver = jsonGlob ? new TruffleArtifactResolver(jsonGlob) : new TruffleArtifactResolver()
this.collector = new TraceCollector()
this.jsonGlob = jsonGlob
}

function debugTraceTransaction(txhash, cb) {
Expand Down Expand Up @@ -60,6 +60,14 @@ function getCode(address, cb) {
}, cb)
}

function getReceipt(txhash, cb) {
const self = this
self.emitPayload({
method: 'eth_getTransactionReceipt',
params: [txhash]
}, cb)
}

function isNewContract(address) {
return (address === '0x' || address === '' || address === undefined)
}
Expand All @@ -82,18 +90,27 @@ function injectInTruffle(artifacts, web3, fglob = null) {
result.setProvider(web3.currentProvider)
return result
}
artifacts.require._coverageProvider = web3.currentProvider
artifacts.require._coverageProvider = engine
return engine
}

// define class methods
CoverageSubprovider.injectInTruffle = injectInTruffle

// promisifies
CoverageSubprovider.prototype._debugTraceTransaction = promisify(debugTraceTransaction)
CoverageSubprovider.prototype._sendTransaction = promisify(sendTransaction)
CoverageSubprovider.prototype._getCode = promisify(getCode)
CoverageSubprovider.prototype._getReceipt = promisify(getReceipt)

CoverageSubprovider.prototype.handleRequest = function(payload, next, end) {
const self = this

/**
* record contract address by code that is gived from node.
* @param contractAddress
* @return {Function}
*/
function findContract(contractAddress) {
return async() => {
if (!self.resolver.exists(contractAddress)) {
Expand All @@ -103,49 +120,84 @@ CoverageSubprovider.prototype.handleRequest = function(payload, next, end) {
}
}

function getTraceAndCollect(contractAddress) {
/**
* getDebugTrace and record that.
* @param contractAddress
* @param traceType
* @return {function(*=): *}
*/
function functionCallTraceAndCollect(contractAddress, traceType) {
return async function(txHash) {
const response = await self._debugTraceTransaction(txHash)
const separated = separateTraceLogs(response.result.structLogs)
self.collector.add(contractAddress, separated[0].traceLogs)
// separated[0] is start point.
// it may be constructor case.
self.collector.add(contractAddress, separated[0].traceLogs, traceType)
await findContract(contractAddress)()
for (let i = 1; i < separated.length; i++) {
const trace = separated[i]
self.collector.recordFunctionCall({ to: trace.address, data: trace.functionId })
self.collector.add(trace.address, trace.traceLogs)
if (trace.functionId === NEW_CONTRACT) {
self.collector.recordCreation(trace.address)
self.collector.add(trace.address, trace.traceLogs, TRACE_LOG_TYPE.CREATE)
} else {
self.collector.recordFunctionCall({ to: trace.address, data: trace.functionId })
self.collector.add(trace.address, trace.traceLogs, TRACE_LOG_TYPE.FUNCTION)
}
await findContract(trace.address)()
}
return txHash
}
}

let traceFunc = async() => {}
/**
* record construcor trace infos.
* @param initBytecodes
* @return {Function}
*/
function creationTraceAndCollect(initBytecodes) {
return async function(txHash) {
const receipt = await self._getReceipt(txHash)
self.collector.recordCreation(receipt.result.contractAddress)
self.resolver.findByCodeHash(initBytecodes, receipt.result.contractAddress)
await functionCallTraceAndCollect(receipt.result.contractAddress, TRACE_LOG_TYPE.CREATE)(txHash)
}
}

let traceFunc = () => { return Promise.resolve('') }
switch (payload.method) {
case 'eth_call':
// self.collector.recordFunctionCall(payload.params[0])
// traceFunc = findContract(payload.params[0].to)
traceFunc = () => self._sendTransaction(payload.params[0])
if (payload.params[0].to) { // workaround for double count constructor when calling `new` method.
traceFunc = () => self._sendTransaction(payload.params[0])
}
break

case 'eth_sendTransaction':
const param = payload.params[0]
if (!isNewContract(param.to) && param.data.length > 4) {
self.collector.recordFunctionCall(param)
traceFunc = getTraceAndCollect(param.to)
if (param.data.length > 4) { // data empty tx is just send ETH tx.
if (isNewContract(param.to)) {
traceFunc = creationTraceAndCollect(param.data)
} else {
self.collector.recordFunctionCall(param)
traceFunc = functionCallTraceAndCollect(param.to, TRACE_LOG_TYPE.FUNCTION)
}
}
break
}

this.provider.sendAsync(payload, function(err, response) {
if (err) return end(err)
if (response.error) return end(response.error.message)
traceFunc(response.result).then(() => {
traceFunc(response.result).then(res => {
// check error just first response.
if (response.error) return end(response.error.message)
end(null, response.result)
}).catch(e => end(e))
})
}

CoverageSubprovider.prototype.start = function() {
this.resolver = this.jsonGlob ? new TruffleArtifactResolver(this.jsonGlob) : new TruffleArtifactResolver()
this.resolver.load()
this.collector = new TraceCollector()
}

CoverageSubprovider.prototype.stop = function() {
Expand Down
26 changes: 20 additions & 6 deletions src/coverager.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const Table = require('cli-table')
const colors = require('colors')
const { flatten, matchCalledFunction, matchUsedOpecodes, countUsed } = require('./match_helper')
const { TRACE_LOG_TYPE } = require('./trace_collector')

/**
*
Expand All @@ -26,12 +27,15 @@ class Coverager {
})

const coverageRawDatas = {}
// transform to ContractName Key map.
indexBaseAddressArray.forEach((addrs, index) => {
const rawData = {}
rawData.contract = self.resolver.contractsData[index]
const traces = addrs.map(addr => self.collector.traceMap[addr])
const functionTraces = addrs.map(addr => self.collector.traceMap[addr][TRACE_LOG_TYPE.FUNCTION])
const createTraces = addrs.map(addr => self.collector.traceMap[addr][TRACE_LOG_TYPE.CREATE])
const funcCalls = addrs.map(addr => self.collector.funcCallMap[addr])
rawData.traces = flatten(traces)
rawData.functionTraces = flatten(functionTraces)
rawData.createTraces = flatten(createTraces)
rawData.funcCalls = flatten(funcCalls)
coverageRawDatas[rawData.contract.contractName] = rawData
})
Expand All @@ -47,19 +51,29 @@ class Coverager {
resultStr += `(${index + 1}) ${contractName}\n`
const data = matchingDatas[contractName]
const numInstructions = data.contract.compiler.name === 'solc' ? null : data.contract.deployedBytecode.length / 2
resultStr += this.reportUsedOpecodes(data.contract.deployedBytecode, data.traces, numInstructions)
resultStr += this.reportUsedOpecodes(data.contract.bytecode, data.contract.deployedBytecode, data.createTraces, data.functionTraces, numInstructions)
resultStr += this.reportMethodCalled(data.contract.functions, data.funcCalls)
resultStr += '\n\n'
})
console.log(resultStr)
}

reportUsedOpecodes(bytecodes, structLogs, numInstructions) {
const matching = matchUsedOpecodes(bytecodes, structLogs, numInstructions)
reportUsedOpecodes(bytecodes, deployedBytecodes, createTraces, functionTraces, numInstructions) {
const matching = matchUsedOpecodes(deployedBytecodes, functionTraces, numInstructions)
const matchingCreation = matchUsedOpecodes(bytecodes, createTraces, numInstructions)
const usedTotally = countUsed(matching)
return `deployedBytecode coverage: ${Number((usedTotally * 100) / matching.length).toFixed(2)}% (${usedTotally}/${matching.length})\n`
const createTotally = countUsed(matchingCreation)
// TODO: Almost case, this code is good.
// but, should check more struct, to analyze difference of both bytecodes and deployedBytecodes.
const constructorCodes = matchingCreation.length - matching.length
let resStr = this._percentageReport('deployedBytecode coverage', usedTotally, matching.length)
resStr += this._percentageReport('constructor code coverage', createTotally, constructorCodes)
return resStr
}

_percentageReport(prefixStr, used, total) {
return `${prefixStr}: ${Number((used * 100) / total).toFixed(2)}% (${used}/${total})\n`
}
reportMethodCalled(functions, calls) {
const masLength = Math.max(...Object.keys(functions).map(sig => sig.length), 20)
const table = new Table({ head: ['method', 'call count'], colWidths: [masLength + 2, 20], style: { head: ['bold'] } })
Expand Down
43 changes: 35 additions & 8 deletions src/trace_collector.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
const { NEW_CONTRACT } = require('./constants')

const TRACE_LOG_TYPE = {
FUNCTION: 'functionCall',
CREATION: 'contractCreation'
}

class TraceCollector {
constructor() {
this.traceMap = {}
this.funcCallMap = {}
}

add(address, traceLogs) {
/**
* add called opecodes. Those are categorized by type, and then store.
* @param address - contract address
* @param traceLogs - debugTraces
* @param type - TRACE_LOG_TYPE
*/
add(address, traceLogs, type) {
if (this.traceMap[address] === undefined) {
this.traceMap[address] = []
this.traceMap[address] = {}
}
if (this.traceMap[address][type] === undefined) {
Object.assign(this.traceMap[address], { [type]: [] })
}
if (traceLogs.structLogs !== undefined) {
traceLogs = traceLogs.structLogs
}
this.traceMap[address].push(traceLogs)
this.traceMap[address][type].push(traceLogs)
}

recordFunctionCall(txRequestParams) {
Expand All @@ -25,14 +41,25 @@ class TraceCollector {
// console.warn(`Not Contract call message. ${JSON.stringify(txRequestParams)}`)
return
}
if (this.funcCallMap[address] === undefined) {
this.funcCallMap[address] = []
}
if (!bytecodes.startsWith('0x')) {
bytecodes = '0x' + bytecodes
}
this.funcCallMap[address].push(bytecodes.slice(0, 10))
this._push(address, bytecodes.slice(0, 10))
}

recordCreation(address) {
this._push(address, NEW_CONTRACT)
}

_push(address, functionId) {
if (this.funcCallMap[address] === undefined) {
this.funcCallMap[address] = []
}
this.funcCallMap[address].push(functionId)
}
}

module.exports = TraceCollector
module.exports = {
TraceCollector,
TRACE_LOG_TYPE
}
Loading

0 comments on commit 77e4687

Please sign in to comment.