This repository has been archived by the owner on Jul 9, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 466
Introduce subprovider for printing revert stack traces #705
Merged
Merged
Changes from 7 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
7ab9216
Introduce subprovider for printing revert stack traces
albrow 263bfb1
Fix a bug in revert_trace.ts
albrow a9c23b7
Reverse order of stack trace to match behavior of most other language…
albrow d9292a7
Remove unused variables and other small fixes
albrow 5a8539a
Fix linter errors and remove coverage.json
albrow 8975607
De-duplicate code by refactoring subprovider classes
albrow ef61c35
Fix linter errors
albrow d118533
Remove redundant check in trace.ts and revert_trace.ts
albrow 7032825
Change wording of error message when you try to use more than one sub…
albrow File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
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], | ||
); | ||
|
||
if (structLog === _.last(normalizedStructLogs)) { | ||
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 = 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 []; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe use or instead of and?