Merge pull request #3068 from ethereum/improve_gas_in_debugger

Improve gas in debugger
pull/3165/head
yann300 2 years ago committed by GitHub
commit 569f7d53f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 19
      apps/debugger/src/app/debugger-api.ts
  2. 4
      apps/remix-ide-e2e/src/commands/waitForElementContainsText.ts
  3. 10
      apps/remix-ide-e2e/src/tests/debugger.test.ts
  4. 2
      apps/remix-ide-e2e/src/tests/solidityUnittests.test.ts
  5. 13
      libs/remix-core-plugin/src/lib/offset-line-to-column-converter.ts
  6. 10
      libs/remix-debug/src/Ethdebugger.ts
  7. 48
      libs/remix-debug/src/debugger/debugger.ts
  8. 4
      libs/remix-debug/src/solidity-decoder/decodeInfo.ts
  9. 244
      libs/remix-debug/src/solidity-decoder/internalCallTree.ts
  10. 4
      libs/remix-debug/src/solidity-decoder/localDecoder.ts
  11. 19
      libs/remix-debug/src/solidity-decoder/solidityProxy.ts
  12. 2
      libs/remix-debug/src/solidity-decoder/types/StringType.ts
  13. 12
      libs/remix-debug/src/source/offsetToLineColumnConverter.ts
  14. 23
      libs/remix-debug/src/source/sourceLocationTracker.ts
  15. 2
      libs/remix-debug/test/decoder/localDecoder.ts
  16. 90
      libs/remix-debug/test/decoder/localsTests/int.ts
  17. 5
      libs/remix-ui/app/src/lib/remix-app/components/modals/matomo.tsx
  18. 13
      libs/remix-ui/debugger-ui/src/lib/debugger-ui.tsx
  19. 2
      libs/remix-ui/debugger-ui/src/lib/idebugger-api.ts
  20. 10
      libs/remix-ui/debugger-ui/src/lib/vm-debugger/assembly-items.tsx
  21. 6
      libs/remix-ui/debugger-ui/src/lib/vm-debugger/vm-debugger-head.tsx
  22. 4
      libs/remix-ui/debugger-ui/src/lib/vm-debugger/vm-debugger.tsx
  23. 24
      libs/remix-ui/debugger-ui/src/reducers/assembly-items.ts

