Skip to content
This repository has been archived by the owner on Jul 9, 2021. It is now read-only.

Introduce subprovider for printing revert stack traces #705

Merged
merged 9 commits into from
Jun 15, 2018
1 change: 1 addition & 0 deletions packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"rebuild_and_test": "run-s build test",
"test:coverage": "SOLIDITY_COVERAGE=true run-s build run_mocha coverage:report:text coverage:report:lcov",
"test:profiler": "SOLIDITY_PROFILER=true run-s build run_mocha profiler:report:html",
"test:trace": "SOLIDITY_REVERT_TRACE=true run-s build run_mocha",
"run_mocha": "mocha --require source-map-support/register 'lib/test/**/*.js' --timeout 100000 --bail --exit",
"compile": "sol-compiler",
"clean": "shx rm -rf lib src/generated_contract_wrappers",
Expand Down
21 changes: 21 additions & 0 deletions packages/contracts/src/utils/revert_trace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { devConstants } from '@0xproject/dev-utils';
import { RevertTraceSubprovider, SolCompilerArtifactAdapter } from '@0xproject/sol-cov';
import * as _ from 'lodash';

let revertTraceSubprovider: RevertTraceSubprovider;

export const revertTrace = {
getRevertTraceSubproviderSingleton(): RevertTraceSubprovider {
if (_.isUndefined(revertTraceSubprovider)) {
revertTraceSubprovider = revertTrace._getRevertTraceSubprovider();
}
return revertTraceSubprovider;
},
_getRevertTraceSubprovider(): RevertTraceSubprovider {
const defaultFromAddress = devConstants.TESTRPC_FIRST_ADDRESS;
const solCompilerArtifactAdapter = new SolCompilerArtifactAdapter();
const isVerbose = true;
const subprovider = new RevertTraceSubprovider(solCompilerArtifactAdapter, defaultFromAddress, isVerbose);
return subprovider;
},
};
15 changes: 11 additions & 4 deletions packages/contracts/src/utils/web3_wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { devConstants, env, EnvVars, web3Factory } from '@0xproject/dev-utils';
import { prependSubprovider } from '@0xproject/subproviders';
import { logUtils } from '@0xproject/utils';
import { Web3Wrapper } from '@0xproject/web3-wrapper';
import * as _ from 'lodash';

import { coverage } from './coverage';
import { profiler } from './profiler';
import { revertTrace } from './revert_trace';

enum ProviderType {
Ganache = 'ganache',
Expand Down Expand Up @@ -48,10 +50,11 @@ const providerConfigs = testProvider === ProviderType.Ganache ? ganacheConfigs :
export const provider = web3Factory.getRpcProvider(providerConfigs);
const isCoverageEnabled = env.parseBoolean(EnvVars.SolidityCoverage);
const isProfilerEnabled = env.parseBoolean(EnvVars.SolidityProfiler);
if (isCoverageEnabled && isProfilerEnabled) {
throw new Error(
`Unfortunately for now you can't enable both coverage and profiler at the same time. They both use coverage.json file and there is no way to configure that.`,
);
const isRevertTraceEnabled = env.parseBoolean(EnvVars.SolidityRevertTrace);
const enabledSubproviderCount = _.filter([isCoverageEnabled, isProfilerEnabled, isRevertTraceEnabled], _.identity)
.length;
if (enabledSubproviderCount > 1) {
throw new Error(`Only one of coverage, profiler, or revert trace subproviders can be enabled at a time`);
}
if (isCoverageEnabled) {
const coverageSubprovider = coverage.getCoverageSubproviderSingleton();
Expand All @@ -71,5 +74,9 @@ if (isProfilerEnabled) {
profilerSubprovider.stop();
prependSubprovider(provider, profilerSubprovider);
}
if (isRevertTraceEnabled) {
const revertTraceSubprovider = revertTrace.getRevertTraceSubproviderSingleton();
prependSubprovider(provider, revertTraceSubprovider);
}

export const web3Wrapper = new Web3Wrapper(provider);
1 change: 1 addition & 0 deletions packages/dev-utils/src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as process from 'process';
export enum EnvVars {
SolidityCoverage = 'SOLIDITY_COVERAGE',
SolidityProfiler = 'SOLIDITY_PROFILER',
SolidityRevertTrace = 'SOLIDITY_REVERT_TRACE',
VerboseGanache = 'VERBOSE_GANACHE',
}

Expand Down
6 changes: 3 additions & 3 deletions packages/sol-cov/src/coverage_subprovider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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);
}
/**
Expand Down
1 change: 1 addition & 0 deletions packages/sol-cov/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
6 changes: 3 additions & 3 deletions packages/sol-cov/src/profiler_subprovider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ 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';

/**
* 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
Expand All @@ -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);
}
/**
Expand Down
90 changes: 90 additions & 0 deletions packages/sol-cov/src/revert_trace.ts
Original file line number Diff line number Diff line change
@@ -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 [];
}
114 changes: 114 additions & 0 deletions packages/sol-cov/src/revert_trace_subprovider.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
}
Loading