diff options
author | Fabio Berger <me@fabioberger.com> | 2018-05-24 03:01:38 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-05-24 03:01:38 +0800 |
commit | 513007a82ce9065d44694516bc94771ee4bc2c6f (patch) | |
tree | fe156fa4bb2dc5fecd2df4dd1fcd82e1e3007821 /packages/sol-cov | |
parent | af0d9439d4f4e05fce8018665e375cf2db07184c (diff) | |
parent | bf18a90da79d43e90901b0cd156f15398e215d91 (diff) | |
download | dexon-sol-tools-513007a82ce9065d44694516bc94771ee4bc2c6f.tar dexon-sol-tools-513007a82ce9065d44694516bc94771ee4bc2c6f.tar.gz dexon-sol-tools-513007a82ce9065d44694516bc94771ee4bc2c6f.tar.bz2 dexon-sol-tools-513007a82ce9065d44694516bc94771ee4bc2c6f.tar.lz dexon-sol-tools-513007a82ce9065d44694516bc94771ee4bc2c6f.tar.xz dexon-sol-tools-513007a82ce9065d44694516bc94771ee4bc2c6f.tar.zst dexon-sol-tools-513007a82ce9065d44694516bc94771ee4bc2c6f.zip |
Merge pull request #589 from 0xProject/feature/truffle-sol-cov
Sol-cov artifact Adapters (truffle)
Diffstat (limited to 'packages/sol-cov')
-rw-r--r-- | packages/sol-cov/CHANGELOG.json | 22 | ||||
-rw-r--r-- | packages/sol-cov/package.json | 7 | ||||
-rw-r--r-- | packages/sol-cov/src/artifact_adapters/abstract_artifact_adapter.ts | 5 | ||||
-rw-r--r-- | packages/sol-cov/src/artifact_adapters/sol_compiler_artifact_adapter.ts | 49 | ||||
-rw-r--r-- | packages/sol-cov/src/artifact_adapters/truffle_artifact_adapter.ts | 43 | ||||
-rw-r--r-- | packages/sol-cov/src/collect_contract_data.ts | 31 | ||||
-rw-r--r-- | packages/sol-cov/src/constants.ts | 2 | ||||
-rw-r--r-- | packages/sol-cov/src/coverage_manager.ts | 105 | ||||
-rw-r--r-- | packages/sol-cov/src/coverage_subprovider.ts | 80 | ||||
-rw-r--r-- | packages/sol-cov/src/index.ts | 4 | ||||
-rw-r--r-- | packages/sol-cov/src/trace.ts | 105 | ||||
-rw-r--r-- | packages/sol-cov/src/types.ts | 4 | ||||
-rw-r--r-- | packages/sol-cov/test/sol_compiler_artifact_adapter_test.ts (renamed from packages/sol-cov/test/collect_contracts_data_test.ts) | 9 | ||||
-rw-r--r-- | packages/sol-cov/test/trace_test.ts | 57 |
14 files changed, 420 insertions, 103 deletions
diff --git a/packages/sol-cov/CHANGELOG.json b/packages/sol-cov/CHANGELOG.json index 167d5077e..0d3303231 100644 --- a/packages/sol-cov/CHANGELOG.json +++ b/packages/sol-cov/CHANGELOG.json @@ -1,6 +1,28 @@ [ { "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 + } + ] + }, + { + "timestamp": 1527009133, "version": "0.0.11", "changes": [ { diff --git a/packages/sol-cov/package.json b/packages/sol-cov/package.json index f3c8ab67e..f18315a26 100644 --- a/packages/sol-cov/package.json +++ b/packages/sol-cov/package.json @@ -46,6 +46,7 @@ }, "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/types": "^0.7.0", "@0xproject/typescript-typings": "^0.3.2", @@ -54,17 +55,21 @@ "glob": "^7.1.2", "istanbul": "^0.4.5", "lodash": "^4.17.4", + "loglevel": "^1.6.1", "mkdirp": "^0.5.1", + "rimraf": "^2.6.2", "semaphore-async-await": "^1.5.1", - "solidity-parser-antlr": "^0.2.8" + "solidity-parser-antlr": "^0.2.11" }, "devDependencies": { "@0xproject/monorepo-scripts": "^0.1.20", "@0xproject/tslint-config": "^0.4.18", "@types/istanbul": "^0.4.30", + "@types/loglevel": "^1.5.3", "@types/mkdirp": "^0.5.1", "@types/mocha": "^2.2.42", "@types/node": "^8.0.53", + "@types/rimraf": "^2.0.2", "chai": "^4.0.1", "copyfiles": "^1.2.0", "dirty-chai": "^2.0.1", diff --git a/packages/sol-cov/src/artifact_adapters/abstract_artifact_adapter.ts b/packages/sol-cov/src/artifact_adapters/abstract_artifact_adapter.ts new file mode 100644 index 000000000..fcc6562ad --- /dev/null +++ b/packages/sol-cov/src/artifact_adapters/abstract_artifact_adapter.ts @@ -0,0 +1,5 @@ +import { ContractData } from '../types'; + +export abstract class AbstractArtifactAdapter { + public abstract async collectContractsDataAsync(): Promise<ContractData[]>; +} diff --git a/packages/sol-cov/src/artifact_adapters/sol_compiler_artifact_adapter.ts b/packages/sol-cov/src/artifact_adapters/sol_compiler_artifact_adapter.ts new file mode 100644 index 000000000..d08828bf6 --- /dev/null +++ b/packages/sol-cov/src/artifact_adapters/sol_compiler_artifact_adapter.ts @@ -0,0 +1,49 @@ +import * as fs from 'fs'; +import * as glob from 'glob'; +import * as _ from 'lodash'; +import * as path from 'path'; + +import { ContractData } from '../types'; + +import { AbstractArtifactAdapter } from './abstract_artifact_adapter'; + +const CONFIG_FILE = 'compiler.json'; + +export class SolCompilerArtifactAdapter extends AbstractArtifactAdapter { + private _artifactsPath: string; + private _sourcesPath: string; + constructor(artifactsPath?: string, sourcesPath?: string) { + super(); + const config = JSON.parse(fs.readFileSync(CONFIG_FILE).toString()); + if (_.isUndefined(artifactsPath) && _.isUndefined(config.artifactsDir)) { + throw new Error(`artifactsDir not found in ${CONFIG_FILE}`); + } + this._artifactsPath = config.artifactsDir; + if (_.isUndefined(sourcesPath) && _.isUndefined(config.contractsDir)) { + throw new Error(`contractsDir not found in ${CONFIG_FILE}`); + } + this._sourcesPath = config.contractsDir; + } + public async collectContractsDataAsync(): Promise<ContractData[]> { + const artifactsGlob = `${this._artifactsPath}/**/*.json`; + const artifactFileNames = glob.sync(artifactsGlob, { absolute: true }); + const contractsData: ContractData[] = []; + for (const artifactFileName of artifactFileNames) { + const artifact = JSON.parse(fs.readFileSync(artifactFileName).toString()); + let sources = _.keys(artifact.sources); + sources = _.map(sources, relativeFilePath => path.resolve(this._sourcesPath, relativeFilePath)); + const contractName = artifact.contractName; + const sourceCodes = _.map(sources, (source: string) => fs.readFileSync(source).toString()); + const contractData = { + sourceCodes, + sources, + bytecode: artifact.compilerOutput.evm.bytecode.object, + sourceMap: artifact.compilerOutput.evm.bytecode.sourceMap, + runtimeBytecode: artifact.compilerOutput.evm.deployedBytecode.object, + sourceMapRuntime: artifact.compilerOutput.evm.deployedBytecode.sourceMap, + }; + contractsData.push(contractData); + } + return contractsData; + } +} diff --git a/packages/sol-cov/src/artifact_adapters/truffle_artifact_adapter.ts b/packages/sol-cov/src/artifact_adapters/truffle_artifact_adapter.ts new file mode 100644 index 000000000..c7f21b6eb --- /dev/null +++ b/packages/sol-cov/src/artifact_adapters/truffle_artifact_adapter.ts @@ -0,0 +1,43 @@ +import { Compiler, CompilerOptions } from '@0xproject/sol-compiler'; +import * as fs from 'fs'; +import * as glob from 'glob'; +import * as _ from 'lodash'; +import * as path from 'path'; +import * as rimraf from 'rimraf'; + +import { ContractData } from '../types'; + +import { AbstractArtifactAdapter } from './abstract_artifact_adapter'; +import { SolCompilerArtifactAdapter } from './sol_compiler_artifact_adapter'; + +export class TruffleArtifactAdapter extends AbstractArtifactAdapter { + private _solcVersion: string; + private _sourcesPath: string; + constructor(sourcesPath: string, solcVersion: string) { + super(); + this._solcVersion = solcVersion; + this._sourcesPath = sourcesPath; + } + public async collectContractsDataAsync(): Promise<ContractData[]> { + const artifactsDir = '.0x-artifacts'; + const compilerOptions: CompilerOptions = { + contractsDir: this._sourcesPath, + artifactsDir, + compilerSettings: { + outputSelection: { + ['*']: { + ['*']: ['abi', 'evm.bytecode.object', 'evm.deployedBytecode.object'], + }, + }, + }, + contracts: '*', + solcVersion: this._solcVersion, + }; + const compiler = new Compiler(compilerOptions); + await compiler.compileAsync(); + const solCompilerArtifactAdapter = new SolCompilerArtifactAdapter(artifactsDir, this._sourcesPath); + const contractsDataFrom0xArtifacts = await solCompilerArtifactAdapter.collectContractsDataAsync(); + rimraf.sync(artifactsDir); + return contractsDataFrom0xArtifacts; + } +} diff --git a/packages/sol-cov/src/collect_contract_data.ts b/packages/sol-cov/src/collect_contract_data.ts deleted file mode 100644 index 2c2a12835..000000000 --- a/packages/sol-cov/src/collect_contract_data.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as fs from 'fs'; -import * as glob from 'glob'; -import * as _ from 'lodash'; -import * as path from 'path'; - -import { ContractData } from './types'; - -export const collectContractsData = (artifactsPath: string, sourcesPath: string) => { - const artifactsGlob = `${artifactsPath}/**/*.json`; - const artifactFileNames = glob.sync(artifactsGlob, { absolute: true }); - const contractsData: ContractData[] = []; - _.forEach(artifactFileNames, artifactFileName => { - const artifact = JSON.parse(fs.readFileSync(artifactFileName).toString()); - const sources = _.keys(artifact.sources); - const contractName = artifact.contractName; - // We don't compute coverage for dependencies - const sourceCodes = _.map(sources, (source: string) => - fs.readFileSync(path.join(sourcesPath, source)).toString(), - ); - const contractData = { - sourceCodes, - sources, - bytecode: artifact.compilerOutput.evm.bytecode.object, - sourceMap: artifact.compilerOutput.evm.bytecode.sourceMap, - runtimeBytecode: artifact.compilerOutput.evm.deployedBytecode.object, - sourceMapRuntime: artifact.compilerOutput.evm.deployedBytecode.sourceMap, - }; - contractsData.push(contractData); - }); - return contractsData; -}; diff --git a/packages/sol-cov/src/constants.ts b/packages/sol-cov/src/constants.ts index 64d2228a3..34d62b537 100644 --- a/packages/sol-cov/src/constants.ts +++ b/packages/sol-cov/src/constants.ts @@ -1,6 +1,6 @@ // tslint:disable:number-literal-format export const constants = { - NEW_CONTRACT: 'NEW_CONTRACT', + NEW_CONTRACT: 'NEW_CONTRACT' as 'NEW_CONTRACT', PUSH1: 0x60, PUSH2: 0x61, PUSH32: 0x7f, diff --git a/packages/sol-cov/src/coverage_manager.ts b/packages/sol-cov/src/coverage_manager.ts index 800ca96dd..31b0e6fbc 100644 --- a/packages/sol-cov/src/coverage_manager.ts +++ b/packages/sol-cov/src/coverage_manager.ts @@ -1,12 +1,13 @@ import { promisify } from '@0xproject/utils'; -import { addHexPrefix } from 'ethereumjs-util'; +import { addHexPrefix, stripHexPrefix } from 'ethereumjs-util'; import * as fs from 'fs'; import { Collector } from 'istanbul'; import * as _ from 'lodash'; +import { getLogger, levels, Logger, LogLevel } from 'loglevel'; import * as mkdirp from 'mkdirp'; import * as path from 'path'; -import { collectContractsData } from './collect_contract_data'; +import { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter'; import { collectCoverageEntries } from './collect_coverage_entries'; import { constants } from './constants'; import { parseSourceMap } from './source_maps'; @@ -34,41 +35,23 @@ import { utils } from './utils'; const mkdirpAsync = promisify<undefined>(mkdirp); export class CoverageManager { - private _sourcesPath: string; + private _artifactAdapter: AbstractArtifactAdapter; + private _logger: Logger; private _traceInfos: TraceInfo[] = []; - private _contractsData: ContractData[] = []; private _getContractCodeAsync: (address: string) => Promise<string>; - constructor( - artifactsPath: string, - sourcesPath: string, - getContractCodeAsync: (address: string) => Promise<string>, - ) { - this._getContractCodeAsync = getContractCodeAsync; - this._sourcesPath = sourcesPath; - this._contractsData = collectContractsData(artifactsPath, this._sourcesPath); - } - public appendTraceInfo(traceInfo: TraceInfo): void { - this._traceInfos.push(traceInfo); - } - public async writeCoverageAsync(): Promise<void> { - const finalCoverage = await this._computeCoverageAsync(); - const stringifiedCoverage = JSON.stringify(finalCoverage, null, '\t'); - await mkdirpAsync('coverage'); - fs.writeFileSync('coverage/coverage.json', stringifiedCoverage); - } - private _getSingleFileCoverageForTrace( + private static _getSingleFileCoverageForTrace( contractData: ContractData, coveredPcs: number[], pcToSourceRange: { [programCounter: number]: SourceRange }, fileIndex: number, ): Coverage { - const fileName = contractData.sources[fileIndex]; + const absoluteFileName = contractData.sources[fileIndex]; const coverageEntriesDescription = collectCoverageEntries(contractData.sourceCodes[fileIndex]); let sourceRanges = _.map(coveredPcs, coveredPc => pcToSourceRange[coveredPc]); sourceRanges = _.compact(sourceRanges); // Some PC's don't map to a source range and we just ignore them. // By default lodash does a shallow object comparasion. We JSON.stringify them and compare as strings. sourceRanges = _.uniqBy(sourceRanges, s => JSON.stringify(s)); // We don't care if one PC was covered multiple times within a single transaction - sourceRanges = _.filter(sourceRanges, sourceRange => sourceRange.fileName === fileName); + sourceRanges = _.filter(sourceRanges, sourceRange => sourceRange.fileName === absoluteFileName); const branchCoverage: BranchCoverage = {}; const branchIds = _.keys(coverageEntriesDescription.branchMap); for (const branchId of branchIds) { @@ -118,7 +101,6 @@ export class CoverageManager { ); statementCoverage[modifierStatementId] = isModifierCovered; } - const absoluteFileName = path.join(this._sourcesPath, fileName); const partialCoverage = { [absoluteFileName]: { ...coverageEntriesDescription, @@ -131,18 +113,63 @@ export class CoverageManager { }; return partialCoverage; } + private static _bytecodeToBytecodeRegex(bytecode: string): string { + const bytecodeRegex = bytecode + // Library linking placeholder: __ConvertLib____________________________ + .replace(/_.*_/, '.*') + // Last 86 characters is solidity compiler metadata that's different between compilations + .replace(/.{86}$/, '') + // Libraries contain their own address at the beginning of the code and it's impossible to know it in advance + .replace(/^0x730000000000000000000000000000000000000000/, '0x73........................................'); + return bytecodeRegex; + } + private static _getContractDataIfExists(contractsData: ContractData[], bytecode: string): ContractData | undefined { + if (!bytecode.startsWith('0x')) { + throw new Error(`0x hex prefix missing: ${bytecode}`); + } + const contractData = _.find(contractsData, contractDataCandidate => { + const bytecodeRegex = CoverageManager._bytecodeToBytecodeRegex(contractDataCandidate.bytecode); + const runtimeBytecodeRegex = CoverageManager._bytecodeToBytecodeRegex( + contractDataCandidate.runtimeBytecode, + ); + // We use that function to find by bytecode or runtimeBytecode. Those are quasi-random strings so + // collisions are practically impossible and it allows us to reuse that code + return !_.isNull(bytecode.match(bytecodeRegex)) || !_.isNull(bytecode.match(runtimeBytecodeRegex)); + }); + return contractData; + } + constructor( + artifactAdapter: AbstractArtifactAdapter, + getContractCodeAsync: (address: string) => Promise<string>, + isVerbose: boolean, + ) { + this._getContractCodeAsync = getContractCodeAsync; + this._artifactAdapter = artifactAdapter; + this._logger = getLogger('sol-cov'); + this._logger.setLevel(isVerbose ? levels.TRACE : levels.ERROR); + } + public appendTraceInfo(traceInfo: TraceInfo): void { + this._traceInfos.push(traceInfo); + } + public async writeCoverageAsync(): Promise<void> { + const finalCoverage = await this._computeCoverageAsync(); + const stringifiedCoverage = JSON.stringify(finalCoverage, null, '\t'); + await mkdirpAsync('coverage'); + fs.writeFileSync('coverage/coverage.json', stringifiedCoverage); + } private async _computeCoverageAsync(): Promise<Coverage> { + const contractsData = await this._artifactAdapter.collectContractsDataAsync(); const collector = new Collector(); for (const traceInfo of this._traceInfos) { if (traceInfo.address !== constants.NEW_CONTRACT) { // Runtime transaction - let runtimeBytecode = (traceInfo as TraceInfoExistingContract).runtimeBytecode; - runtimeBytecode = addHexPrefix(runtimeBytecode); - const contractData = _.find(this._contractsData, { runtimeBytecode }) as ContractData; + const runtimeBytecode = (traceInfo as TraceInfoExistingContract).runtimeBytecode; + const contractData = CoverageManager._getContractDataIfExists(contractsData, runtimeBytecode); if (_.isUndefined(contractData)) { - throw new Error(`Transaction to an unknown address: ${traceInfo.address}`); + this._logger.warn(`Transaction to an unknown address: ${traceInfo.address}`); + continue; } - const bytecodeHex = contractData.runtimeBytecode.slice(2); + const bytecodeHex = stripHexPrefix(runtimeBytecode); const sourceMap = contractData.sourceMapRuntime; const pcToSourceRange = parseSourceMap( contractData.sourceCodes, @@ -151,7 +178,7 @@ export class CoverageManager { contractData.sources, ); for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) { - const singleFileCoverageForTrace = this._getSingleFileCoverageForTrace( + const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace( contractData, traceInfo.coveredPcs, pcToSourceRange, @@ -161,15 +188,13 @@ export class CoverageManager { } } else { // Contract creation transaction - let bytecode = (traceInfo as TraceInfoNewContract).bytecode; - bytecode = addHexPrefix(bytecode); - const contractData = _.find(this._contractsData, contractDataCandidate => - bytecode.startsWith(contractDataCandidate.bytecode), - ) as ContractData; + const bytecode = (traceInfo as TraceInfoNewContract).bytecode; + const contractData = CoverageManager._getContractDataIfExists(contractsData, bytecode); if (_.isUndefined(contractData)) { - throw new Error(`Unknown contract creation transaction`); + this._logger.warn(`Unknown contract creation transaction`); + continue; } - const bytecodeHex = contractData.bytecode.slice(2); + const bytecodeHex = stripHexPrefix(bytecode); const sourceMap = contractData.sourceMap; const pcToSourceRange = parseSourceMap( contractData.sourceCodes, @@ -178,7 +203,7 @@ export class CoverageManager { contractData.sources, ); for (let fileIndex = 0; fileIndex < contractData.sources.length; fileIndex++) { - const singleFileCoverageForTrace = this._getSingleFileCoverageForTrace( + const singleFileCoverageForTrace = CoverageManager._getSingleFileCoverageForTrace( contractData, traceInfo.coveredPcs, pcToSourceRange, diff --git a/packages/sol-cov/src/coverage_subprovider.ts b/packages/sol-cov/src/coverage_subprovider.ts index 08efeaa24..438339a3f 100644 --- a/packages/sol-cov/src/coverage_subprovider.ts +++ b/packages/sol-cov/src/coverage_subprovider.ts @@ -1,11 +1,14 @@ import { Callback, ErrorCallback, NextCallback, Subprovider } from '@0xproject/subproviders'; import { BlockParam, CallData, JSONRPCRequestPayload, TransactionTrace, TxData } from '@0xproject/types'; +import * as fs from 'fs'; import * as _ from 'lodash'; import { Lock } from 'semaphore-async-await'; +import { AbstractArtifactAdapter } from './artifact_adapters/abstract_artifact_adapter'; import { constants } from './constants'; import { CoverageManager } from './coverage_manager'; -import { TraceInfoExistingContract, TraceInfoNewContract } from './types'; +import { getTracesByContractAddress } from './trace'; +import { BlockParamLiteral, TraceInfoExistingContract, TraceInfoNewContract } from './types'; interface MaybeFakeTxData extends TxData { isFakeTransaction?: boolean; @@ -26,15 +29,15 @@ export class CoverageSubprovider extends Subprovider { private _defaultFromAddress: string; /** * Instantiates a CoverageSubprovider instance - * @param artifactsPath Path to the smart contract artifacts - * @param sourcesPath Path to the smart contract source files + * @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(artifactsPath: string, sourcesPath: string, defaultFromAddress: string) { + constructor(artifactAdapter: AbstractArtifactAdapter, defaultFromAddress: string, isVerbose: boolean = true) { super(); this._lock = new Lock(); this._defaultFromAddress = defaultFromAddress; - this._coverageManager = new CoverageManager(artifactsPath, sourcesPath, this._getContractCodeAsync.bind(this)); + this._coverageManager = new CoverageManager(artifactAdapter, this._getContractCodeAsync.bind(this), isVerbose); } /** * Write the test coverage results to a file in Istanbul format. @@ -86,7 +89,7 @@ export class CoverageSubprovider extends Subprovider { } else { const payload = { method: 'eth_getBlockByNumber', - params: ['latest', true], + params: [BlockParamLiteral.Latest, true], }; const jsonRPCResponsePayload = await this.emitPayloadAsync(payload); const transactions = jsonRPCResponsePayload.result.transactions; @@ -115,29 +118,54 @@ export class CoverageSubprovider extends Subprovider { private async _recordTxTraceAsync(address: string, data: string | undefined, txHash: string): Promise<void> { let payload = { method: 'debug_traceTransaction', - params: [txHash, { disableMemory: true, disableStack: true, disableStorage: true }], // TODO For now testrpc just ignores those parameters https://github.com/trufflesuite/ganache-cli/issues/489 + params: [txHash, { disableMemory: true, disableStack: false, disableStorage: true }], }; - const jsonRPCResponsePayload = await this.emitPayloadAsync(payload); + let jsonRPCResponsePayload = await this.emitPayloadAsync(payload); const trace: TransactionTrace = jsonRPCResponsePayload.result; - const coveredPcs = _.map(trace.structLogs, log => log.pc); + const tracesByContractAddress = getTracesByContractAddress(trace.structLogs, address); + const subcallAddresses = _.keys(tracesByContractAddress); if (address === constants.NEW_CONTRACT) { - const traceInfo: TraceInfoNewContract = { - coveredPcs, - txHash, - address: address as 'NEW_CONTRACT', - bytecode: data as string, - }; - this._coverageManager.appendTraceInfo(traceInfo); + for (const subcallAddress of subcallAddresses) { + let traceInfo: TraceInfoNewContract | TraceInfoExistingContract; + if (subcallAddress === 'NEW_CONTRACT') { + const traceForThatSubcall = tracesByContractAddress[subcallAddress]; + const coveredPcs = _.map(traceForThatSubcall, log => log.pc); + traceInfo = { + coveredPcs, + txHash, + address: constants.NEW_CONTRACT, + bytecode: data as string, + }; + } else { + payload = { method: 'eth_getCode', params: [subcallAddress, BlockParamLiteral.Latest] }; + jsonRPCResponsePayload = await this.emitPayloadAsync(payload); + const runtimeBytecode = jsonRPCResponsePayload.result; + const traceForThatSubcall = tracesByContractAddress[subcallAddress]; + const coveredPcs = _.map(traceForThatSubcall, log => log.pc); + traceInfo = { + coveredPcs, + txHash, + address: subcallAddress, + runtimeBytecode, + }; + } + this._coverageManager.appendTraceInfo(traceInfo); + } } else { - payload = { method: 'eth_getCode', params: [address, 'latest'] }; - const runtimeBytecode = (await this.emitPayloadAsync(payload)).result; - const traceInfo: TraceInfoExistingContract = { - coveredPcs, - txHash, - address, - runtimeBytecode, - }; - this._coverageManager.appendTraceInfo(traceInfo); + for (const subcallAddress of subcallAddresses) { + payload = { method: 'eth_getCode', params: [subcallAddress, BlockParamLiteral.Latest] }; + jsonRPCResponsePayload = await this.emitPayloadAsync(payload); + const runtimeBytecode = jsonRPCResponsePayload.result; + const traceForThatSubcall = tracesByContractAddress[subcallAddress]; + const coveredPcs = _.map(traceForThatSubcall, log => log.pc); + const traceInfo: TraceInfoExistingContract = { + coveredPcs, + txHash, + address: subcallAddress, + runtimeBytecode, + }; + this._coverageManager.appendTraceInfo(traceInfo); + } } } private async _recordCallTraceAsync(callData: Partial<CallData>, blockNumber: BlockParam): Promise<void> { @@ -168,7 +196,7 @@ export class CoverageSubprovider extends Subprovider { private async _getContractCodeAsync(address: string): Promise<string> { const payload = { method: 'eth_getCode', - params: [address, 'latest'], + params: [address, BlockParamLiteral.Latest], }; const jsonRPCResponsePayload = await this.emitPayloadAsync(payload); const contractCode: string = jsonRPCResponsePayload.result; diff --git a/packages/sol-cov/src/index.ts b/packages/sol-cov/src/index.ts index e5c5e5be3..7a2afbe80 100644 --- a/packages/sol-cov/src/index.ts +++ b/packages/sol-cov/src/index.ts @@ -1 +1,5 @@ export { CoverageSubprovider } from './coverage_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'; +export { ContractData } from './types'; diff --git a/packages/sol-cov/src/trace.ts b/packages/sol-cov/src/trace.ts new file mode 100644 index 000000000..6caea1610 --- /dev/null +++ b/packages/sol-cov/src/trace.ts @@ -0,0 +1,105 @@ +import { OpCode, StructLog, TransactionTrace } from '@0xproject/types'; +import { addressUtils, BigNumber, logUtils } from '@0xproject/utils'; +import { addHexPrefix, stripHexPrefix } from 'ethereumjs-util'; +import * as fs from 'fs'; +import * as _ from 'lodash'; + +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]; + // 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) { + 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 + 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; + const jumpAddressOffset = 1; + const newAddress = 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]; + if (nextStructLog.depth !== structLog.depth) { + callStack.push(newAddress); + traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat( + currentTraceSegment, + ); + currentTraceSegment = []; + } + } else if (isEndOpcode) { + const currentAddress = callStack.pop() as string; + traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat( + currentTraceSegment, + ); + currentTraceSegment = []; + 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.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 traceByContractAddress; + } else { + if (structLog !== _.last(structLogs)) { + const nextStructLog = structLogs[i + 1]; + if (nextStructLog.depth === structLog.depth) { + continue; + } else if (nextStructLog.depth === structLog.depth - 1) { + const currentAddress = callStack.pop() as string; + traceByContractAddress[currentAddress] = (traceByContractAddress[currentAddress] || []).concat( + currentTraceSegment, + ); + currentTraceSegment = []; + } else { + throw new Error('Malformed trace. Unexpected call depth change'); + } + } + } + } + if (callStack.length !== 0) { + throw new Error('Malformed trace. Call stack non empty at the end'); + } + if (currentTraceSegment.length !== 0) { + throw new Error('Malformed trace. Current trace segment non empty at the end'); + } + return traceByContractAddress; +} diff --git a/packages/sol-cov/src/types.ts b/packages/sol-cov/src/types.ts index 01359d858..4c3de55a1 100644 --- a/packages/sol-cov/src/types.ts +++ b/packages/sol-cov/src/types.ts @@ -98,3 +98,7 @@ export interface TraceInfoExistingContract extends TraceInfoBase { } export type TraceInfo = TraceInfoNewContract | TraceInfoExistingContract; + +export enum BlockParamLiteral { + Latest = 'latest', +} diff --git a/packages/sol-cov/test/collect_contracts_data_test.ts b/packages/sol-cov/test/sol_compiler_artifact_adapter_test.ts index d84ac5a39..0ebad669b 100644 --- a/packages/sol-cov/test/collect_contracts_data_test.ts +++ b/packages/sol-cov/test/sol_compiler_artifact_adapter_test.ts @@ -4,16 +4,17 @@ import 'make-promises-safe'; import 'mocha'; import * as path from 'path'; -import { collectContractsData } from '../src/collect_contract_data'; +import { SolCompilerArtifactAdapter } from '../src/artifact_adapters/sol_compiler_artifact_adapter'; const expect = chai.expect; -describe('Collect contracts data', () => { +describe('SolCompilerArtifactAdapter', () => { describe('#collectContractsData', () => { - it('correctly collects contracts data', () => { + it('correctly collects contracts data', async () => { const artifactsPath = path.resolve(__dirname, 'fixtures/artifacts'); const sourcesPath = path.resolve(__dirname, 'fixtures/contracts'); - const contractsData = collectContractsData(artifactsPath, sourcesPath); + const zeroExArtifactsAdapter = new SolCompilerArtifactAdapter(artifactsPath, sourcesPath); + const contractsData = await zeroExArtifactsAdapter.collectContractsDataAsync(); _.forEach(contractsData, contractData => { expect(contractData).to.have.keys([ 'sourceCodes', diff --git a/packages/sol-cov/test/trace_test.ts b/packages/sol-cov/test/trace_test.ts new file mode 100644 index 000000000..c140cba0d --- /dev/null +++ b/packages/sol-cov/test/trace_test.ts @@ -0,0 +1,57 @@ +import { OpCode, StructLog } from '@0xproject/types'; +import * as chai from 'chai'; +import * as fs from 'fs'; +import * as _ from 'lodash'; +import 'mocha'; +import * as path from 'path'; + +import { getTracesByContractAddress } from '../src/trace'; + +const expect = chai.expect; + +const DEFAULT_STRUCT_LOG: StructLog = { + depth: 0, + error: '', + gas: 0, + gasCost: 0, + memory: [], + op: OpCode.Invalid, + pc: 0, + stack: [], + storage: {}, +}; + +function addDefaultStructLogFields(compactStructLog: Partial<StructLog> & { op: OpCode; depth: number }): StructLog { + return { ...DEFAULT_STRUCT_LOG, ...compactStructLog }; +} + +describe('Trace', () => { + describe('#getTracesByContractAddress', () => { + it('correctly splits trace by contract address', () => { + const delegateCallAddress = '0x0000000000000000000000000000000000000002'; + const trace = [ + { + op: OpCode.DelegateCall, + stack: [delegateCallAddress, '0x'], + depth: 0, + }, + { + op: OpCode.Return, + depth: 1, + }, + { + op: OpCode.Return, + depth: 0, + }, + ]; + const fullTrace = _.map(trace, compactStructLog => addDefaultStructLogFields(compactStructLog)); + const startAddress = '0x0000000000000000000000000000000000000001'; + const traceByContractAddress = getTracesByContractAddress(fullTrace, startAddress); + const expectedTraceByContractAddress = { + [startAddress]: [fullTrace[0], fullTrace[2]], + [delegateCallAddress]: [fullTrace[1]], + }; + expect(traceByContractAddress).to.be.deep.equal(expectedTraceByContractAddress); + }); + }); +}); |