diff options
author | fragosti <francesco.agosti93@gmail.com> | 2018-06-20 01:19:38 +0800 |
---|---|---|
committer | fragosti <francesco.agosti93@gmail.com> | 2018-06-20 01:19:38 +0800 |
commit | 7454e16aae476b3908123dadab0cf9fdf6ab5990 (patch) | |
tree | 3477ace66bc71c2aa4e09b85dd1923087effac1b /packages/sol-cov | |
parent | 2cc7289b7b3c3177230957ff2384c58bed4266f3 (diff) | |
parent | e4acad60e91943c2dcbdd55b7766b543e78df6f6 (diff) | |
download | dexon-sol-tools-7454e16aae476b3908123dadab0cf9fdf6ab5990.tar dexon-sol-tools-7454e16aae476b3908123dadab0cf9fdf6ab5990.tar.gz dexon-sol-tools-7454e16aae476b3908123dadab0cf9fdf6ab5990.tar.bz2 dexon-sol-tools-7454e16aae476b3908123dadab0cf9fdf6ab5990.tar.lz dexon-sol-tools-7454e16aae476b3908123dadab0cf9fdf6ab5990.tar.xz dexon-sol-tools-7454e16aae476b3908123dadab0cf9fdf6ab5990.tar.zst dexon-sol-tools-7454e16aae476b3908123dadab0cf9fdf6ab5990.zip |
Merge branch 'v2-prototype' of https://github.com/0xProject/0x-monorepo into feature/website/portal-v2-analytics
Diffstat (limited to 'packages/sol-cov')
-rw-r--r-- | packages/sol-cov/CHANGELOG.json | 71 | ||||
-rw-r--r-- | packages/sol-cov/CHANGELOG.md | 10 | ||||
-rw-r--r-- | packages/sol-cov/package.json | 18 | ||||
-rw-r--r-- | packages/sol-cov/src/coverage_subprovider.ts | 6 | ||||
-rw-r--r-- | packages/sol-cov/src/index.ts | 1 | ||||
-rw-r--r-- | packages/sol-cov/src/profiler_subprovider.ts | 6 | ||||
-rw-r--r-- | packages/sol-cov/src/revert_trace.ts | 90 | ||||
-rw-r--r-- | packages/sol-cov/src/revert_trace_subprovider.ts | 114 | ||||
-rw-r--r-- | packages/sol-cov/src/trace.ts | 59 | ||||
-rw-r--r-- | packages/sol-cov/src/trace_collection_subprovider.ts | 64 | ||||
-rw-r--r-- | packages/sol-cov/src/trace_info_subprovider.ts | 59 | ||||
-rw-r--r-- | packages/sol-cov/src/types.ts | 7 | ||||
-rw-r--r-- | packages/sol-cov/src/utils.ts | 24 |
13 files changed, 356 insertions, 173 deletions
diff --git a/packages/sol-cov/CHANGELOG.json b/packages/sol-cov/CHANGELOG.json index 7e934ad6e..464a5a13c 100644 --- a/packages/sol-cov/CHANGELOG.json +++ b/packages/sol-cov/CHANGELOG.json @@ -1,81 +1,24 @@ [ { - "version": "0.2.0", + "timestamp": 1529397769, + "version": "0.1.1", "changes": [ { - "note": "Fixed a bug causing RegExp to crash if contract code is longer that 32767 characters", - "pr": 675 - }, - { - "note": "Fixed a bug caused by Geth debug trace depth being 1-indexed", - "pr": 675 - }, - { - "note": "Fixed a bug when the tool crashed on empty traces", - "pr": 675 - }, - { - "note": "Use `BlockchainLifecycle` to support reverts on Geth", - "pr": 675 - }, - { - "note": "Add `ProfilerSubprovider` as a hacky way to profile code using coverage tools", - "pr": 675 - }, - { - "note": "Collect traces from `estimate_gas` calls", - "pr": 675 - }, - { - "note": "Fix a race condition caused by not awaiting the transaction before getting a trace", - "pr": 675 - }, - { - "note": "Add `start`/`stop` functionality to `CoverageSubprovider` and `ProfilerSubprovider`", - "pr": 675 - }, - { - "note": "Skip interface artifacts with a warning instead of failing", - "pr": 675 - }, - { - "note": "Fix solcVersion regex in parameter validation", - "pr": 690 - }, - { - "note": "Fix a bug when in TruffleArtifactsAdapter causing it to throw if compiler.json is not there", - "pr": 690 - }, - { - "note": "HUGE perf improvements", - "pr": 690 + "note": "Dependencies updated" } ] }, { - "timestamp": 1527009134, "version": "0.1.0", "changes": [ { - "note": "Add artifact adapter as a parameter for CoverageSubprovider. Export AbstractArtifactAdapter", - "pr": 589 - }, - { - "note": "Implement SolCompilerArtifactAdapter and TruffleArtifactAdapter", - "pr": 589 - }, - { - "note": "Properly parse multi-level traces", - "pr": 589 - }, - { - "note": "Add support for solidity libraries", - "pr": 589 + "note": "Incorrect publish that was unpublished" } - ] + ], + "timestamp": 1527810075 }, { - "timestamp": 1527009133, + "timestamp": 1527009134, "version": "0.0.11", "changes": [ { diff --git a/packages/sol-cov/CHANGELOG.md b/packages/sol-cov/CHANGELOG.md index f0312d297..d8f7c5310 100644 --- a/packages/sol-cov/CHANGELOG.md +++ b/packages/sol-cov/CHANGELOG.md @@ -5,11 +5,19 @@ Edit the package's CHANGELOG.json file only. CHANGELOG +## v0.1.1 - _June 19, 2018_ + + * Dependencies updated + +## v0.1.0 - _June 1, 2018_ + + * Incorrect publish that was unpublished + ## v0.0.11 - _May 22, 2018_ * Dependencies updated -## v0.0.10 - _May 4, 2018_ +## v0.0.10 - _May 5, 2018_ * Dependencies updated diff --git a/packages/sol-cov/package.json b/packages/sol-cov/package.json index 1d9d7f94d..39618eacf 100644 --- a/packages/sol-cov/package.json +++ b/packages/sol-cov/package.json @@ -1,6 +1,6 @@ { "name": "@0xproject/sol-cov", - "version": "0.0.11", + "version": "0.1.1", "engines": { "node": ">=6.12" }, @@ -50,12 +50,12 @@ }, "homepage": "https://github.com/0xProject/0x.js/packages/sol-cov/README.md", "dependencies": { - "@0xproject/sol-compiler": "^0.5.0", - "@0xproject/subproviders": "^0.10.2", - "@0xproject/typescript-typings": "^0.3.2", - "@0xproject/utils": "^0.6.2", - "@0xproject/web3-wrapper": "^0.6.4", - "@0xproject/dev-utils": "^0.4.2", + "@0xproject/sol-compiler": "^0.5.2", + "@0xproject/subproviders": "^0.10.4", + "@0xproject/typescript-typings": "^0.4.1", + "@0xproject/utils": "^0.7.1", + "@0xproject/web3-wrapper": "^0.7.1", + "@0xproject/dev-utils": "^0.4.4", "ethereum-types": "^0.0.1", "ethereumjs-util": "^5.1.1", "glob": "^7.1.2", @@ -68,8 +68,8 @@ "solidity-parser-antlr": "^0.2.12" }, "devDependencies": { - "@0xproject/monorepo-scripts": "^0.1.20", - "@0xproject/tslint-config": "^0.4.18", + "@0xproject/monorepo-scripts": "^0.2.1", + "@0xproject/tslint-config": "^0.4.20", "@types/istanbul": "^0.4.30", "@types/loglevel": "^1.5.3", "@types/mkdirp": "^0.5.1", diff --git a/packages/sol-cov/src/coverage_subprovider.ts b/packages/sol-cov/src/coverage_subprovider.ts index 0fa7f873e..065a48434 100644 --- a/packages/sol-cov/src/coverage_subprovider.ts +++ b/packages/sol-cov/src/coverage_subprovider.ts @@ -2,8 +2,8 @@ import * as _ from 'lodash'; import { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter'; import { collectCoverageEntries } from './collect_coverage_entries'; -import { TraceCollectionSubprovider } from './trace_collection_subprovider'; import { SingleFileSubtraceHandler, TraceCollector } from './trace_collector'; +import { TraceInfoSubprovider } from './trace_info_subprovider'; import { BranchCoverage, ContractData, @@ -22,7 +22,7 @@ import { utils } from './utils'; * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface. * It's used to compute your code coverage while running solidity tests. */ -export class CoverageSubprovider extends TraceCollectionSubprovider { +export class CoverageSubprovider extends TraceInfoSubprovider { private _coverageCollector: TraceCollector; /** * Instantiates a CoverageSubprovider instance @@ -39,7 +39,7 @@ export class CoverageSubprovider extends TraceCollectionSubprovider { super(defaultFromAddress, traceCollectionSubproviderConfig); this._coverageCollector = new TraceCollector(artifactAdapter, isVerbose, coverageHandler); } - public async handleTraceInfoAsync(traceInfo: TraceInfo): Promise<void> { + protected async _handleTraceInfoAsync(traceInfo: TraceInfo): Promise<void> { await this._coverageCollector.computeSingleTraceCoverageAsync(traceInfo); } /** diff --git a/packages/sol-cov/src/index.ts b/packages/sol-cov/src/index.ts index 10f6d9597..003a27374 100644 --- a/packages/sol-cov/src/index.ts +++ b/packages/sol-cov/src/index.ts @@ -1,6 +1,7 @@ export { CoverageSubprovider } from './coverage_subprovider'; // HACK: ProfilerSubprovider is a hacky way to do profiling using coverage tools. Not production ready export { ProfilerSubprovider } from './profiler_subprovider'; +export { RevertTraceSubprovider } from './revert_trace_subprovider'; export { SolCompilerArtifactAdapter } from './artifact_adapters/sol_compiler_artifact_adapter'; export { TruffleArtifactAdapter } from './artifact_adapters/truffle_artifact_adapter'; export { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter'; diff --git a/packages/sol-cov/src/profiler_subprovider.ts b/packages/sol-cov/src/profiler_subprovider.ts index 62ed1b472..9f98da524 100644 --- a/packages/sol-cov/src/profiler_subprovider.ts +++ b/packages/sol-cov/src/profiler_subprovider.ts @@ -2,8 +2,8 @@ import * as _ from 'lodash'; import { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter'; import { collectCoverageEntries } from './collect_coverage_entries'; -import { TraceCollectionSubprovider } from './trace_collection_subprovider'; import { SingleFileSubtraceHandler, TraceCollector } from './trace_collector'; +import { TraceInfoSubprovider } from './trace_info_subprovider'; import { ContractData, Coverage, SourceRange, Subtrace, TraceInfo } from './types'; import { utils } from './utils'; @@ -11,7 +11,7 @@ import { utils } from './utils'; * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface. * ProfilerSubprovider is used to profile Solidity code while running tests. */ -export class ProfilerSubprovider extends TraceCollectionSubprovider { +export class ProfilerSubprovider extends TraceInfoSubprovider { private _profilerCollector: TraceCollector; /** * Instantiates a ProfilerSubprovider instance @@ -28,7 +28,7 @@ export class ProfilerSubprovider extends TraceCollectionSubprovider { super(defaultFromAddress, traceCollectionSubproviderConfig); this._profilerCollector = new TraceCollector(artifactAdapter, isVerbose, profilerHandler); } - public async handleTraceInfoAsync(traceInfo: TraceInfo): Promise<void> { + protected async _handleTraceInfoAsync(traceInfo: TraceInfo): Promise<void> { await this._profilerCollector.computeSingleTraceCoverageAsync(traceInfo); } /** diff --git a/packages/sol-cov/src/revert_trace.ts b/packages/sol-cov/src/revert_trace.ts new file mode 100644 index 000000000..a78d1afa8 --- /dev/null +++ b/packages/sol-cov/src/revert_trace.ts @@ -0,0 +1,90 @@ +import { logUtils } from '@0xproject/utils'; +import { OpCode, StructLog } from 'ethereum-types'; + +import * as _ from 'lodash'; + +import { EvmCallStack } from './types'; +import { utils } from './utils'; + +export function getRevertTrace(structLogs: StructLog[], startAddress: string): EvmCallStack { + const evmCallStack: EvmCallStack = []; + const addressStack = [startAddress]; + if (_.isEmpty(structLogs)) { + return []; + } + const normalizedStructLogs = utils.normalizeStructLogs(structLogs); + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < normalizedStructLogs.length; i++) { + const structLog = normalizedStructLogs[i]; + if (structLog.depth !== addressStack.length - 1) { + throw new Error("Malformed trace. Trace depth doesn't match call stack depth"); + } + // After that check we have a guarantee that call stack is never empty + // If it would: callStack.length - 1 === structLog.depth === -1 + // That means that we can always safely pop from it + + if (utils.isCallLike(structLog.op)) { + const currentAddress = _.last(addressStack) as string; + const jumpAddressOffset = 1; + const newAddress = utils.getAddressFromStackEntry( + structLog.stack[structLog.stack.length - jumpAddressOffset - 1], + ); + + // Sometimes calls don't change the execution context (current address). When we do a transfer to an + // externally owned account - it does the call and immediately returns because there is no fallback + // function. We manually check if the call depth had changed to handle that case. + const nextStructLog = normalizedStructLogs[i + 1]; + if (nextStructLog.depth !== structLog.depth) { + addressStack.push(newAddress); + evmCallStack.push({ + address: currentAddress, + structLog, + }); + } + } else if (utils.isEndOpcode(structLog.op) && structLog.op !== OpCode.Revert) { + // Just like with calls, sometimes returns/stops don't change the execution context (current address). + const nextStructLog = normalizedStructLogs[i + 1]; + if (_.isUndefined(nextStructLog) || nextStructLog.depth !== structLog.depth) { + evmCallStack.pop(); + addressStack.pop(); + } + if (structLog.op === OpCode.SelfDestruct) { + // After contract execution, we look at all sub-calls to external contracts, and for each one, fetch + // the bytecode and compute the coverage for the call. If the contract is destroyed with a call + // to `selfdestruct`, we are unable to fetch it's bytecode and compute coverage. + // TODO: Refactor this logic to fetch the sub-called contract bytecode before the selfdestruct is called + // in order to handle this edge-case. + logUtils.warn( + "Detected a selfdestruct. Sol-cov currently doesn't support that scenario. We'll just skip the trace part for a destructed contract", + ); + } + } else if (structLog.op === OpCode.Revert) { + evmCallStack.push({ + address: _.last(addressStack) as string, + structLog, + }); + return evmCallStack; + } else if (structLog.op === OpCode.Create) { + // TODO: Extract the new contract address from the stack and handle that scenario + logUtils.warn( + "Detected a contract created from within another contract. Sol-cov currently doesn't support that scenario. We'll just skip that trace", + ); + return []; + } else { + if (structLog !== _.last(normalizedStructLogs)) { + const nextStructLog = normalizedStructLogs[i + 1]; + if (nextStructLog.depth === structLog.depth) { + continue; + } else if (nextStructLog.depth === structLog.depth - 1) { + addressStack.pop(); + } else { + throw new Error('Malformed trace. Unexpected call depth change'); + } + } + } + } + if (evmCallStack.length !== 0) { + logUtils.warn('Malformed trace. Call stack non empty at the end. (probably out of gas)'); + } + return []; +} diff --git a/packages/sol-cov/src/revert_trace_subprovider.ts b/packages/sol-cov/src/revert_trace_subprovider.ts new file mode 100644 index 000000000..b1d4da10c --- /dev/null +++ b/packages/sol-cov/src/revert_trace_subprovider.ts @@ -0,0 +1,114 @@ +import { stripHexPrefix } from 'ethereumjs-util'; +import * as _ from 'lodash'; +import { getLogger, levels, Logger } from 'loglevel'; + +import { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter'; +import { constants } from './constants'; +import { getRevertTrace } from './revert_trace'; +import { parseSourceMap } from './source_maps'; +import { TraceCollectionSubprovider } from './trace_collection_subprovider'; +import { ContractData, EvmCallStack, SourceRange } from './types'; +import { utils } from './utils'; + +/** + * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface. + * It is used to report call stack traces whenever a revert occurs. + */ +export class RevertTraceSubprovider extends TraceCollectionSubprovider { + // Lock is used to not accept normal transactions while doing call/snapshot magic because they'll be reverted later otherwise + private _contractsData!: ContractData[]; + private _artifactAdapter: AbstractArtifactAdapter; + private _logger: Logger; + + /** + * Instantiates a RevertTraceSubprovider instance + * @param artifactAdapter Adapter for used artifacts format (0x, truffle, giveth, etc.) + * @param defaultFromAddress default from address to use when sending transactions + * @param isVerbose If true, we will log any unknown transactions. Otherwise we will ignore them + */ + constructor(artifactAdapter: AbstractArtifactAdapter, defaultFromAddress: string, isVerbose: boolean = true) { + const traceCollectionSubproviderConfig = { + shouldCollectTransactionTraces: true, + shouldCollectGasEstimateTraces: true, + shouldCollectCallTraces: true, + }; + super(defaultFromAddress, traceCollectionSubproviderConfig); + this._artifactAdapter = artifactAdapter; + this._logger = getLogger('sol-cov'); + this._logger.setLevel(isVerbose ? levels.TRACE : levels.ERROR); + } + // tslint:disable-next-line:no-unused-variable + protected async _recordTxTraceAsync(address: string, data: string | undefined, txHash: string): Promise<void> { + await this._web3Wrapper.awaitTransactionMinedAsync(txHash, 0); + const trace = await this._web3Wrapper.getTransactionTraceAsync(txHash, { + disableMemory: true, + disableStack: false, + disableStorage: true, + }); + const evmCallStack = getRevertTrace(trace.structLogs, address); + if (evmCallStack.length > 0) { + // if getRevertTrace returns a call stack it means there was a + // revert. + await this._printStackTraceAsync(evmCallStack); + } + } + private async _printStackTraceAsync(evmCallStack: EvmCallStack): Promise<void> { + const sourceRanges: SourceRange[] = []; + if (_.isUndefined(this._contractsData)) { + this._contractsData = await this._artifactAdapter.collectContractsDataAsync(); + } + for (const evmCallStackEntry of evmCallStack) { + const isContractCreation = evmCallStackEntry.address === constants.NEW_CONTRACT; + if (isContractCreation) { + this._logger.error('Contract creation not supported'); + continue; + } + const bytecode = await this._web3Wrapper.getContractCodeAsync(evmCallStackEntry.address); + const contractData = utils.getContractDataIfExists(this._contractsData, bytecode); + if (_.isUndefined(contractData)) { + const errMsg = isContractCreation + ? `Unknown contract creation transaction` + : `Transaction to an unknown address: ${evmCallStackEntry.address}`; + this._logger.warn(errMsg); + continue; + } + const bytecodeHex = stripHexPrefix(bytecode); + const sourceMap = isContractCreation ? contractData.sourceMap : contractData.sourceMapRuntime; + const pcToSourceRange = parseSourceMap( + contractData.sourceCodes, + sourceMap, + bytecodeHex, + contractData.sources, + ); + // tslint:disable-next-line:no-unnecessary-initializer + let sourceRange: SourceRange | undefined = undefined; + let pc = evmCallStackEntry.structLog.pc; + // Sometimes there is not a mapping for this pc (e.g. if the revert + // actually happens in assembly). In that case, we want to keep + // searching backwards by decrementing the pc until we find a + // mapped source range. + while (_.isUndefined(sourceRange)) { + sourceRange = pcToSourceRange[pc]; + pc -= 1; + if (pc <= 0) { + this._logger.warn( + `could not find matching sourceRange for structLog: ${evmCallStackEntry.structLog}`, + ); + continue; + } + } + sourceRanges.push(sourceRange); + } + if (sourceRanges.length > 0) { + this._logger.error('\n\nStack trace for REVERT:\n'); + _.forEach(_.reverse(sourceRanges), sourceRange => { + this._logger.error( + `${sourceRange.fileName}:${sourceRange.location.start.line}:${sourceRange.location.start.column}`, + ); + }); + this._logger.error('\n'); + } else { + this._logger.error('Could not determine stack trace'); + } + } +} diff --git a/packages/sol-cov/src/trace.ts b/packages/sol-cov/src/trace.ts index 45e45e9c5..635019fc0 100644 --- a/packages/sol-cov/src/trace.ts +++ b/packages/sol-cov/src/trace.ts @@ -1,32 +1,25 @@ -import { addressUtils, BigNumber, logUtils } from '@0xproject/utils'; +import { logUtils } from '@0xproject/utils'; import { OpCode, StructLog } from 'ethereum-types'; -import { addHexPrefix } from 'ethereumjs-util'; import * as _ from 'lodash'; +import { utils } from './utils'; + export interface TraceByContractAddress { [contractAddress: string]: StructLog[]; } -function getAddressFromStackEntry(stackEntry: string): string { - const hexBase = 16; - return addressUtils.padZeros(new BigNumber(addHexPrefix(stackEntry)).toString(hexBase)); -} - export function getTracesByContractAddress(structLogs: StructLog[], startAddress: string): TraceByContractAddress { const traceByContractAddress: TraceByContractAddress = {}; let currentTraceSegment = []; - const callStack = [startAddress]; + const addressStack = [startAddress]; if (_.isEmpty(structLogs)) { return traceByContractAddress; } - if (structLogs[0].depth === 1) { - // Geth uses 1-indexed depth counter whilst ganache starts from 0 - _.forEach(structLogs, structLog => structLog.depth--); - } + const normalizedStructLogs = utils.normalizeStructLogs(structLogs); // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < structLogs.length; i++) { - const structLog = structLogs[i]; - if (structLog.depth !== callStack.length - 1) { + for (let i = 0; i < normalizedStructLogs.length; i++) { + const structLog = normalizedStructLogs[i]; + if (structLog.depth !== addressStack.length - 1) { throw new Error("Malformed trace. Trace depth doesn't match call stack depth"); } // After that check we have a guarantee that call stack is never empty @@ -34,36 +27,26 @@ export function getTracesByContractAddress(structLogs: StructLog[], startAddress // That means that we can always safely pop from it currentTraceSegment.push(structLog); - const isCallLike = _.includes( - [OpCode.CallCode, OpCode.StaticCall, OpCode.Call, OpCode.DelegateCall], - structLog.op, - ); - const isEndOpcode = _.includes( - [OpCode.Return, OpCode.Stop, OpCode.Revert, OpCode.Invalid, OpCode.SelfDestruct], - structLog.op, - ); - if (isCallLike) { - const currentAddress = _.last(callStack) as string; + if (utils.isCallLike(structLog.op)) { + const currentAddress = _.last(addressStack) as string; const jumpAddressOffset = 1; - const newAddress = getAddressFromStackEntry( + const newAddress = utils.getAddressFromStackEntry( structLog.stack[structLog.stack.length - jumpAddressOffset - 1], ); - if (structLog === _.last(structLogs)) { - throw new Error('Malformed trace. CALL-like opcode can not be the last one'); - } + // Sometimes calls don't change the execution context (current address). When we do a transfer to an // externally owned account - it does the call and immediately returns because there is no fallback // function. We manually check if the call depth had changed to handle that case. - const nextStructLog = structLogs[i + 1]; + const nextStructLog = normalizedStructLogs[i + 1]; if (nextStructLog.depth !== structLog.depth) { - callStack.push(newAddress); + addressStack.push(newAddress); traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat( currentTraceSegment, ); currentTraceSegment = []; } - } else if (isEndOpcode) { - const currentAddress = callStack.pop() as string; + } else if (utils.isEndOpcode(structLog.op)) { + const currentAddress = addressStack.pop() as string; traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat( currentTraceSegment, ); @@ -85,12 +68,12 @@ export function getTracesByContractAddress(structLogs: StructLog[], startAddress ); return traceByContractAddress; } else { - if (structLog !== _.last(structLogs)) { - const nextStructLog = structLogs[i + 1]; + if (structLog !== _.last(normalizedStructLogs)) { + const nextStructLog = normalizedStructLogs[i + 1]; if (nextStructLog.depth === structLog.depth) { continue; } else if (nextStructLog.depth === structLog.depth - 1) { - const currentAddress = callStack.pop() as string; + const currentAddress = addressStack.pop() as string; traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat( currentTraceSegment, ); @@ -101,11 +84,11 @@ export function getTracesByContractAddress(structLogs: StructLog[], startAddress } } } - if (callStack.length !== 0) { + if (addressStack.length !== 0) { logUtils.warn('Malformed trace. Call stack non empty at the end'); } if (currentTraceSegment.length !== 0) { - const currentAddress = callStack.pop() as string; + const currentAddress = addressStack.pop() as string; traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat( currentTraceSegment, ); diff --git a/packages/sol-cov/src/trace_collection_subprovider.ts b/packages/sol-cov/src/trace_collection_subprovider.ts index 742735935..9866472b9 100644 --- a/packages/sol-cov/src/trace_collection_subprovider.ts +++ b/packages/sol-cov/src/trace_collection_subprovider.ts @@ -6,8 +6,7 @@ import * as _ from 'lodash'; import { Lock } from 'semaphore-async-await'; import { constants } from './constants'; -import { getTracesByContractAddress } from './trace'; -import { BlockParamLiteral, TraceInfo, TraceInfoExistingContract, TraceInfoNewContract } from './types'; +import { BlockParamLiteral } from './types'; interface MaybeFakeTxData extends TxData { isFakeTransaction?: boolean; @@ -27,13 +26,14 @@ export interface TraceCollectionSubproviderConfig { /** * This class implements the [web3-provider-engine](https://github.com/MetaMask/provider-engine) subprovider interface. - * It collects traces of all transactions that were sent and all calls that were executed through JSON RPC. + * It collects traces of all transactions that were sent and all calls that were executed through JSON RPC. It must + * be extended by implementing the _recordTxTraceAsync method which is called for every transaction. */ export abstract class TraceCollectionSubprovider extends Subprovider { + protected _web3Wrapper!: Web3Wrapper; // Lock is used to not accept normal transactions while doing call/snapshot magic because they'll be reverted later otherwise private _lock = new Lock(); private _defaultFromAddress: string; - private _web3Wrapper!: Web3Wrapper; private _isEnabled = true; private _config: TraceCollectionSubproviderConfig; /** @@ -58,11 +58,6 @@ export abstract class TraceCollectionSubprovider extends Subprovider { this._isEnabled = false; } /** - * Called for each subtrace. - * @param traceInfo Trace info for this subtrace. - */ - public abstract handleTraceInfoAsync(traceInfo: TraceInfo): Promise<void>; - /** * This method conforms to the web3-provider-engine interface. * It is called internally by the ProviderEngine when it is this subproviders * turn to handle a JSON RPC request. @@ -119,6 +114,11 @@ export abstract class TraceCollectionSubprovider extends Subprovider { super.setEngine(engine); this._web3Wrapper = new Web3Wrapper(engine); } + protected abstract async _recordTxTraceAsync( + address: string, + data: string | undefined, + txHash: string, + ): Promise<void>; private async _onTransactionSentAsync( txData: MaybeFakeTxData, err: Error | null, @@ -160,52 +160,6 @@ export abstract class TraceCollectionSubprovider extends Subprovider { await this._recordCallOrGasEstimateTraceAsync(callData); cb(); } - private async _recordTxTraceAsync(address: string, data: string | undefined, txHash: string): Promise<void> { - await this._web3Wrapper.awaitTransactionMinedAsync(txHash, 0); - const trace = await this._web3Wrapper.getTransactionTraceAsync(txHash, { - disableMemory: true, - disableStack: false, - disableStorage: true, - }); - const tracesByContractAddress = getTracesByContractAddress(trace.structLogs, address); - const subcallAddresses = _.keys(tracesByContractAddress); - if (address === constants.NEW_CONTRACT) { - for (const subcallAddress of subcallAddresses) { - let traceInfo: TraceInfoNewContract | TraceInfoExistingContract; - if (subcallAddress === 'NEW_CONTRACT') { - const traceForThatSubcall = tracesByContractAddress[subcallAddress]; - traceInfo = { - subtrace: traceForThatSubcall, - txHash, - address: subcallAddress, - bytecode: data as string, - }; - } else { - const runtimeBytecode = await this._web3Wrapper.getContractCodeAsync(subcallAddress); - const traceForThatSubcall = tracesByContractAddress[subcallAddress]; - traceInfo = { - subtrace: traceForThatSubcall, - txHash, - address: subcallAddress, - runtimeBytecode, - }; - } - await this.handleTraceInfoAsync(traceInfo); - } - } else { - for (const subcallAddress of subcallAddresses) { - const runtimeBytecode = await this._web3Wrapper.getContractCodeAsync(subcallAddress); - const traceForThatSubcall = tracesByContractAddress[subcallAddress]; - const traceInfo: TraceInfoExistingContract = { - subtrace: traceForThatSubcall, - txHash, - address: subcallAddress, - runtimeBytecode, - }; - await this.handleTraceInfoAsync(traceInfo); - } - } - } private async _recordCallOrGasEstimateTraceAsync(callData: Partial<CallData>): Promise<void> { // We don't want other transactions to be exeucted during snashotting period, that's why we lock the // transaction execution for all transactions except our fake ones. diff --git a/packages/sol-cov/src/trace_info_subprovider.ts b/packages/sol-cov/src/trace_info_subprovider.ts new file mode 100644 index 000000000..635a68f58 --- /dev/null +++ b/packages/sol-cov/src/trace_info_subprovider.ts @@ -0,0 +1,59 @@ +import * as _ from 'lodash'; + +import { constants } from './constants'; +import { getTracesByContractAddress } from './trace'; +import { TraceCollectionSubprovider } from './trace_collection_subprovider'; +import { TraceInfo, TraceInfoExistingContract, TraceInfoNewContract } from './types'; + +// TraceInfoSubprovider is extended by subproviders which need to work with one +// TraceInfo at a time. It has one abstract method: _handleTraceInfoAsync, which +// is called for each TraceInfo. +export abstract class TraceInfoSubprovider extends TraceCollectionSubprovider { + protected abstract _handleTraceInfoAsync(traceInfo: TraceInfo): Promise<void>; + protected async _recordTxTraceAsync(address: string, data: string | undefined, txHash: string): Promise<void> { + await this._web3Wrapper.awaitTransactionMinedAsync(txHash, 0); + const trace = await this._web3Wrapper.getTransactionTraceAsync(txHash, { + disableMemory: true, + disableStack: false, + disableStorage: true, + }); + const tracesByContractAddress = getTracesByContractAddress(trace.structLogs, address); + const subcallAddresses = _.keys(tracesByContractAddress); + if (address === constants.NEW_CONTRACT) { + for (const subcallAddress of subcallAddresses) { + let traceInfo: TraceInfoNewContract | TraceInfoExistingContract; + if (subcallAddress === 'NEW_CONTRACT') { + const traceForThatSubcall = tracesByContractAddress[subcallAddress]; + traceInfo = { + subtrace: traceForThatSubcall, + txHash, + address: subcallAddress, + bytecode: data as string, + }; + } else { + const runtimeBytecode = await this._web3Wrapper.getContractCodeAsync(subcallAddress); + const traceForThatSubcall = tracesByContractAddress[subcallAddress]; + traceInfo = { + subtrace: traceForThatSubcall, + txHash, + address: subcallAddress, + runtimeBytecode, + }; + } + await this._handleTraceInfoAsync(traceInfo); + } + } else { + for (const subcallAddress of subcallAddresses) { + const runtimeBytecode = await this._web3Wrapper.getContractCodeAsync(subcallAddress); + const traceForThatSubcall = tracesByContractAddress[subcallAddress]; + const traceInfo: TraceInfoExistingContract = { + subtrace: traceForThatSubcall, + txHash, + address: subcallAddress, + runtimeBytecode, + }; + await this._handleTraceInfoAsync(traceInfo); + } + } + } +} diff --git a/packages/sol-cov/src/types.ts b/packages/sol-cov/src/types.ts index 896d4a7b5..cef7141cb 100644 --- a/packages/sol-cov/src/types.ts +++ b/packages/sol-cov/src/types.ts @@ -107,3 +107,10 @@ export type TraceInfo = TraceInfoNewContract | TraceInfoExistingContract; export enum BlockParamLiteral { Latest = 'latest', } + +export interface EvmCallStackEntry { + structLog: StructLog; + address: string; +} + +export type EvmCallStack = EvmCallStackEntry[]; diff --git a/packages/sol-cov/src/utils.ts b/packages/sol-cov/src/utils.ts index 0b32df02e..4f16a1cda 100644 --- a/packages/sol-cov/src/utils.ts +++ b/packages/sol-cov/src/utils.ts @@ -1,3 +1,6 @@ +import { addressUtils, BigNumber } from '@0xproject/utils'; +import { OpCode, StructLog } from 'ethereum-types'; +import { addHexPrefix } from 'ethereumjs-util'; import * as _ from 'lodash'; import { ContractData, LineColumn, SingleFileSourceRange } from './types'; @@ -42,4 +45,25 @@ export const utils = { }); return contractData; }, + isCallLike(op: OpCode): boolean { + return _.includes([OpCode.CallCode, OpCode.StaticCall, OpCode.Call, OpCode.DelegateCall], op); + }, + isEndOpcode(op: OpCode): boolean { + return _.includes([OpCode.Return, OpCode.Stop, OpCode.Revert, OpCode.Invalid, OpCode.SelfDestruct], op); + }, + getAddressFromStackEntry(stackEntry: string): string { + const hexBase = 16; + return addressUtils.padZeros(new BigNumber(addHexPrefix(stackEntry)).toString(hexBase)); + }, + normalizeStructLogs(structLogs: StructLog[]): StructLog[] { + if (structLogs[0].depth === 1) { + // Geth uses 1-indexed depth counter whilst ganache starts from 0 + const newStructLogs = _.map(structLogs, structLog => ({ + ...structLog, + depth: structLog.depth - 1, + })); + return newStructLogs; + } + return structLogs; + }, }; |