diff options
author | Leonid Logvinov <logvinov.leon@gmail.com> | 2019-01-08 19:23:33 +0800 |
---|---|---|
committer | Leonid Logvinov <logvinov.leon@gmail.com> | 2019-01-08 21:48:06 +0800 |
commit | 2c974b5f3ffa0e9736000273e39cdeee4a251b94 (patch) | |
tree | a1772f93d796e3b4ba7a988194a44a3e8bcd6d31 /packages/sol-trace/src | |
parent | 0ac36cef288deecd36caa601c53d13517eef5ca8 (diff) | |
download | dexon-sol-tools-2c974b5f3ffa0e9736000273e39cdeee4a251b94.tar dexon-sol-tools-2c974b5f3ffa0e9736000273e39cdeee4a251b94.tar.gz dexon-sol-tools-2c974b5f3ffa0e9736000273e39cdeee4a251b94.tar.bz2 dexon-sol-tools-2c974b5f3ffa0e9736000273e39cdeee4a251b94.tar.lz dexon-sol-tools-2c974b5f3ffa0e9736000273e39cdeee4a251b94.tar.xz dexon-sol-tools-2c974b5f3ffa0e9736000273e39cdeee4a251b94.tar.zst dexon-sol-tools-2c974b5f3ffa0e9736000273e39cdeee4a251b94.zip |
Refactor out sol-cov, sol-profiler and sol-trace into their separate packages
Diffstat (limited to 'packages/sol-trace/src')
-rw-r--r-- | packages/sol-trace/src/globals.d.ts | 7 | ||||
-rw-r--r-- | packages/sol-trace/src/index.ts | 24 | ||||
-rw-r--r-- | packages/sol-trace/src/revert_trace_subprovider.ts | 167 |
3 files changed, 198 insertions, 0 deletions
diff --git a/packages/sol-trace/src/globals.d.ts b/packages/sol-trace/src/globals.d.ts new file mode 100644 index 000000000..e799b3529 --- /dev/null +++ b/packages/sol-trace/src/globals.d.ts @@ -0,0 +1,7 @@ +// tslint:disable:completed-docs +declare module '*.json' { + const json: any; + /* tslint:disable */ + export default json; + /* tslint:enable */ +} diff --git a/packages/sol-trace/src/index.ts b/packages/sol-trace/src/index.ts new file mode 100644 index 000000000..1f3ca3a99 --- /dev/null +++ b/packages/sol-trace/src/index.ts @@ -0,0 +1,24 @@ +export { + AbstractArtifactAdapter, + TruffleArtifactAdapter, + SolCompilerArtifactAdapter, + ContractData, +} from '@0x/sol-trace-based-tools-common'; + +export { RevertTraceSubprovider } from './revert_trace_subprovider'; + +export { + JSONRPCRequestPayload, + Provider, + JSONRPCErrorCallback, + JSONRPCResponsePayload, + JSONRPCResponseError, +} from 'ethereum-types'; + +export { + JSONRPCRequestPayloadWithMethod, + NextCallback, + ErrorCallback, + OnNextCompleted, + Callback, +} from '@0x/subproviders'; diff --git a/packages/sol-trace/src/revert_trace_subprovider.ts b/packages/sol-trace/src/revert_trace_subprovider.ts new file mode 100644 index 000000000..37e62b3f8 --- /dev/null +++ b/packages/sol-trace/src/revert_trace_subprovider.ts @@ -0,0 +1,167 @@ +import { + AbstractArtifactAdapter, + constants, + ContractData, + EvmCallStack, + getRevertTrace, + getSourceRangeSnippet, + parseSourceMap, + SourceRange, + SourceSnippet, + TraceCollectionSubprovider, + utils, +} from '@0x/sol-trace-based-tools-common'; +import { stripHexPrefix } from 'ethereumjs-util'; +import * as _ from 'lodash'; +import { getLogger, levels, Logger } from 'loglevel'; + +/** + * 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 readonly _artifactAdapter: AbstractArtifactAdapter; + private readonly _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-trace'); + 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 sourceSnippets: SourceSnippet[] = []; + 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) && pc > 0) { + sourceRange = pcToSourceRange[pc]; + pc -= 1; + } + if (_.isUndefined(sourceRange)) { + this._logger.warn( + `could not find matching sourceRange for structLog: ${JSON.stringify( + _.omit(evmCallStackEntry.structLog, 'stack'), + )}`, + ); + continue; + } + + const fileIndex = contractData.sources.indexOf(sourceRange.fileName); + const sourceSnippet = getSourceRangeSnippet(sourceRange, contractData.sourceCodes[fileIndex]); + if (sourceSnippet !== null) { + sourceSnippets.push(sourceSnippet); + } + } + const filteredSnippets = filterSnippets(sourceSnippets); + if (filteredSnippets.length > 0) { + this._logger.error('\n\nStack trace for REVERT:\n'); + _.forEach(_.reverse(filteredSnippets), snippet => { + const traceString = getStackTraceString(snippet); + this._logger.error(traceString); + }); + this._logger.error('\n'); + } else { + this._logger.error('REVERT detected but could not determine stack trace'); + } + } +} + +// removes duplicates and if statements +function filterSnippets(sourceSnippets: SourceSnippet[]): SourceSnippet[] { + if (sourceSnippets.length === 0) { + return []; + } + const results: SourceSnippet[] = [sourceSnippets[0]]; + let prev = sourceSnippets[0]; + for (const sourceSnippet of sourceSnippets) { + if (sourceSnippet.type === 'IfStatement') { + continue; + } else if (sourceSnippet.source === prev.source) { + prev = sourceSnippet; + continue; + } + results.push(sourceSnippet); + prev = sourceSnippet; + } + return results; +} + +function getStackTraceString(sourceSnippet: SourceSnippet): string { + let result = `${sourceSnippet.fileName}:${sourceSnippet.range.start.line}:${sourceSnippet.range.start.column}`; + const snippetString = getSourceSnippetString(sourceSnippet); + if (snippetString !== '') { + result += `:\n ${snippetString}`; + } + return result; +} + +function getSourceSnippetString(sourceSnippet: SourceSnippet): string { + switch (sourceSnippet.type) { + case 'ContractDefinition': + return `contract ${sourceSnippet.name}`; + case 'FunctionDefinition': + return `function ${sourceSnippet.name}`; + default: + return `${sourceSnippet.source}`; + } +} |