Merge pull request #165 from ethereum/jumpOut

add jump out button
pull/7/head
chriseth 8 years ago committed by GitHub
commit f85d6504f7
  1. 1
      package.json
  2. 43
      src/helpers/util.js
  3. 11
      src/trace/traceAnalyser.js
  4. 40
      src/trace/traceCache.js
  5. 46
      src/trace/traceManager.js
  6. 8
      src/trace/traceRetriever.js
  7. 24
      src/trace/traceStepManager.js
  8. 64
      src/ui/ButtonNavigator.js
  9. 25
      src/ui/StepManager.js
  10. 36
      src/web3Provider/web3VmProvider.js
  11. 14
      test-browser/vmdebugger.js
  12. 8
      test/traceManager.js

@ -40,6 +40,7 @@
"babel-plugin-transform-regenerator": "^6.16.1",
"babelify": "^7.3.0",
"browserify": "^13.0.1",
"ethereum-common": "0.0.18",
"ethereumjs-util": "^4.5.0",
"fast-async": "^6.1.2",
"http-server": "^0.9.0",

@ -83,5 +83,48 @@ module.exports = {
findLowerBoundValue: function (target, array) {
var index = this.findLowerBound(target, array)
return index >= 0 ? array[index] : null
},
/**
* Find the call from @args rootCall which contains @args index (recursive)
*
* @param {Int} index - index of the vmtrace
* @param {Object} rootCall - call tree, built by the trace analyser
* @return {Object} - return the call which include the @args index
*/
findCall: findCall,
buildCallPath: buildCallPath
}
/**
* Find calls path from @args rootCall which leads to @args index (recursive)
*
* @param {Int} index - index of the vmtrace
* @param {Object} rootCall - call tree, built by the trace analyser
* @return {Array} - return the calls path to @args index
*/
function buildCallPath (index, rootCall) {
var ret = []
findCallInternal(index, rootCall, ret)
return ret
}
function findCall (index, rootCall) {
var ret = buildCallPath(index, rootCall)
return ret[ret.length - 1]
}
function findCallInternal (index, rootCall, callsPath) {
var calls = Object.keys(rootCall.calls)
var ret = rootCall
callsPath.push(rootCall)
for (var k in calls) {
var subCall = rootCall.calls[calls[k]]
if (index >= subCall.start && index <= subCall.return) {
findCallInternal(index, subCall, callsPath)
break
}
}
return ret
}