@ -1,7 +1,7 @@
import Web3 from 'web3' import Web3 from 'web3'
import remixDebug, { TransactionDebugger as Debugger } from '@remix-project/remix-debug' import remixDebug, { TransactionDebugger as Debugger } from '@remix-project/remix-debug'
import { CompilerAbstract } from '@remix-project/remix-solidity' import { CompilerAbstract } from '@remix-project/remix-solidity'
import { lineText } from '@remix-ui/editor'
export const DebuggerApiMixin = (Base) => class extends Base { export const DebuggerApiMixin = (Base) => class extends Base {
@ -39,10 +39,25 @@ export const DebuggerApiMixin = (Base) => class extends Base {
async discardHighlight () { async discardHighlight () {
await this.call('editor', 'discardHighlight') await this.call('editor', 'discardHighlight')
await this.call('editor', 'discardLineTexts' as any)
} }
async highlight (lineColumnPos, path) { async highlight (lineColumnPos, path, rawLocation, stepDetail, lineGasCost) {
await this.call('editor', 'highlight', lineColumnPos, path, '', { focus: true }) await this.call('editor', 'highlight', lineColumnPos, path, '', { focus: true })
const label = `${stepDetail.op} costs ${stepDetail.gasCost} gas - this line costs ${lineGasCost} gas - ${stepDetail.gas} gas left`
const linetext: lineText = {
content: label,
position: lineColumnPos,
hide: false,
className: 'text-muted small',
afterContentClassName: 'text-muted small fas fa-gas-pump pl-4',
from: 'debugger',
hoverMessage: [{
value: label,
},
],
}
await this.call('editor', 'addLineText' as any, linetext, path)
} }
async getFile (path) { async getFile (path) {

@ -4,8 +4,10 @@ import EventEmitter from 'events'
class WaitForElementContainsText extends EventEmitter { class WaitForElementContainsText extends EventEmitter {
command (this: NightwatchBrowser, id: string, value: string, timeout = 10000): NightwatchBrowser { command (this: NightwatchBrowser, id: string, value: string, timeout = 10000): NightwatchBrowser {
let waitId // eslint-disable-line let waitId // eslint-disable-line
let currentValue
const runid = setInterval(() => { const runid = setInterval(() => {
this.api.getText(id, (result) => { this.api.getText(id, (result) => {
currentValue = result.value
if (typeof result.value === 'string' && result.value.indexOf(value) !== -1) { if (typeof result.value === 'string' && result.value.indexOf(value) !== -1) {
clearInterval(runid) clearInterval(runid)
clearTimeout(waitId) clearTimeout(waitId)
@ -17,7 +19,7 @@ class WaitForElementContainsText extends EventEmitter {
waitId = setTimeout(() => { waitId = setTimeout(() => {
clearInterval(runid) clearInterval(runid)
this.api.assert.fail(`TimeoutError: An error occurred while running .waitForElementContainsText() command on ${id} after ${timeout} milliseconds`) this.api.assert.fail(`TimeoutError: An error occurred while running .waitForElementContainsText() command on ${id} after ${timeout} milliseconds. expected: ${value} - got: ${currentValue}`)
}, timeout) }, timeout)
return this return this
} }

@ -97,11 +97,6 @@ module.exports = {
locateStrategy: 'xpath', locateStrategy: 'xpath',
selector: '//*[@data-id="treeViewLivm trace step" and contains(.,"545")]', selector: '//*[@data-id="treeViewLivm trace step" and contains(.,"545")]',
}) })
.goToVMTraceStep(10)
.waitForElementVisible({
locateStrategy: 'xpath',
selector: '//*[@data-id="treeViewLivm trace step" and contains(.,"10")]',
})
.getEditorValue((content) => { .getEditorValue((content) => {
browser.assert.ok(content.indexOf(`constructor (string memory name_, string memory symbol_) { browser.assert.ok(content.indexOf(`constructor (string memory name_, string memory symbol_) {
_name = name_; _name = name_;
@ -109,6 +104,11 @@ module.exports = {
}`) !== -1, }`) !== -1,
'current displayed content is not from the ERC20 source code') 'current displayed content is not from the ERC20 source code')
}) })
.goToVMTraceStep(10)
.waitForElementVisible({
locateStrategy: 'xpath',
selector: '//*[@data-id="treeViewLivm trace step" and contains(.,"10")]',
})
}, },
'Should display correct source highlighting while debugging a contract which has ABIEncoderV2 #group2': function (browser: NightwatchBrowser) { 'Should display correct source highlighting while debugging a contract which has ABIEncoderV2 #group2': function (browser: NightwatchBrowser) {

@ -290,7 +290,7 @@ module.exports = {
.waitForElementContainsText('*[data-id="sidePanelSwapitTitle"]', 'DEBUGGER', 60000) .waitForElementContainsText('*[data-id="sidePanelSwapitTitle"]', 'DEBUGGER', 60000)
.waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalFailed()', 60000) .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalFailed()', 60000)
.waitForElementVisible('*[data-id="dropdownPanelSolidityLocals"]').pause(1000) .waitForElementVisible('*[data-id="dropdownPanelSolidityLocals"]').pause(1000)
.waitForElementContainsText('*[data-id="solidityLocals"]', 'no locals', 60000) .waitForElementContainsText('*[data-id="solidityLocals"]', 'No data available', 60000)
.goToVMTraceStep(316) .goToVMTraceStep(316)
.waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalFailed()', 60000) .waitForElementContainsText('*[data-id="functionPanel"]', 'checkWinningProposalFailed()', 60000)
.waitForElementContainsText('*[data-id="functionPanel"]', 'vote(proposal)', 60000) .waitForElementContainsText('*[data-id="functionPanel"]', 'vote(proposal)', 60000)

@ -13,9 +13,11 @@ const profile = {
export class OffsetToLineColumnConverter extends Plugin { export class OffsetToLineColumnConverter extends Plugin {
lineBreakPositionsByContent: Record<number, Array<number>> lineBreakPositionsByContent: Record<number, Array<number>>
sourceMappingDecoder: any sourceMappingDecoder: any
offsetConvertion: any
constructor () { constructor () {
super(profile) super(profile)
this.lineBreakPositionsByContent = {} this.lineBreakPositionsByContent = {}
this.offsetConvertion = {}
this.sourceMappingDecoder = sourceMappingDecoder this.sourceMappingDecoder = sourceMappingDecoder
} }
@ -45,7 +47,15 @@ export class OffsetToLineColumnConverter extends Plugin {
} }
} }
} }
return this.sourceMappingDecoder.convertOffsetToLineColumn(rawLocation, this.lineBreakPositionsByContent[file])
const token = `${rawLocation.start}:${rawLocation.length}:${file}`
if (this.offsetConvertion[token]) {
return this.offsetConvertion[token]
} else {
const convertion = this.sourceMappingDecoder.convertOffsetToLineColumn(rawLocation, this.lineBreakPositionsByContent[file])
this.offsetConvertion[token] = convertion
return convertion
}
} }
/** /**
@ -64,6 +74,7 @@ export class OffsetToLineColumnConverter extends Plugin {
*/ */
clear () { clear () {
this.lineBreakPositionsByContent = {} this.lineBreakPositionsByContent = {}
this.offsetConvertion = {}
} }
/** /**

@ -32,9 +32,11 @@ export class Ethdebugger {
storageResolver storageResolver
callTree callTree
breakpointManager breakpointManager
offsetToLineColumnConverter
constructor (opts) { constructor (opts) {
this.compilationResult = opts.compilationResult || function (contractAddress) { return null } this.compilationResult = opts.compilationResult || function (contractAddress) { return null }
this.offsetToLineColumnConverter = opts.offsetToLineColumnConverter
this.web3 = opts.web3 this.web3 = opts.web3
this.opts = opts this.opts = opts
@ -49,7 +51,8 @@ export class Ethdebugger {
this.traceManager, this.traceManager,
this.solidityProxy, this.solidityProxy,
this.codeManager, this.codeManager,
{ ...opts, includeLocalVariables }) { ...opts, includeLocalVariables },
this.offsetToLineColumnConverter)
} }
setManagers () { setManagers () {
@ -63,7 +66,8 @@ export class Ethdebugger {
this.traceManager, this.traceManager,
this.solidityProxy, this.solidityProxy,
this.codeManager, this.codeManager,
{ ...this.opts, includeLocalVariables }) { ...this.opts, includeLocalVariables },
this.offsetToLineColumnConverter)
} }
resolveStep (index) { resolveStep (index) {
@ -71,7 +75,7 @@ export class Ethdebugger {
} }
setCompilationResult (compilationResult) { setCompilationResult (compilationResult) {
this.solidityProxy.reset((compilationResult && compilationResult.data) || {}) this.solidityProxy.reset((compilationResult && compilationResult.data) || {}, (compilationResult && compilationResult.source && compilationResult.source.sources) || {})
} }
async sourceLocationFromVMTraceIndex (address, stepIndex) { async sourceLocationFromVMTraceIndex (address, stepIndex) {

@ -14,6 +14,8 @@ export class Debugger {
breakPointManager breakPointManager
step_manager // eslint-disable-line camelcase step_manager // eslint-disable-line camelcase
vmDebuggerLogic vmDebuggerLogic
currentFile = -1
currentLine = -1
constructor (options) { constructor (options) {
this.event = new EventManager() this.event = new EventManager()
@ -26,7 +28,8 @@ export class Debugger {
this.debugger = new Ethdebugger({ this.debugger = new Ethdebugger({
web3: options.web3, web3: options.web3,
debugWithGeneratedSources: options.debugWithGeneratedSources, debugWithGeneratedSources: options.debugWithGeneratedSources,
compilationResult: this.compilationResult compilationResult: this.compilationResult,
offsetToLineColumnConverter: this.offsetToLineColumnConverter
}) })
const { traceManager, callTree, solidityProxy } = this.debugger const { traceManager, callTree, solidityProxy } = this.debugger
@ -73,35 +76,56 @@ export class Debugger {
const compilationResultForAddress = await this.compilationResult(address) const compilationResultForAddress = await this.compilationResult(address)
if (!compilationResultForAddress) { if (!compilationResultForAddress) {
this.event.trigger('newSourceLocation', [null]) this.event.trigger('newSourceLocation', [null])
this.currentFile = -1
this.currentLine = -1
this.vmDebuggerLogic.event.trigger('lineGasCostChanged', [null])
return return
} }
this.debugger.callTree.sourceLocationTracker.getValidSourceLocationFromVMTraceIndex(address, index, compilationResultForAddress.data.contracts).then(async (rawLocation) => { this.debugger.callTree.getValidSourceLocationFromVMTraceIndexFromCache(address, index, compilationResultForAddress.data.contracts).then(async (rawLocationAndOpcode) => {
if (compilationResultForAddress && compilationResultForAddress.data) { if (compilationResultForAddress && compilationResultForAddress.data) {
const rawLocation = rawLocationAndOpcode.sourceLocation
const stepDetail = rawLocationAndOpcode.stepDetail
const generatedSources = this.debugger.callTree.sourceLocationTracker.getGeneratedSourcesFromAddress(address) const generatedSources = this.debugger.callTree.sourceLocationTracker.getGeneratedSourcesFromAddress(address)
const astSources = Object.assign({}, compilationResultForAddress.data.sources)
const sources = Object.assign({}, compilationResultForAddress.source.sources) const lineColumnPos = rawLocationAndOpcode.lineColumnPos
if (generatedSources) {
for (const genSource of generatedSources) { let lineGasCostObj = null
astSources[genSource.name] = { id: genSource.id, ast: genSource.ast } try {
sources[genSource.name] = { content: genSource.contents } lineGasCostObj = await this.debugger.callTree.getGasCostPerLine(rawLocation.file, lineColumnPos.start.line)
} } catch (e) {
console.log(e)
} }
const lineColumnPos = await this.offsetToLineColumnConverter.offsetToLineColumn(rawLocation, rawLocation.file, sources, astSources) this.event.trigger('newSourceLocation', [lineColumnPos, rawLocation, generatedSources, address, stepDetail, (lineGasCostObj && lineGasCostObj.gasCost) || -1])
this.event.trigger('newSourceLocation', [lineColumnPos, rawLocation, generatedSources, address])
this.vmDebuggerLogic.event.trigger('sourceLocationChanged', [rawLocation]) this.vmDebuggerLogic.event.trigger('sourceLocationChanged', [rawLocation])
if (this.currentFile !== rawLocation.file || this.currentLine !== lineColumnPos.start.line) {
const instructionIndexes = lineGasCostObj.indexes.map((index) => { // translate from vmtrace index to instruction index
return this.debugger.codeManager.getInstructionIndex(address, index)
})
this.vmDebuggerLogic.event.trigger('lineGasCostChanged', [instructionIndexes, lineColumnPos.start.line ])
this.currentFile = rawLocation.file
this.currentLine = lineColumnPos.start.line
}
} else { } else {
this.event.trigger('newSourceLocation', [null]) this.event.trigger('newSourceLocation', [null])
this.vmDebuggerLogic.event.trigger('sourceLocationChanged', [null]) this.currentFile = -1
this.currentLine = -1
this.vmDebuggerLogic.event.trigger('lineGasCostChanged', [null])
} }
}).catch((_error) => { }).catch((_error) => {
this.event.trigger('newSourceLocation', [null]) this.event.trigger('newSourceLocation', [null])
this.vmDebuggerLogic.event.trigger('sourceLocationChanged', [null]) this.vmDebuggerLogic.event.trigger('sourceLocationChanged', [null])
this.currentFile = -1
this.currentLine = -1
this.vmDebuggerLogic.event.trigger('lineGasCostChanged', [null])
}) })
// }) // })
} catch (error) { } catch (error) {
this.event.trigger('newSourceLocation', [null]) this.event.trigger('newSourceLocation', [null])
this.vmDebuggerLogic.event.trigger('sourceLocationChanged', [null]) this.vmDebuggerLogic.event.trigger('sourceLocationChanged', [null])
this.currentFile = -1
this.currentLine = -1
this.vmDebuggerLogic.event.trigger('lineGasCostChanged', [null])
return console.log(error) return console.log(error)
} }
} }

@ -241,9 +241,7 @@ function getStructMembers (type, stateDefinitions, contractName, location) {
if (type.indexOf('.') === -1) { if (type.indexOf('.') === -1) {
type = contractName + '.' + type type = contractName + '.' + type
} }
if (!contractName) { contractName = type.split('.')[0]
contractName = type.split('.')[0]
}
const state = stateDefinitions[contractName] const state = stateDefinitions[contractName]
if (state) { if (state) {
for (const dec of state.stateDefinitions) { for (const dec of state.stateDefinitions) {

@ -7,6 +7,16 @@ import { parseType } from './decodeInfo'
import { isContractCreation, isCallInstruction, isCreateInstruction, isJumpDestInstruction } from '../trace/traceHelper' import { isContractCreation, isCallInstruction, isCreateInstruction, isJumpDestInstruction } from '../trace/traceHelper'
import { extractLocationFromAstVariable } from './types/util' import { extractLocationFromAstVariable } from './types/util'
export type StepDetail = {
depth: number,
gas: number | string,
gasCost: number,
memory: number[],
op: string,
pc: number,
stack: number[],
}
/** /**
* Tree representing internal jump into function. * Tree representing internal jump into function.
* Triggers `callTreeReady` event when tree is ready * Triggers `callTreeReady` event when tree is ready
@ -27,6 +37,15 @@ export class InternalCallTree {
functionDefinitionByFile functionDefinitionByFile
astWalker astWalker
reducedTrace reducedTrace
locationAndOpcodePerVMTraceIndex: {
[Key: number]: any
}
gasCostPerLine
offsetToLineColumnConverter
pendingConstructorExecutionAt: number
pendingConstructorId: number
pendingConstructor
constructorsStartExecution
/** /**
* constructor * constructor
@ -37,14 +56,16 @@ export class InternalCallTree {
* @param {Object} codeManager - code manager * @param {Object} codeManager - code manager
* @param {Object} opts - { includeLocalVariables, debugWithGeneratedSources } * @param {Object} opts - { includeLocalVariables, debugWithGeneratedSources }
*/ */
constructor (debuggerEvent, traceManager, solidityProxy, codeManager, opts) { constructor (debuggerEvent, traceManager, solidityProxy, codeManager, opts, offsetToLineColumnConverter?) {
this.includeLocalVariables = opts.includeLocalVariables this.includeLocalVariables = opts.includeLocalVariables
this.debugWithGeneratedSources = opts.debugWithGeneratedSources this.debugWithGeneratedSources = opts.debugWithGeneratedSources
this.event = new EventManager() this.event = new EventManager()
this.solidityProxy = solidityProxy this.solidityProxy = solidityProxy
this.traceManager = traceManager this.traceManager = traceManager
this.offsetToLineColumnConverter = offsetToLineColumnConverter
this.sourceLocationTracker = new SourceLocationTracker(codeManager, { debugWithGeneratedSources: opts.debugWithGeneratedSources }) this.sourceLocationTracker = new SourceLocationTracker(codeManager, { debugWithGeneratedSources: opts.debugWithGeneratedSources })
debuggerEvent.register('newTraceLoaded', (trace) => { debuggerEvent.register('newTraceLoaded', (trace) => {
const time = Date.now()
this.reset() this.reset()
if (!this.solidityProxy.loaded()) { if (!this.solidityProxy.loaded()) {
this.event.trigger('callTreeBuildFailed', ['compilation result not loaded. Cannot build internal call tree']) this.event.trigger('callTreeBuildFailed', ['compilation result not loaded. Cannot build internal call tree'])
@ -52,11 +73,17 @@ export class InternalCallTree {
// each recursive call to buildTree represent a new context (either call, delegatecall, internal function) // each recursive call to buildTree represent a new context (either call, delegatecall, internal function)
const calledAddress = traceManager.getCurrentCalledAddressAt(0) const calledAddress = traceManager.getCurrentCalledAddressAt(0)
const isCreation = isContractCreation(calledAddress) const isCreation = isContractCreation(calledAddress)
buildTree(this, 0, '', true, isCreation).then((result) => {
const scopeId = '1'
this.scopeStarts[0] = scopeId
this.scopes[scopeId] = { firstStep: 0, locals: {}, isCreation, gasCost: 0 }
buildTree(this, 0, scopeId, isCreation).then((result) => {
if (result.error) { if (result.error) {
this.event.trigger('callTreeBuildFailed', [result.error]) this.event.trigger('callTreeBuildFailed', [result.error])
} else { } else {
createReducedTrace(this, traceManager.trace.length - 1) createReducedTrace(this, traceManager.trace.length - 1)
console.log('call tree build lasts ', (Date.now() - time) / 1000)
this.event.trigger('callTreeReady', [this.scopes, this.scopeStarts]) this.event.trigger('callTreeReady', [this.scopes, this.scopeStarts])
} }
}, (reason) => { }, (reason) => {
@ -85,10 +112,16 @@ export class InternalCallTree {
this.functionCallStack = [] this.functionCallStack = []
this.functionDefinitionsByScope = {} this.functionDefinitionsByScope = {}
this.scopeStarts = {} this.scopeStarts = {}
this.gasCostPerLine = {}
this.variableDeclarationByFile = {} this.variableDeclarationByFile = {}
this.functionDefinitionByFile = {} this.functionDefinitionByFile = {}
this.astWalker = new AstWalker() this.astWalker = new AstWalker()
this.reducedTrace = [] this.reducedTrace = []
this.locationAndOpcodePerVMTraceIndex = {}
this.pendingConstructorExecutionAt = -1
this.pendingConstructorId = -1
this.constructorsStartExecution = {}
this.pendingConstructor = null
} }
/** /**
@ -123,6 +156,7 @@ export class InternalCallTree {
const scope = this.findScope(vmtraceIndex) const scope = this.findScope(vmtraceIndex)
if (!scope) return [] if (!scope) return []
let scopeId = this.scopeStarts[scope.firstStep] let scopeId = this.scopeStarts[scope.firstStep]
const scopeDetail = this.scopes[scopeId]
const functions = [] const functions = []
if (!scopeId) return functions if (!scopeId) return functions
let i = 0 let i = 0
@ -132,7 +166,7 @@ export class InternalCallTree {
if (i > 1000) throw new Error('retrieFunctionStack: recursion too deep') if (i > 1000) throw new Error('retrieFunctionStack: recursion too deep')
const functionDefinition = this.functionDefinitionsByScope[scopeId] const functionDefinition = this.functionDefinitionsByScope[scopeId]
if (functionDefinition !== undefined) { if (functionDefinition !== undefined) {
functions.push(functionDefinition) functions.push({ ...functionDefinition, ...scopeDetail })
} }
const parent = this.parentScope(scopeId) const parent = this.parentScope(scopeId)
if (!parent) break if (!parent) break
@ -141,32 +175,42 @@ export class InternalCallTree {
return functions return functions
} }
async extractSourceLocation (step) { async extractSourceLocation (step: number, address?: string) {
try { try {
const address = this.traceManager.getCurrentCalledAddressAt(step) if (!address) address = this.traceManager.getCurrentCalledAddressAt(step)
const location = await this.sourceLocationTracker.getSourceLocationFromVMTraceIndex(address, step, this.solidityProxy.contracts) return await this.sourceLocationTracker.getSourceLocationFromVMTraceIndex(address, step, this.solidityProxy.contracts)
return location
} catch (error) { } catch (error) {
throw new Error('InternalCallTree - Cannot retrieve sourcelocation for step ' + step + ' ' + error) throw new Error('InternalCallTree - Cannot retrieve sourcelocation for step ' + step + ' ' + error)
} }
} }
async extractValidSourceLocation (step) { async extractValidSourceLocation (step: number, address?: string) {
try { try {
const address = this.traceManager.getCurrentCalledAddressAt(step) if (!address) address = this.traceManager.getCurrentCalledAddressAt(step)
const location = await this.sourceLocationTracker.getValidSourceLocationFromVMTraceIndex(address, step, this.solidityProxy.contracts) return await this.sourceLocationTracker.getValidSourceLocationFromVMTraceIndex(address, step, this.solidityProxy.contracts)
return location
} catch (error) { } catch (error) {
throw new Error('InternalCallTree - Cannot retrieve valid sourcelocation for step ' + step + ' ' + error) throw new Error('InternalCallTree - Cannot retrieve valid sourcelocation for step ' + step + ' ' + error)
} }
} }
async getValidSourceLocationFromVMTraceIndexFromCache (address: string, step: number, contracts: any) {
return await this.sourceLocationTracker.getValidSourceLocationFromVMTraceIndexFromCache(address, step, contracts, this.locationAndOpcodePerVMTraceIndex)
}
async getGasCostPerLine(file: number, line: number) {
if (this.gasCostPerLine[file] && this.gasCostPerLine[file][line]) {
return this.gasCostPerLine[file][line]
}
throw new Error('Could not find gas cost per line')
}
} }
async function buildTree (tree, step, scopeId, isExternalCall, isCreation) { async function buildTree (tree, step, scopeId, isCreation, functionDefinition?, contractObj?, sourceLocation?, validSourceLocation?) {
let subScope = 1 let subScope = 1
tree.scopeStarts[step] = scopeId if (functionDefinition) {
tree.scopes[scopeId] = { firstStep: step, locals: {}, isCreation } await registerFunctionParameters(tree, functionDefinition, step, scopeId, contractObj, validSourceLocation)
}
function callDepthChange (step, trace) { function callDepthChange (step, trace) {
if (step + 1 < trace.length) { if (step + 1 < trace.length) {
return trace[step].depth !== trace[step + 1].depth return trace[step].depth !== trace[step + 1].depth
@ -183,30 +227,104 @@ async function buildTree (tree, step, scopeId, isExternalCall, isCreation) {
included.file === source.file) included.file === source.file)
} }
let currentSourceLocation = { start: -1, length: -1, file: -1 } let currentSourceLocation = sourceLocation || { start: -1, length: -1, file: -1, jump: '-' }
let previousSourceLocation = currentSourceLocation let previousSourceLocation = currentSourceLocation
let previousValidSourceLocation = validSourceLocation || currentSourceLocation
while (step < tree.traceManager.trace.length) { while (step < tree.traceManager.trace.length) {
let sourceLocation let sourceLocation
let newLocation = false let validSourceLocation
let address
try { try {
sourceLocation = await tree.extractSourceLocation(step) address = tree.traceManager.getCurrentCalledAddressAt(step)
sourceLocation = await tree.extractSourceLocation(step, address)
if (!includedSource(sourceLocation, currentSourceLocation)) { if (!includedSource(sourceLocation, currentSourceLocation)) {
tree.reducedTrace.push(step) tree.reducedTrace.push(step)
currentSourceLocation = sourceLocation currentSourceLocation = sourceLocation
newLocation = true
} }
const amountOfSources = tree.sourceLocationTracker.getTotalAmountOfSources(address, tree.solidityProxy.contracts)
if (tree.sourceLocationTracker.isInvalidSourceLocation(currentSourceLocation, amountOfSources)) { // file is -1 or greater than amount of sources
validSourceLocation = previousValidSourceLocation
} else
validSourceLocation = currentSourceLocation
} catch (e) { } catch (e) {
return { outStep: step, error: 'InternalCallTree - Error resolving source location. ' + step + ' ' + e } return { outStep: step, error: 'InternalCallTree - Error resolving source location. ' + step + ' ' + e }
} }
if (!sourceLocation) { if (!sourceLocation) {
return { outStep: step, error: 'InternalCallTree - No source Location. ' + step } return { outStep: step, error: 'InternalCallTree - No source Location. ' + step }
} }
const isCallInstrn = isCallInstruction(tree.traceManager.trace[step]) const stepDetail: StepDetail = tree.traceManager.trace[step]
const isCreateInstrn = isCreateInstruction(tree.traceManager.trace[step]) const nextStepDetail: StepDetail = tree.traceManager.trace[step + 1]
if (stepDetail && nextStepDetail) {
stepDetail.gasCost = parseInt(stepDetail.gas as string) - parseInt(nextStepDetail.gas as string)
}
// gas per line
let lineColumnPos
if (tree.offsetToLineColumnConverter) {
try {
const generatedSources = tree.sourceLocationTracker.getGeneratedSourcesFromAddress(address)
const astSources = Object.assign({}, tree.solidityProxy.sources)
const sources = Object.assign({}, tree.solidityProxy.sourcesCode)
if (generatedSources) {
for (const genSource of generatedSources) {
astSources[genSource.name] = { id: genSource.id, ast: genSource.ast }
sources[genSource.name] = { content: genSource.contents }
}
}
lineColumnPos = await tree.offsetToLineColumnConverter.offsetToLineColumn(validSourceLocation, validSourceLocation.file, sources, astSources)
if (!tree.gasCostPerLine[validSourceLocation.file]) tree.gasCostPerLine[validSourceLocation.file] = {}
if (!tree.gasCostPerLine[validSourceLocation.file][lineColumnPos.start.line]) {
tree.gasCostPerLine[validSourceLocation.file][lineColumnPos.start.line] = {
gasCost: 0,
indexes: []
}
}
tree.gasCostPerLine[validSourceLocation.file][lineColumnPos.start.line].gasCost += stepDetail.gasCost
tree.gasCostPerLine[validSourceLocation.file][lineColumnPos.start.line].indexes.push(step)
} catch (e) {
console.log(e)
}
}
tree.locationAndOpcodePerVMTraceIndex[step] = { sourceLocation, stepDetail, lineColumnPos }
tree.scopes[scopeId].gasCost += stepDetail.gasCost
const contractObj = await tree.solidityProxy.contractObjectAtAddress(address)
const generatedSources = getGeneratedSources(tree, scopeId, contractObj)
const functionDefinition = resolveFunctionDefinition(tree, sourceLocation, generatedSources)
const isInternalTxInstrn = isCallInstruction(stepDetail)
const isCreateInstrn = isCreateInstruction(stepDetail)
// we are checking if we are jumping in a new CALL or in an internal function // we are checking if we are jumping in a new CALL or in an internal function
if (isCallInstrn || sourceLocation.jump === 'i') {
const constructorExecutionStarts = tree.pendingConstructorExecutionAt > -1 && tree.pendingConstructorExecutionAt < validSourceLocation.start
if (functionDefinition && functionDefinition.kind === 'constructor' && tree.pendingConstructorExecutionAt === -1 && !tree.constructorsStartExecution[functionDefinition.id]) {
tree.pendingConstructorExecutionAt = validSourceLocation.start
tree.pendingConstructorId = functionDefinition.id
tree.pendingConstructor = functionDefinition
// from now on we'll be waiting for a change in the source location which will mark the beginning of the constructor execution.
// constructorsStartExecution allows to keep track on which constructor has already been executed.
}
const internalfunctionCall = functionDefinition && previousSourceLocation.jump === 'i'
if (constructorExecutionStarts || isInternalTxInstrn || internalfunctionCall) {
try { try {
const externalCallResult = await buildTree(tree, step + 1, scopeId === '' ? subScope.toString() : scopeId + '.' + subScope, isCallInstrn, isCreateInstrn) const newScopeId = scopeId === '' ? subScope.toString() : scopeId + '.' + subScope
tree.scopeStarts[step] = newScopeId
tree.scopes[newScopeId] = { firstStep: step, locals: {}, isCreation, gasCost: 0 }
// for the ctor we we are at the start of its trace, we have to replay this step in order to catch all the locals:
const nextStep = constructorExecutionStarts ? step : step + 1
if (constructorExecutionStarts) {
tree.constructorsStartExecution[tree.pendingConstructorId] = tree.pendingConstructorExecutionAt
tree.pendingConstructorExecutionAt = -1
tree.pendingConstructorId = -1
await registerFunctionParameters(tree, tree.pendingConstructor, step, newScopeId, contractObj, previousValidSourceLocation)
tree.pendingConstructor = null
}
const externalCallResult = await buildTree(tree, nextStep, newScopeId, isCreateInstrn, functionDefinition, contractObj, sourceLocation, validSourceLocation)
if (externalCallResult.error) { if (externalCallResult.error) {
return { outStep: step, error: 'InternalCallTree - ' + externalCallResult.error } return { outStep: step, error: 'InternalCallTree - ' + externalCallResult.error }
} else { } else {
@ -216,7 +334,7 @@ async function buildTree (tree, step, scopeId, isExternalCall, isCreation) {
} catch (e) { } catch (e) {
return { outStep: step, error: 'InternalCallTree - ' + e.message } return { outStep: step, error: 'InternalCallTree - ' + e.message }
} }
} else if ((isExternalCall && callDepthChange(step, tree.traceManager.trace)) || (!isExternalCall && sourceLocation.jump === 'o')) { } else if (callDepthChange(step, tree.traceManager.trace) || (sourceLocation.jump === 'o' && functionDefinition)) {
// if not, we might be returning from a CALL or internal function. This is what is checked here. // if not, we might be returning from a CALL or internal function. This is what is checked here.
tree.scopes[scopeId].lastStep = step tree.scopes[scopeId].lastStep = step
return { outStep: step + 1 } return { outStep: step + 1 }
@ -224,9 +342,10 @@ async function buildTree (tree, step, scopeId, isExternalCall, isCreation) {
// if not, we are in the current scope. // if not, we are in the current scope.
// We check in `includeVariableDeclaration` if there is a new local variable in scope for this specific `step` // We check in `includeVariableDeclaration` if there is a new local variable in scope for this specific `step`
if (tree.includeLocalVariables) { if (tree.includeLocalVariables) {
await includeVariableDeclaration(tree, step, sourceLocation, scopeId, newLocation, previousSourceLocation) await includeVariableDeclaration(tree, step, sourceLocation, scopeId, contractObj, generatedSources)
} }
previousSourceLocation = sourceLocation previousSourceLocation = sourceLocation
previousValidSourceLocation = validSourceLocation
step++ step++
} }
} }
@ -245,10 +364,33 @@ function getGeneratedSources (tree, scopeId, contractObj) {
return null return null
} }
async function includeVariableDeclaration (tree, step, sourceLocation, scopeId, newLocation, previousSourceLocation) { async function registerFunctionParameters (tree, functionDefinition, step, scopeId, contractObj, sourceLocation) {
const contractObj = await tree.solidityProxy.contractObjectAt(step) tree.functionCallStack.push(step)
const functionDefinitionAndInputs = { functionDefinition, inputs: [] }
// means: the previous location was a function definition && JUMPDEST
// => we are at the beginning of the function and input/output are setup
try {
const stack = tree.traceManager.getStackAt(step)
const states = tree.solidityProxy.extractStatesDefinitions()
if (functionDefinition.parameters) {
const inputs = functionDefinition.parameters
const outputs = functionDefinition.returnParameters
// input params
if (inputs && inputs.parameters) {
functionDefinitionAndInputs.inputs = addParams(inputs, tree, scopeId, states, contractObj, sourceLocation, stack.length, inputs.parameters.length, -1)
}
// output params
if (outputs) addParams(outputs, tree, scopeId, states, contractObj, sourceLocation, stack.length, 0, 1)
}
} catch (error) {
console.log(error)
}
tree.functionDefinitionsByScope[scopeId] = functionDefinitionAndInputs
}
async function includeVariableDeclaration (tree, step, sourceLocation, scopeId, contractObj, generatedSources) {
let states = null let states = null
const generatedSources = getGeneratedSources(tree, scopeId, contractObj)
const variableDeclarations = resolveVariableDeclaration(tree, sourceLocation, generatedSources) const variableDeclarations = resolveVariableDeclaration(tree, sourceLocation, generatedSources)
// using the vm trace step, the current source location and the ast, // using the vm trace step, the current source location and the ast,
// we check if the current vm trace step target a new ast node of type VariableDeclaration // we check if the current vm trace step target a new ast node of type VariableDeclaration
@ -278,49 +420,6 @@ async function includeVariableDeclaration (tree, step, sourceLocation, scopeId,
} }
} }
} }
// we check here if we are at the beginning inside a new function.
// if that is the case, we have to add to locals tree the inputs and output params
const functionDefinition = resolveFunctionDefinition(tree, previousSourceLocation, generatedSources)
if (!functionDefinition) return
const previousIsJumpDest2 = isJumpDestInstruction(tree.traceManager.trace[step - 2])
const previousIsJumpDest1 = isJumpDestInstruction(tree.traceManager.trace[step - 1])
const isConstructor = functionDefinition.kind === 'constructor'
if (newLocation && (previousIsJumpDest1 || previousIsJumpDest2 || isConstructor)) {
tree.functionCallStack.push(step)
const functionDefinitionAndInputs = { functionDefinition, inputs: [] }
// means: the previous location was a function definition && JUMPDEST
// => we are at the beginning of the function and input/output are setup
try {
const stack = tree.traceManager.getStackAt(step)
states = tree.solidityProxy.extractStatesDefinitions()
if (functionDefinition.parameters) {
const inputs = functionDefinition.parameters
const outputs = functionDefinition.returnParameters
// for (const element of functionDefinition.parameters) {
// if (element.nodeType === 'ParameterList') {
// if (!inputs) inputs = element
// else {
// outputs = element
// break
// }
// }
// }
// input params
if (inputs && inputs.parameters) {
functionDefinitionAndInputs.inputs = addParams(inputs, tree, scopeId, states, contractObj, previousSourceLocation, stack.length, inputs.parameters.length, -1)
}
// output params
if (outputs) addParams(outputs, tree, scopeId, states, contractObj, previousSourceLocation, stack.length, 0, 1)
}
} catch (error) {
console.log(error)
}
tree.functionDefinitionsByScope[scopeId] = functionDefinitionAndInputs
}
} }
// this extract all the variable declaration for a given ast and file // this extract all the variable declaration for a given ast and file
@ -388,7 +487,8 @@ function addParams (parameterList, tree, scopeId, states, contractObj, sourceLoc
type: parseType(param.typeDescriptions.typeString, states, contractName, location), type: parseType(param.typeDescriptions.typeString, states, contractName, location),
stackDepth: stackDepth, stackDepth: stackDepth,
sourceLocation: sourceLocation, sourceLocation: sourceLocation,
abi: contractObj.contract.abi abi: contractObj.contract.abi,
isParameter: true
} }
params.push(attributesName) params.push(attributesName)
} }

@ -11,7 +11,7 @@ export async function solidityLocals (vmtraceIndex, internalTreeCall, stack, mem
let anonymousIncr = 1 let anonymousIncr = 1
for (const local in scope.locals) { for (const local in scope.locals) {
const variable = scope.locals[local] const variable = scope.locals[local]
if (variable.stackDepth < stack.length && variable.sourceLocation.start <= currentSourceLocation.start) { if (variable.stackDepth < stack.length && (variable.sourceLocation.start <= currentSourceLocation.start || variable.isParameter)) {
let name = variable.name let name = variable.name
if (name.indexOf('$') !== -1) { if (name.indexOf('$') !== -1) {
name = '<' + anonymousIncr + '>' name = '<' + anonymousIncr + '>'
@ -21,7 +21,7 @@ export async function solidityLocals (vmtraceIndex, internalTreeCall, stack, mem
locals[name] = await variable.type.decodeFromStack(variable.stackDepth, stack, memory, storageResolver, calldata, cursor, variable) locals[name] = await variable.type.decodeFromStack(variable.stackDepth, stack, memory, storageResolver, calldata, cursor, variable)
} catch (e) { } catch (e) {
console.log(e) console.log(e)
locals[name] = { error: '<decoding failed - ' + e.message + '>' } locals[name] = { error: '<decoding failed - ' + e.message + '>', type: variable && variable.type && variable.type.typeName || 'unknown' }
} }
} }
} }

@ -10,6 +10,8 @@ export class SolidityProxy {
getCode getCode
sources sources
contracts contracts
compilationResult
sourcesCode
constructor ({ getCurrentCalledAddressAt, getCode }) { constructor ({ getCurrentCalledAddressAt, getCode }) {
this.cache = new Cache() this.cache = new Cache()
@ -23,9 +25,10 @@ export class SolidityProxy {
* *
* @param {Object} compilationResult - result os a compilatiion (diectly returned by the compiler) * @param {Object} compilationResult - result os a compilatiion (diectly returned by the compiler)
*/ */
reset (compilationResult) { reset (compilationResult, sources?) {
this.sources = compilationResult.sources this.sources = compilationResult.sources // ast
this.contracts = compilationResult.contracts this.contracts = compilationResult.contracts
if (sources) this.sourcesCode = sources
this.cache.reset() this.cache.reset()
} }
@ -44,8 +47,18 @@ export class SolidityProxy {
* @param {Int} vmTraceIndex - index in the vm trave where to resolve the executed contract name * @param {Int} vmTraceIndex - index in the vm trave where to resolve the executed contract name
* @param {Function} cb - callback returns (error, contractName) * @param {Function} cb - callback returns (error, contractName)
*/ */
async contractObjectAt (vmTraceIndex) { async contractObjectAt (vmTraceIndex: number) {
const address = this.getCurrentCalledAddressAt(vmTraceIndex) const address = this.getCurrentCalledAddressAt(vmTraceIndex)
return this.contractObjectAtAddress(address)
}
/**
* retrieve the compiled contract name at the @arg vmTraceIndex (cached)
*
* @param {Int} vmTraceIndex - index in the vm trave where to resolve the executed contract name
* @param {Function} cb - callback returns (error, contractName)
*/
async contractObjectAtAddress (address: string) {
if (this.cache.contractObjectByAddress[address]) { if (this.cache.contractObjectByAddress[address]) {
return this.cache.contractObjectByAddress[address] return this.cache.contractObjectByAddress[address]
} }

@ -25,7 +25,7 @@ export class StringType extends DynamicByteArray {
return await super.decodeFromStack(stackDepth, stack, memory, storageResolver, calldata, cursor, variableDetails) return await super.decodeFromStack(stackDepth, stack, memory, storageResolver, calldata, cursor, variableDetails)
} catch (e) { } catch (e) {
console.log(e) console.log(e)
return { error: '<decoding failed - ' + e.message + '>' } return { error: '<decoding failed - ' + e.message + '>', type: this.typeName }
} }
} }

@ -4,9 +4,11 @@ import { getLinebreakPositions, convertOffsetToLineColumn } from './sourceMappin
export class OffsetToColumnConverter { export class OffsetToColumnConverter {
lineBreakPositionsByContent lineBreakPositionsByContent
sourceMappingDecoder sourceMappingDecoder
offsetConvertion
constructor (compilerEvent) { constructor (compilerEvent) {
this.lineBreakPositionsByContent = {} this.lineBreakPositionsByContent = {}
this.offsetConvertion = {}
if (compilerEvent) { if (compilerEvent) {
compilerEvent.register('compilationFinished', (success, data, source, input, version) => { compilerEvent.register('compilationFinished', (success, data, source, input, version) => {
this.clear() this.clear()
@ -26,10 +28,18 @@ export class OffsetToColumnConverter {
} }
} }
} }
return convertOffsetToLineColumn(rawLocation, this.lineBreakPositionsByContent[file]) const token = `${rawLocation.start}:${rawLocation.length}:${file}`
if (this.offsetConvertion[token]) {
return this.offsetConvertion[token]
} else {
const convertion = convertOffsetToLineColumn(rawLocation, this.lineBreakPositionsByContent[file])
this.offsetConvertion[token] = convertion
return convertion
}
} }
clear () { clear () {
this.lineBreakPositionsByContent = {} this.lineBreakPositionsByContent = {}
this.offsetConvertion = {}
} }
} }

@ -87,13 +87,34 @@ export class SourceLocationTracker {
(map.file > amountOfSources - 1) this indicates the current file index exceed the total number of files. (map.file > amountOfSources - 1) this indicates the current file index exceed the total number of files.
this happens when generated sources should not be considered. this happens when generated sources should not be considered.
*/ */
while (vmtraceStepIndex >= 0 && (map.file === -1 || map.file > amountOfSources - 1)) { while (vmtraceStepIndex >= 0 && this.isInvalidSourceLocation(map, amountOfSources)) {
map = await this.getSourceLocationFromVMTraceIndex(address, vmtraceStepIndex, contracts) map = await this.getSourceLocationFromVMTraceIndex(address, vmtraceStepIndex, contracts)
vmtraceStepIndex = vmtraceStepIndex - 1 vmtraceStepIndex = vmtraceStepIndex - 1
} }
return map return map
} }
isInvalidSourceLocation (sourceLocation, amountOfSources) {
return sourceLocation.file === -1 || sourceLocation.file > amountOfSources - 1
}
async getValidSourceLocationFromVMTraceIndexFromCache (address: string, vmtraceStepIndex: number, contracts: any, cache: Map<number, any>) {
const amountOfSources = this.getTotalAmountOfSources(address, contracts)
let map: any = { file: -1 }
/*
(map.file === -1) this indicates that it isn't associated with a known source code
(map.file > amountOfSources - 1) this indicates the current file index exceed the total number of files.
this happens when generated sources should not be considered.
*/
const originStep = cache[vmtraceStepIndex]
while (vmtraceStepIndex >= 0 && (map.file === -1 || map.file > amountOfSources - 1)) {
map = cache[vmtraceStepIndex].sourceLocation
vmtraceStepIndex = vmtraceStepIndex - 1
originStep.sourceLocation = map
}
return originStep
}
clearCache () { clearCache () {
this.sourceMapByAddress = {} this.sourceMapByAddress = {}
} }

@ -23,7 +23,7 @@ tape('solidity', function (t) {
async function test (st, privateKey) { async function test (st, privateKey) {
var output = compiler.compile(compilerInput(intLocal.contract)) var output = compiler.compile(compilerInput(intLocal.contract))
output = JSON.parse(output) output = JSON.parse(output)
await intLocalTest(st, privateKey, output.contracts['test.sol']['intLocal'].evm.bytecode.object, output) await intLocalTest(st, privateKey, output.contracts['test.sol']['intLocal'].evm.bytecode.object, output, intLocal.contract)
output = compiler.compile(compilerInput(miscLocal.contract)) output = compiler.compile(compilerInput(miscLocal.contract))
output = JSON.parse(output) output = JSON.parse(output)
await miscLocalTest(st, privateKey, output.contracts['test.sol']['miscLocal'].evm.bytecode.object, output) await miscLocalTest(st, privateKey, output.contracts['test.sol']['miscLocal'].evm.bytecode.object, output)

@ -7,9 +7,10 @@ import { contractCreationToken } from '../../../src/trace/traceHelper'
import { SolidityProxy } from '../../../src/solidity-decoder/solidityProxy' import { SolidityProxy } from '../../../src/solidity-decoder/solidityProxy'
import { InternalCallTree } from '../../../src/solidity-decoder/internalCallTree' import { InternalCallTree } from '../../../src/solidity-decoder/internalCallTree'
import { EventManager } from '../../../src/eventManager' import { EventManager } from '../../../src/eventManager'
import * as sourceMappingDecoder from '../../../src/source/sourceMappingDecoder'
import * as helper from './helper' import * as helper from './helper'
module.exports = function (st, privateKey, contractBytecode, compilationResult) { module.exports = function (st, privateKey, contractBytecode, compilationResult, contractCode) {
return new Promise(async (resolve) => { return new Promise(async (resolve) => {
let web3 = await (vmCall as any).getWeb3(); let web3 = await (vmCall as any).getWeb3();
(vmCall as any).sendTx(web3, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, hash) { (vmCall as any).sendTx(web3, { nonce: 0, privateKey: privateKey }, null, 0, contractBytecode, function (error, hash) {
@ -27,22 +28,37 @@ module.exports = function (st, privateKey, contractBytecode, compilationResult)
var solidityProxy = new SolidityProxy({ getCurrentCalledAddressAt: traceManager.getCurrentCalledAddressAt.bind(traceManager), getCode: codeManager.getCode.bind(codeManager) }) var solidityProxy = new SolidityProxy({ getCurrentCalledAddressAt: traceManager.getCurrentCalledAddressAt.bind(traceManager), getCode: codeManager.getCode.bind(codeManager) })
solidityProxy.reset(compilationResult) solidityProxy.reset(compilationResult)
var debuggerEvent = new EventManager() var debuggerEvent = new EventManager()
var callTree = new InternalCallTree(debuggerEvent, traceManager, solidityProxy, codeManager, { includeLocalVariables: true }) const offsetToLineColumnConverter = {
offsetToLineColumn: (rawLocation) => {
return new Promise((resolve) => {
const lineBreaks = sourceMappingDecoder.getLinebreakPositions(contractCode)
resolve(sourceMappingDecoder.convertOffsetToLineColumn(rawLocation, lineBreaks))
})
}
}
var callTree = new InternalCallTree(debuggerEvent, traceManager, solidityProxy, codeManager, { includeLocalVariables: true }, offsetToLineColumnConverter)
callTree.event.register('callTreeBuildFailed', (error) => { callTree.event.register('callTreeBuildFailed', (error) => {
st.fail(error) st.fail(error)
}) })
callTree.event.register('callTreeNotReady', (reason) => { callTree.event.register('callTreeNotReady', (reason) => {
st.fail(reason) st.fail(reason)
}) })
callTree.event.register('callTreeReady', (scopes, scopeStarts) => { callTree.event.register('callTreeReady', async (scopes, scopeStarts) => {
try { try {
let functions1 = callTree.retrieveFunctionsStack(102)
let functions2 = callTree.retrieveFunctionsStack(115) // test gas cost per line
st.equals((await callTree.getGasCostPerLine(0, 16)).gasCost, 11)
st.equals((await callTree.getGasCostPerLine(0, 32)).gasCost, 84)
let functions1 = callTree.retrieveFunctionsStack(103)
let functions2 = callTree.retrieveFunctionsStack(116)
let functions3 = callTree.retrieveFunctionsStack(13) let functions3 = callTree.retrieveFunctionsStack(13)
st.equals(functions1.length, 1) st.equals(functions1.length, 2)
st.equals(functions2.length, 2) st.equals(functions2.length, 3)
st.equals(functions3.length, 0) st.equals(functions3.length, 1)
st.equal(functions1[0].gasCost, 54)
st.equals(Object.keys(functions1[0])[0], 'functionDefinition') st.equals(Object.keys(functions1[0])[0], 'functionDefinition')
st.equals(Object.keys(functions1[0])[1], 'inputs') st.equals(Object.keys(functions1[0])[1], 'inputs')
@ -57,34 +73,34 @@ module.exports = function (st, privateKey, contractBytecode, compilationResult)
st.equals(functions1[0].functionDefinition.name, 'level11') st.equals(functions1[0].functionDefinition.name, 'level11')
st.equals(functions2[0].functionDefinition.name, 'level12') st.equals(functions2[0].functionDefinition.name, 'level12')
st.equals(functions2[1].functionDefinition.name, 'level11') st.equals(functions2[1].functionDefinition.name, 'level11')
st.equals(scopeStarts[0], '') st.equals(scopeStarts[0], '1')
st.equals(scopeStarts[13], '1') st.equals(scopeStarts[10], '1.1')
st.equals(scopeStarts[102], '2') st.equals(scopeStarts[102], '1.1.1')
st.equals(scopeStarts[115], '2.1') st.equals(scopeStarts[115], '1.1.1.1')
st.equals(scopeStarts[136], '3') st.equals(scopeStarts[136], '1.1.2')
st.equals(scopeStarts[153], '4') st.equals(scopeStarts[153], '1.1.3')
st.equals(scopeStarts[166], '4.1') st.equals(scopeStarts[166], '1.1.3.1')
st.equals(scopes[''].locals['ui8'].type.typeName, 'uint8') st.equals(scopes['1.1'].locals['ui8'].type.typeName, 'uint8')
st.equals(scopes[''].locals['ui16'].type.typeName, 'uint16') st.equals(scopes['1.1'].locals['ui16'].type.typeName, 'uint16')
st.equals(scopes[''].locals['ui32'].type.typeName, 'uint32') st.equals(scopes['1.1'].locals['ui32'].type.typeName, 'uint32')
st.equals(scopes[''].locals['ui64'].type.typeName, 'uint64') st.equals(scopes['1.1'].locals['ui64'].type.typeName, 'uint64')
st.equals(scopes[''].locals['ui128'].type.typeName, 'uint128') st.equals(scopes['1.1'].locals['ui128'].type.typeName, 'uint128')
st.equals(scopes[''].locals['ui256'].type.typeName, 'uint256') st.equals(scopes['1.1'].locals['ui256'].type.typeName, 'uint256')
st.equals(scopes[''].locals['ui'].type.typeName, 'uint256') st.equals(scopes['1.1'].locals['ui'].type.typeName, 'uint256')
st.equals(scopes[''].locals['i8'].type.typeName, 'int8') st.equals(scopes['1.1'].locals['i8'].type.typeName, 'int8')
st.equals(scopes[''].locals['i16'].type.typeName, 'int16') st.equals(scopes['1.1'].locals['i16'].type.typeName, 'int16')
st.equals(scopes[''].locals['i32'].type.typeName, 'int32') st.equals(scopes['1.1'].locals['i32'].type.typeName, 'int32')
st.equals(scopes[''].locals['i64'].type.typeName, 'int64') st.equals(scopes['1.1'].locals['i64'].type.typeName, 'int64')
st.equals(scopes[''].locals['i128'].type.typeName, 'int128') st.equals(scopes['1.1'].locals['i128'].type.typeName, 'int128')
st.equals(scopes[''].locals['i256'].type.typeName, 'int256') st.equals(scopes['1.1'].locals['i256'].type.typeName, 'int256')
st.equals(scopes[''].locals['i'].type.typeName, 'int256') st.equals(scopes['1.1'].locals['i'].type.typeName, 'int256')
st.equals(scopes[''].locals['ishrink'].type.typeName, 'int32') st.equals(scopes['1.1'].locals['ishrink'].type.typeName, 'int32')
st.equals(scopes['2'].locals['ui8'].type.typeName, 'uint8') st.equals(scopes['1.1.1'].locals['ui8'].type.typeName, 'uint8')
st.equals(scopes['2.1'].locals['ui81'].type.typeName, 'uint8') st.equals(scopes['1.1.1.1'].locals['ui81'].type.typeName, 'uint8')
st.equals(scopes['3'].locals['ui81'].type.typeName, 'uint8') st.equals(scopes['1.1.2'].locals['ui81'].type.typeName, 'uint8')
st.equals(scopes['4'].locals['ui8'].type.typeName, 'uint8') st.equals(scopes['1.1.3'].locals['ui8'].type.typeName, 'uint8')
st.equals(scopes['4.1'].locals['ui81'].type.typeName, 'uint8') st.equals(scopes['1.1.3.1'].locals['ui81'].type.typeName, 'uint8')
} catch (e) { } catch (e) {
st.fail(e.message) st.fail(e.message)
} }

@ -1,6 +1,11 @@
import React, { useContext, useEffect, useState } from 'react' import React, { useContext, useEffect, useState } from 'react'
import { AppContext } from '../../context/context' import { AppContext } from '../../context/context'
import { useDialogDispatchers } from '../../context/provider' import { useDialogDispatchers } from '../../context/provider'
declare global {
interface Window {
_paq: any
}
}
const _paq = window._paq = window._paq || [] const _paq = window._paq = window._paq || []
const MatomoDialog = (props) => { const MatomoDialog = (props) => {

@ -121,7 +121,7 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
}) })
}) })
debuggerInstance.event.register('newSourceLocation', async (lineColumnPos, rawLocation, generatedSources, address) => { debuggerInstance.event.register('newSourceLocation', async (lineColumnPos, rawLocation, generatedSources, address, stepDetail, lineGasCost) => {
if (!lineColumnPos) { if (!lineColumnPos) {
await debuggerModule.discardHighlight() await debuggerModule.discardHighlight()
setState(prevState => { setState(prevState => {
@ -158,7 +158,7 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
return { ...prevState, sourceLocationStatus: '' } return { ...prevState, sourceLocationStatus: '' }
}) })
await debuggerModule.discardHighlight() await debuggerModule.discardHighlight()
await debuggerModule.highlight(lineColumnPos, path) await debuggerModule.highlight(lineColumnPos, path, rawLocation, stepDetail, lineGasCost)
} }
} }
}) })
@ -266,13 +266,14 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
console.log(e.message) console.log(e.message)
} }
const localCache = {}
const debuggerInstance = new Debugger({ const debuggerInstance = new Debugger({
web3, web3,
offsetToLineColumnConverter: debuggerModule.offsetToLineColumnConverter, offsetToLineColumnConverter: debuggerModule.offsetToLineColumnConverter,
compilationResult: async (address) => { compilationResult: async (address) => {
try { try {
const ret = await debuggerModule.fetchContractAndCompile(address, currentReceipt) if (!localCache[address]) localCache[address] = await debuggerModule.fetchContractAndCompile(address, currentReceipt)
return ret return localCache[address]
} catch (e) { } catch (e) {
// debuggerModule.showMessage('Debugging error', 'Unable to fetch a transaction.') // debuggerModule.showMessage('Debugging error', 'Unable to fetch a transaction.')
console.error(e) console.error(e)
@ -395,8 +396,8 @@ export const DebuggerUI = (props: DebuggerUIProps) => {
{ state.debugging && <StepManager stepManager={ stepManager } /> } { state.debugging && <StepManager stepManager={ stepManager } /> }
</div> </div>
<div className="debuggerPanels" ref={panelsRef}> <div className="debuggerPanels" ref={panelsRef}>
{ state.debugging && <VmDebuggerHead vmDebugger={ vmDebugger } /> } { state.debugging && <VmDebuggerHead debugging={state.debugging} vmDebugger={ vmDebugger } /> }
{ state.debugging && <VmDebugger vmDebugger={ vmDebugger } currentBlock={ state.currentBlock } currentReceipt={ state.currentReceipt } currentTransaction={ state.currentTransaction } /> } { state.debugging && <VmDebugger debugging={state.debugging} vmDebugger={ vmDebugger } currentBlock={ state.currentBlock } currentReceipt={ state.currentReceipt } currentTransaction={ state.currentTransaction } /> }
</div> </div>
</div> </div>
) )

@ -44,7 +44,7 @@ export interface IDebuggerApi {
onEditorContentChanged: (listener: onEditorContentChanged) => void onEditorContentChanged: (listener: onEditorContentChanged) => void
onEnvChanged: (listener: onEnvChangedListener) => void onEnvChanged: (listener: onEnvChangedListener) => void
discardHighlight: () => Promise<void> discardHighlight: () => Promise<void>
highlight: (lineColumnPos: LineColumnLocation, path: string) => Promise<void> highlight: (lineColumnPos: LineColumnLocation, path: string, rawLocation: any, stepDetail: any, highlight: any) => Promise<void>
fetchContractAndCompile: (address: string, currentReceipt: TransactionReceipt) => Promise<CompilerAbstract> fetchContractAndCompile: (address: string, currentReceipt: TransactionReceipt) => Promise<CompilerAbstract>
getFile: (path: string) => Promise<string> getFile: (path: string) => Promise<string>
setFile: (path: string, content: string) => Promise<void> setFile: (path: string, content: string) => Promise<void>

@ -1,3 +1,4 @@
import { stateDecoder } from 'dist/libs/remix-debug/src/solidity-decoder'
import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint-disable-line import React, { useState, useRef, useEffect, useReducer } from 'react' // eslint-disable-line
import { initialState, reducer } from '../../reducers/assembly-items' import { initialState, reducer } from '../../reducers/assembly-items'
import './styles/assembly-items.css' import './styles/assembly-items.css'
@ -9,6 +10,7 @@ export const AssemblyItems = ({ registerEvent }) => {
const [nextSelectedItems, setNextSelectedItems] = useState([1]) const [nextSelectedItems, setNextSelectedItems] = useState([1])
const [returnInstructionIndexes, setReturnInstructionIndexes] = useState([]) const [returnInstructionIndexes, setReturnInstructionIndexes] = useState([])
const [outOfGasInstructionIndexes, setOutOfGasInstructionIndexes] = useState([]) const [outOfGasInstructionIndexes, setOutOfGasInstructionIndexes] = useState([])
const [opcodeTooltipText, setOpcodeTooltipText] = useState('')
const refs = useRef({}) const refs = useRef({})
const asmItemsRef = useRef(null) const asmItemsRef = useRef(null)
@ -16,6 +18,10 @@ export const AssemblyItems = ({ registerEvent }) => {
registerEvent && registerEvent('codeManagerChanged', (code, address, index, nextIndexes, returnInstructionIndexes, outOfGasInstructionIndexes) => { registerEvent && registerEvent('codeManagerChanged', (code, address, index, nextIndexes, returnInstructionIndexes, outOfGasInstructionIndexes) => {
dispatch({ type: 'FETCH_OPCODES_SUCCESS', payload: { code, address, index, nextIndexes, returnInstructionIndexes, outOfGasInstructionIndexes } }) dispatch({ type: 'FETCH_OPCODES_SUCCESS', payload: { code, address, index, nextIndexes, returnInstructionIndexes, outOfGasInstructionIndexes } })
}) })
registerEvent && registerEvent('lineGasCostChanged', (instructionsIndexes: number[], line: []) => {
dispatch({ type: 'FETCH_INDEXES_FOR_NEW_LINE', payload: { currentLineIndexes: instructionsIndexes || [], line } })
})
}, []) }, [])
useEffect(() => { useEffect(() => {
@ -129,7 +135,9 @@ export const AssemblyItems = ({ registerEvent }) => {
<div className="pl-2 my-1 small instructions" data-id="asmitems" id='asmitems' ref={asmItemsRef}> <div className="pl-2 my-1 small instructions" data-id="asmitems" id='asmitems' ref={asmItemsRef}>
{ {
assemblyItems.display.map((item, i) => { assemblyItems.display.map((item, i) => {
return <div className="px-1" key={i} ref={ref => { refs.current[i] = ref }}><span>{item}</span></div> return <div className="px-1" key={i} ref={ref => { refs.current[i] = ref }}>
<span>{item}</span>{assemblyItems.currentLineIndexes.includes(i) ? <span><i><b> - LINE {assemblyItems.line + 1}</b></i></span> : ' - '}
</div>
}) })
} }
</div> </div>

@ -5,7 +5,7 @@ import StepDetail from './step-detail' // eslint-disable-line
import SolidityState from './solidity-state' // eslint-disable-line import SolidityState from './solidity-state' // eslint-disable-line
import SolidityLocals from './solidity-locals' // eslint-disable-line import SolidityLocals from './solidity-locals' // eslint-disable-line
export const VmDebuggerHead = ({ vmDebugger: { registerEvent, triggerEvent } }) => { export const VmDebuggerHead = ({ vmDebugger: { registerEvent, triggerEvent }, debugging }) => {
const [functionPanel, setFunctionPanel] = useState(null) const [functionPanel, setFunctionPanel] = useState(null)
const [stepDetail, setStepDetail] = useState({ const [stepDetail, setStepDetail] = useState({
'vm trace step': '-', 'vm trace step': '-',
@ -31,7 +31,7 @@ export const VmDebuggerHead = ({ vmDebugger: { registerEvent, triggerEvent } })
const functions = [] const functions = []
for (const func of stack) { for (const func of stack) {
functions.push(func.functionDefinition.name + '(' + func.inputs.join(', ') + ')') functions.push((func.functionDefinition.name || func.functionDefinition.kind) + '(' + func.inputs.join(', ') + ')' + ' - ' + func.gasCost + ' gas')
} }
setFunctionPanel(() => functions) setFunctionPanel(() => functions)
}) })
@ -95,7 +95,7 @@ export const VmDebuggerHead = ({ vmDebugger: { registerEvent, triggerEvent } })
return { ...solidityLocals, message } return { ...solidityLocals, message }
}) })
}) })
}, [registerEvent]) }, [debugging])
return ( return (
<div id='vmheadView' className="mt-1 px-2 d-flex"> <div id='vmheadView' className="mt-1 px-2 d-flex">

@ -8,7 +8,7 @@ import ReturnValuesPanel from './dropdown-panel' // eslint-disable-line
import FullStoragesChangesPanel from './full-storages-changes' // eslint-disable-line import FullStoragesChangesPanel from './full-storages-changes' // eslint-disable-line
import GlobalVariables from './global-variables' // eslint-disable-line import GlobalVariables from './global-variables' // eslint-disable-line
export const VmDebugger = ({ vmDebugger: { registerEvent }, currentBlock, currentReceipt, currentTransaction }) => { export const VmDebugger = ({ vmDebugger: { registerEvent }, currentBlock, currentReceipt, currentTransaction, debugging }) => {
const [calldataPanel, setCalldataPanel] = useState(null) const [calldataPanel, setCalldataPanel] = useState(null)
const [memoryPanel, setMemoryPanel] = useState(null) const [memoryPanel, setMemoryPanel] = useState(null)
const [callStackPanel, setCallStackPanel] = useState(null) const [callStackPanel, setCallStackPanel] = useState(null)
@ -49,7 +49,7 @@ export const VmDebugger = ({ vmDebugger: { registerEvent }, currentBlock, curren
registerEvent && registerEvent('traceStorageUpdate', (calldata) => { registerEvent && registerEvent('traceStorageUpdate', (calldata) => {
setFullStoragesChangesPanel(() => calldata) setFullStoragesChangesPanel(() => calldata)
}) })
}, [registerEvent]) }, [debugging])
return ( return (
<div id='vmdebugger' className="d-flex"> <div id='vmdebugger' className="d-flex">

@ -13,6 +13,7 @@ export const initialState = {
}, },
display: [], display: [],
index: 0, index: 0,
initialIndex: 0,
nextIndexes: [-1], nextIndexes: [-1],
returnInstructionIndexes: [], returnInstructionIndexes: [],
outOfGasInstructionIndexes: [], outOfGasInstructionIndexes: [],
@ -20,7 +21,10 @@ export const initialState = {
bottom: 0, bottom: 0,
isRequesting: false, isRequesting: false,
isSuccessful: false, isSuccessful: false,
hasError: null hasError: null,
absoluteCurrentLineIndexes: [],
currentLineIndexes: [],
line: -1
} }
const reducedOpcode = (opCodes, payload) => { const reducedOpcode = (opCodes, payload) => {
@ -31,6 +35,7 @@ const reducedOpcode = (opCodes, payload) => {
return { return {
index: opCodes.index - bottom, index: opCodes.index - bottom,
nextIndexes: opCodes.nextIndexes.map(index => index - bottom), nextIndexes: opCodes.nextIndexes.map(index => index - bottom),
currentLineIndexes: (opCodes.absoluteCurrentLineIndexes && opCodes.absoluteCurrentLineIndexes.map(index => index - bottom)) || [],
display: opCodes.code.slice(bottom, top), display: opCodes.code.slice(bottom, top),
returnInstructionIndexes: payload.returnInstructionIndexes.map((index) => index.instructionIndex - bottom), returnInstructionIndexes: payload.returnInstructionIndexes.map((index) => index.instructionIndex - bottom),
outOfGasInstructionIndexes: payload.outOfGasInstructionIndexes.map((index) => index.instructionIndex - bottom) outOfGasInstructionIndexes: payload.outOfGasInstructionIndexes.map((index) => index.instructionIndex - bottom)
@ -49,20 +54,23 @@ export const reducer = (state = initialState, action: Action) => {
} }
case 'FETCH_OPCODES_SUCCESS': { case 'FETCH_OPCODES_SUCCESS': {
const opCodes = action.payload.address === state.opCodes.address ? { const opCodes = action.payload.address === state.opCodes.address ? {
...state.opCodes, index: action.payload.index, nextIndexes: action.payload.nextIndexes ...state.opCodes, index: action.payload.index, nextIndexes: action.payload.nextIndexes, absoluteCurrentLineIndexes: state.absoluteCurrentLineIndexes
} : deepEqual(action.payload.code, state.opCodes.code) ? state.opCodes : action.payload } : deepEqual(action.payload.code, state.opCodes.code) ? state.opCodes : action.payload
const reduced = reducedOpcode(opCodes, action.payload) const reduced = reducedOpcode(opCodes, action.payload)
return { return {
...state,
opCodes, opCodes,
display: reduced.display, display: reduced.display,
initialIndex: action.payload.index,
index: reduced.index, index: reduced.index,
nextIndexes: reduced.nextIndexes, nextIndexes: reduced.nextIndexes,
isRequesting: false, isRequesting: false,
isSuccessful: true, isSuccessful: true,
hasError: null, hasError: null,
returnInstructionIndexes: reduced.returnInstructionIndexes, returnInstructionIndexes: reduced.returnInstructionIndexes,
outOfGasInstructionIndexes: reduced.outOfGasInstructionIndexes outOfGasInstructionIndexes: reduced.outOfGasInstructionIndexes,
currentLineIndexes: reduced.currentLineIndexes
} }
} }
case 'FETCH_OPCODES_ERROR': { case 'FETCH_OPCODES_ERROR': {
@ -73,6 +81,16 @@ export const reducer = (state = initialState, action: Action) => {
hasError: action.payload hasError: action.payload
} }
} }
case 'FETCH_INDEXES_FOR_NEW_LINE': {
let bottom = state.initialIndex - 10
bottom = bottom < 0 ? 0 : bottom
return {
...state,
absoluteCurrentLineIndexes: action.payload.currentLineIndexes,
currentLineIndexes: action.payload.currentLineIndexes.map(index => index - bottom),
line: action.payload.line
}
}
default: default:
throw new Error() throw new Error()
} }

Loading…
Cancel
Save