@ -90,6 +90,7 @@ TraceAnalyser.prototype.buildStorage = function (index, step, context) {
}
TraceAnalyser.prototype.buildDepth = function (index, step, tx, callStack, context) {
var outOfGas = runOutOfGas(step)
if (traceHelper.isCallInstruction(step) && !traceHelper.isCallToPrecompiledContract(index, this.trace)) {
var newAddress
if (traceHelper.isCreateInstruction(step)) {
@ -110,10 +111,10 @@ TraceAnalyser.prototype.buildDepth = function (index, step, tx, callStack, conte
this.traceCache.pushSteps(index, context.currentCallIndex)
context.lastCallIndex = context.currentCallIndex
context.currentCallIndex = 0
} else if (traceHelper.isReturnInstruction(step) || traceHelper.isStopInstruction(step)) {
if (index + 1 < this.trace.length) {
} else if (traceHelper.isReturnInstruction(step) || traceHelper.isStopInstruction(step) || outOfGas || step.error || step.invalidDepthChange) {
if (index < this.trace.length) {
callStack.pop()
this.traceCache.pushCall(step, index + 1, null, callStack.slice(0))
this.traceCache.pushCall(step, index + 1, null, callStack.slice(0), outOfGas || step.error || step.invalidDepthChange, outOfGas)
this.buildCalldata(index, step, tx, false)
this.traceCache.pushSteps(index, context.currentCallIndex)
context.currentCallIndex = context.lastCallIndex + 1
@ -125,4 +126,8 @@ TraceAnalyser.prototype.buildDepth = function (index, step, tx, callStack, conte
return context
}
function runOutOfGas (step) {
return parseInt(step.gas) - parseInt(step.gasCost) < 0
}
module.exports = TraceAnalyser

@ -7,9 +7,8 @@ TraceCache.prototype.init = function () {
// ...Changes contains index in the vmtrace of the corresponding changes
this.returnValues = {}
this.callChanges = []
this.calls = {}
this.callsRef = [0] // track of calls during the vm trace analysis
this.currentCall = null
this.callsTree = null
this.callsData = {}
this.contractCreation = {}
this.steps = {}
@ -34,19 +33,30 @@ TraceCache.prototype.pushMemoryChanges = function (value) {
this.memoryChanges.push(value)
}
TraceCache.prototype.pushCall = function (step, index, address, callStack) {
this.callChanges.push(index)
this.calls[index] = {
op: step.op,
address: address,
callStack: callStack
}
if (step.op === 'RETURN' || step.op === 'STOP') {
var call = this.callsRef.pop()
this.calls[index].call = call
this.calls[call].return = index
TraceCache.prototype.pushCall = function (step, index, address, callStack, reverted, outOfGas) {
if (step.op === 'RETURN' || step.op === 'STOP' || reverted) {
if (this.currentCall) {
this.currentCall.call.return = index - 1
this.currentCall.call.reverted = reverted
this.currentCall.call.outOfGas = outOfGas
var parent = this.currentCall.parent
this.currentCall = parent ? { call: parent.call, parent: parent.parent } : null
}
} else {
this.callsRef.push(index)
var call = {
op: step.op,
address: address,
callStack: callStack,
calls: {},
start: index
}
this.addresses.push(address)
if (this.currentCall) {
this.currentCall.call.calls[index] = call
} else {
this.callsTree = { call: call }
}
this.currentCall = { call: call, parent: this.currentCall }
}
}

@ -101,14 +101,7 @@ TraceManager.prototype.getStorageAt = function (stepIndex, tx, callback, address
}
TraceManager.prototype.getAddresses = function (callback) {
var addresses = [ this.tx.to ]
for (var k in this.traceCache.calls) {
var address = this.traceCache.calls[k].address
if (address && addresses.join('').indexOf(address) === -1) {
addresses.push(address)
}
}
callback(null, addresses)
callback(null, this.traceCache.addresses)
}
TraceManager.prototype.getCallDataAt = function (stepIndex, callback) {
@ -121,14 +114,24 @@ TraceManager.prototype.getCallDataAt = function (stepIndex, callback) {
callback(null, [this.traceCache.callsData[callDataChange]])
}
TraceManager.prototype.buildCallPath = function (stepIndex, callback) {
var check = this.checkRequestedStep(stepIndex)
if (check) {
return callback(check, null)
}
var callsPath = util.buildCallPath(stepIndex, this.traceCache.callsTree.call)
if (callsPath === null) return callback('no call path built', null)
callback(null, callsPath)
}
TraceManager.prototype.getCallStackAt = function (stepIndex, callback) {
var check = this.checkRequestedStep(stepIndex)
if (check) {
return callback(check, null)
}
var callStackChange = util.findLowerBoundValue(stepIndex, this.traceCache.callChanges)
if (callStackChange === null) return callback('no callstack found', null)
callback(null, this.traceCache.calls[callStackChange].callStack)
var call = util.findCall(stepIndex, this.traceCache.callsTree.call)
if (call === null) return callback('no callstack found', null)
callback(null, call.callStack)
}
TraceManager.prototype.getStackAt = function (stepIndex, callback) {
@ -151,7 +154,7 @@ TraceManager.prototype.getLastCallChangeSince = function (stepIndex, callback) {
if (check) {
return callback(check, null)
}
var callChange = util.findLowerBoundValue(stepIndex, this.traceCache.callChanges)
var callChange = util.findCall(stepIndex, this.traceCache.callsTree.call)
if (callChange === null) {
callback(null, 0)
} else {
@ -164,21 +167,14 @@ TraceManager.prototype.getCurrentCalledAddressAt = function (stepIndex, callback
if (check) {
return callback(check, null)
}
var self = this
this.getLastCallChangeSince(stepIndex, function (error, addressIndex) {
this.getLastCallChangeSince(stepIndex, function (error, resp) {
if (error) {
callback(error, null)
} else {
if (addressIndex === 0) {
callback(null, self.tx.to)
if (resp) {
callback(null, resp.address)
} else {
var callStack = self.traceCache.calls[addressIndex].callStack
var calledAddress = callStack[callStack.length - 1]
if (calledAddress) {
callback(null, calledAddress)
} else {
callback('unable to get current called address. ' + stepIndex + ' does not match with a CALL', null)
}
callback('unable to get current called address. ' + stepIndex + ' does not match with a CALL')
}
}
})
@ -271,6 +267,10 @@ TraceManager.prototype.findNextCall = function (currentStep) {
return this.traceStepManager.findNextCall(currentStep)
}
TraceManager.prototype.findStepOut = function (currentStep) {
return this.traceStepManager.findStepOut(currentStep)
}
// util
TraceManager.prototype.checkRequestedStep = function (stepIndex) {
if (!this.trace) {

@ -27,7 +27,13 @@ TraceRetriever.prototype.getStorage = function (tx, address, callback) {
// The VM gives only a tx hash
// TODO: get rid of that and use the range parameters
util.web3.debug.storageRangeAt(tx.blockHash, tx.transactionIndex === undefined ? tx.hash : tx.transactionIndex, address, '0x0', '0x' + end, maxSize, function (error, result) {
callback(error, result.storage)
if (error) {
callback(error)
} else if (result.storage) {
callback(null, result.storage)
} else {
callback('storage has not been provided')
}
})
} else {
callback('no storageRangeAt endpoint found')

@ -8,7 +8,7 @@ function TraceStepManager (_traceAnalyser) {
TraceStepManager.prototype.isCallInstruction = function (index) {
var state = this.traceAnalyser.trace[index]
return traceHelper.isCallInstruction(state)
return traceHelper.isCallInstruction(state) && !traceHelper.isCallToPrecompiledContract(index, this.traceAnalyser.trace)
}
TraceStepManager.prototype.isReturnInstruction = function (index) {
@ -17,8 +17,9 @@ TraceStepManager.prototype.isReturnInstruction = function (index) {
}
TraceStepManager.prototype.findStepOverBack = function (currentStep) {
if (this.isReturnInstruction(currentStep - 1)) {
return this.traceAnalyser.traceCache.calls[currentStep].call - 1
if (this.isReturnInstruction(currentStep)) {
var call = util.findCall(currentStep, this.traceAnalyser.traceCache.callsTree.call)
return call.start > 0 ? call.start - 1 : 0
} else {
return currentStep > 0 ? currentStep - 1 : 0
}
@ -26,21 +27,26 @@ TraceStepManager.prototype.findStepOverBack = function (currentStep) {
TraceStepManager.prototype.findStepOverForward = function (currentStep) {
if (this.isCallInstruction(currentStep)) {
return this.traceAnalyser.traceCache.calls[currentStep + 1].return
var call = util.findCall(currentStep + 1, this.traceAnalyser.traceCache.callsTree.call)
return call.return + 1 < this.traceAnalyser.trace.length ? call.return + 1 : this.traceAnalyser.trace.length - 1
} else {
return this.traceAnalyser.trace.length >= currentStep + 1 ? currentStep + 1 : currentStep
}
}
TraceStepManager.prototype.findNextCall = function (currentStep) {
var callChanges = this.traceAnalyser.traceCache.callChanges
var stepIndex = util.findLowerBound(currentStep, callChanges)
var callchange = callChanges[stepIndex + 1]
if (callchange && this.isCallInstruction(callchange - 1)) {
return callchange - 1
var call = util.findCall(currentStep, this.traceAnalyser.traceCache.callsTree.call)
var subCalls = Object.keys(call.calls)
if (subCalls.length) {
return call.calls[subCalls[0]].start - 1
} else {
return currentStep
}
}
TraceStepManager.prototype.findStepOut = function (currentStep) {
var call = util.findCall(currentStep, this.traceAnalyser.traceCache.callsTree.call)
return call.return
}
module.exports = TraceStepManager

@ -4,15 +4,52 @@ var style = require('./styles/basicStyles')
var ui = require('../helpers/ui')
var yo = require('yo-yo')
function ButtonNavigator (_traceManager) {
function ButtonNavigator (_parent, _traceManager) {
this.event = new EventManager()
this.intoBackDisabled = true
this.overBackDisabled = true
this.intoForwardDisabled = true
this.overForwardDisabled = true
this.nextCallDisabled = true
this.jumpOutDisabled = true
this.traceManager = _traceManager
this.currentCall = null
this.revertionPoint = null
_parent.event.register('indexChanged', this, (index) => {
if (index < 0) return
if (_parent.currentStepIndex !== index) return
this.traceManager.buildCallPath(index, (error, callsPath) => {
if (error) {
console.log(error)
resetWarning(this)
} else {
this.currentCall = callsPath[callsPath.length - 1]
if (this.currentCall.reverted) {
this.revertionPoint = this.currentCall.return
this.view.querySelector('#reverted').style.display = 'block'
this.view.querySelector('#reverted #outofgas').style.display = this.currentCall.outOfGas ? 'inline' : 'none'
this.view.querySelector('#reverted #parenthasthrown').style.display = 'none'
} else {
var k = callsPath.length - 2
while (k > 0) {
var parent = callsPath[k]
if (parent.reverted) {
this.revertionPoint = parent.return
this.view.querySelector('#reverted').style.display = 'block'
this.view.querySelector('#reverted #parenthasthrown').style.display = parent ? 'inline' : 'none'
this.view.querySelector('#reverted #outofgas').style.display = 'none'
return
}
k--
}
resetWarning(this)
}
}
})
})
this.view
}
@ -32,6 +69,15 @@ ButtonNavigator.prototype.render = function () {
</button>
<button id='nextcall' title='step next call' class='fa fa-chevron-right' style=${ui.formatCss(style.button)} onclick=${function () { self.event.trigger('jumpNextCall') }} disabled=${this.nextCallDisabled} >
</button>
<button id='jumpout' title='jump out' class='fa fa-share' style=${ui.formatCss(style.button)} onclick=${function () { self.event.trigger('jumpOut') }} disabled=${this.jumpOutDisabled} >
</button>
<div id='reverted' style="display:none">
<button id='jumptoexception' title='jump to exception' class='fa fa-exclamation-triangle' style=${ui.formatCss(style.button)} onclick=${function () { self.event.trigger('jumpToException', [self.revertionPoint]) }} disabled=${this.jumpOutDisabled} >
</button>
<span>State changes made during this call will be reverted.</span>
<span id='outofgas' style="display:none">This call will run out of gas.</span>
<span id='parenthasthrown' style="display:none">The parent call will throw an exception</span>
</div>
</div>`
if (!this.view) {
this.view = view
@ -45,6 +91,8 @@ ButtonNavigator.prototype.reset = function () {
this.intoForwardDisabled = true
this.overForwardDisabled = true
this.nextCallDisabled = true
this.jumpOutDisabled = true
resetWarning(this)
}
ButtonNavigator.prototype.stepChanged = function (step) {
@ -53,7 +101,6 @@ ButtonNavigator.prototype.stepChanged = function (step) {
if (!this.traceManager) {
this.intoForwardDisabled = true
this.overForwardDisabled = true
this.nextCallDisabled = true
} else {
var self = this
this.traceManager.getLength(function (error, length) {
@ -63,7 +110,10 @@ ButtonNavigator.prototype.stepChanged = function (step) {
} else {
self.intoForwardDisabled = step >= length - 1
self.overForwardDisabled = step >= length - 1
self.nextCallDisabled = step >= length - 1
var nextCall = self.traceManager.findNextCall(step)
self.nextCallDisabled = nextCall === step
var stepOut = self.traceManager.findStepOut(step)
self.jumpOutDisabled = stepOut === step
}
self.updateAll()
})
@ -77,6 +127,8 @@ ButtonNavigator.prototype.updateAll = function () {
this.updateDisabled('overforward', this.overForwardDisabled)
this.updateDisabled('intoforward', this.intoForwardDisabled)
this.updateDisabled('nextcall', this.nextCallDisabled)
this.updateDisabled('jumpout', this.jumpOutDisabled)
this.updateDisabled('jumptoexception', this.jumpOutDisabled)
}
ButtonNavigator.prototype.updateDisabled = function (id, disabled) {
@ -87,4 +139,10 @@ ButtonNavigator.prototype.updateDisabled = function (id, disabled) {
}
}
function resetWarning (self) {
self.view.querySelector('#reverted #outofgas').style.display = 'none'
self.view.querySelector('#reverted #parenthasthrown').style.display = 'none'
self.view.querySelector('#reverted').style.display = 'none'
}
module.exports = ButtonNavigator

@ -26,7 +26,7 @@ function StepManager (_parent, _traceManager) {
self.sliderMoved(step)
})
this.buttonNavigator = new ButtonNavigator(this.traceManager)
this.buttonNavigator = new ButtonNavigator(_parent, this.traceManager)
this.buttonNavigator.event.register('stepIntoBack', this, function () {
self.stepIntoBack()
})
@ -42,6 +42,12 @@ function StepManager (_parent, _traceManager) {
this.buttonNavigator.event.register('jumpNextCall', this, function () {
self.jumpNextCall()
})
this.buttonNavigator.event.register('jumpOut', this, function () {
self.jumpOut()
})
this.buttonNavigator.event.register('jumpToException', this, function (exceptionIndex) {
self.jumpTo(exceptionIndex)
})
}
StepManager.prototype.render = function () {
@ -68,6 +74,14 @@ StepManager.prototype.newTraceAvailable = function () {
this.init()
}
StepManager.prototype.jumpTo = function (step) {
if (!this.traceManager.inRange(step)) {
return
}
this.slider.setValue(step)
this.changeState(step)
}
StepManager.prototype.sliderMoved = function (step) {
if (!this.traceManager.inRange(step)) {
return
@ -126,6 +140,15 @@ StepManager.prototype.jumpNextCall = function () {
this.changeState(step)
}
StepManager.prototype.jumpOut = function () {
if (!this.traceManager.isLoaded()) {
return
}
var step = this.traceManager.findStepOut(this.currentStepIndex)
this.slider.setValue(step)
this.changeState(step)
}
StepManager.prototype.changeState = function (step) {
this.currentStepIndex = step
this.buttonNavigator.stepChanged(step)

@ -1,4 +1,5 @@
var util = require('../helpers/util')
var uiutil = require('../helpers/ui')
var traceHelper = require('../helpers/traceHelper')
var Web3 = require('web3')
@ -9,6 +10,9 @@ function web3VmProvider () {
this.vmTraces = {}
this.txs = {}
this.processingHash
this.processingAddress
this.processingIndex
this.previousDepth = 0
this.incr = 0
this.eth = {}
this.debug = {}
@ -57,6 +61,7 @@ web3VmProvider.prototype.txWillProcess = function (self, data) {
if (data.to && data.to.length) {
tx.to = util.hexConvert(data.to)
}
this.processingAddress = tx.to
tx.data = util.hexConvert(data.data)
tx.input = util.hexConvert(data.input)
tx.gas = util.hexConvert(data.gas)
@ -70,6 +75,7 @@ web3VmProvider.prototype.txWillProcess = function (self, data) {
self.storageCache[self.processingHash][tx.to] = storage
})
}
this.processingIndex = 0
}
web3VmProvider.prototype.txProcessed = function (self, data) {
@ -79,13 +85,22 @@ web3VmProvider.prototype.txProcessed = function (self, data) {
} else {
self.vmTraces[self.processingHash].return = util.hexConvert(data.vm.return)
}
this.processingIndex = null
this.processingAddress = null
this.previousDepth = 0
}
web3VmProvider.prototype.pushTrace = function (self, data) {
var depth = data.depth + 1 // geth starts the depth from 1
if (!self.processingHash) {
console.log('no tx processing')
return
}
if (this.previousDepth > depth) {
// returning from context, set error it is not STOP, RETURN
var previousopcode = self.vmTraces[self.processingHash].structLogs[this.processingIndex - 1]
previousopcode.invalidDepthChange = previousopcode.op !== 'RETURN' && previousopcode.op !== 'STOP'
}
var step = {
stack: util.hexListConvert(data.stack),
memory: util.formatMemory(data.memory),
@ -93,17 +108,26 @@ web3VmProvider.prototype.pushTrace = function (self, data) {
op: data.opcode.name,
pc: data.pc,
gasCost: data.opcode.fee.toString(),
gas: data.gasLeft.toString()
gas: data.gasLeft.toString(),
depth: depth,
error: data.error === false ? undefined : data.error
}
self.vmTraces[self.processingHash].structLogs.push(step)
if (traceHelper.newContextStorage(step)) {
if (!self.storageCache[self.processingHash][address]) {
var address = step.stack[step.stack.length - 2]
self.vm.stateManager.dumpStorage(address, function (storage) {
self.storageCache[self.processingHash][address] = storage
})
if (step.op === 'CREATE') {
this.processingAddress = traceHelper.contractCreationToken(this.processingIndex)
this.storageCache[this.processingHash][this.processingAddress] = {}
} else {
this.processingAddress = uiutil.normalizeHex(step.stack[step.stack.length - 2])
if (!self.storageCache[self.processingHash][this.processingAddress]) {
self.vm.stateManager.dumpStorage(this.processingAddress, function (storage) {
self.storageCache[self.processingHash][this.processingAddress] = storage
})
}
}
}
this.processingIndex++
this.previousDepth = depth
}
web3VmProvider.prototype.getCode = function (address, cb) {

@ -130,15 +130,19 @@ function stepping (browser) {
.click('#intoforward')
.click('#overforward')
.assertCurrentSelectedItem('007 MLOAD')
.click('#overback')
.click('#overback')
.click('#overback')
.click('#overback')
.click('#overback')
.click('#intoback')
.click('#intoback')
.click('#intoback')
.click('#intoback')
.click('#intoback')
.click('#overforward')
.assertCurrentSelectedItem('182 PUSH1 01')
.click('#overforward')
.assertCurrentSelectedItem('184 PUSH1 00')
.click('#intoback')
.click('#intoback')
.click('#overback')
.assertCurrentSelectedItem('181 CREATE')
return browser
}

@ -125,7 +125,7 @@ tape('TraceManager', function (t) {
if (error) {
st.fail(error)
} else {
st.ok(result === 0)
st.ok(result.start === 0)
}
})
@ -134,7 +134,7 @@ tape('TraceManager', function (t) {
if (error) {
st.fail(error)
} else {
st.ok(result === 64)
st.ok(result.start === 64)
}
})
@ -143,7 +143,9 @@ tape('TraceManager', function (t) {
if (error) {
st.fail(error)
} else {
st.ok(result === 109)
st.ok(result.start === 0)
// this was 109 before: 111 is targeting the root call (starting index 0)
// this test make more sense as it is now (109 is the index of RETURN).
}
})
})

Loading…
Cancel
Save