Skip to content
This repository was archived by the owner on Mar 3, 2021. It is now read-only.
4 changes: 4 additions & 0 deletions src/helpers/traceHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ module.exports = {
return step.op === 'SSTORE'
},

isSHA3Instruction: function (step) {
return step.op === 'SHA3'
},

newContextStorage: function (step) {
return step.op === 'CREATE' || step.op === 'CALL'
},
Expand Down
20 changes: 15 additions & 5 deletions src/solidity/decodeInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,19 @@ var util = require('./types/util')
* @param {String} type - type given by the AST
* @return {Object} returns decoded info about the current type: { storageBytes, typeName}
*/
function mapping (type) {
return new MappingType()
function mapping (type, stateDefinitions, contractName) {
var match = type.match(/mapping\((.*?)( =>)? (.*)\)$/)
var keyTypeName = match[1]
var valueTypeName = match[3]

var keyType = parseType(keyTypeName, stateDefinitions, contractName, 'storage')
var valueType = parseType(valueTypeName, stateDefinitions, contractName, 'storage')

var underlyingTypes = {
'keyType': keyType,
'valueType': valueType
}
return new MappingType(underlyingTypes, 'location', util.removeLocation(type))
}

/**
Expand Down Expand Up @@ -179,7 +190,7 @@ function struct (type, stateDefinitions, contractName, location) {
if (!location) {
location = match[2].trim()
}
var memberDetails = getStructMembers(match[1], stateDefinitions, contractName, location) // type is used to extract the ast struct definition
var memberDetails = getStructMembers(match[1], stateDefinitions, contractName) // type is used to extract the ast struct definition
if (!memberDetails) return null
return new StructType(memberDetails, location, match[1])
} else {
Expand Down Expand Up @@ -219,10 +230,9 @@ function getEnum (type, stateDefinitions, contractName) {
* @param {String} typeName - name of the struct type (e.g struct <name>)
* @param {Object} stateDefinitions - all state definition given by the AST (including struct and enum type declaration) for all contracts
* @param {String} contractName - contract the @args typeName belongs to
* @param {String} location - location of the data (storage ref| storage pointer| memory| calldata)
* @return {Array} containing all members of the current struct type
*/
function getStructMembers (type, stateDefinitions, contractName, location) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test for the following type: struct X { uint a; mapping(uint=>uint) b; uint c; }
If such a struct is used in memory, it should be identical to struct X { uint a; uint c; }.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function getStructMembers (type, stateDefinitions, contractName) {
var split = type.split('.')
if (!split.length) {
type = contractName + '.' + type
Expand Down
57 changes: 53 additions & 4 deletions src/solidity/types/Mapping.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,46 @@
'use strict'
var RefType = require('./RefType')
var util = require('./util')
var ethutil = require('ethereumjs-util')

class Mapping extends RefType {
constructor () {
super(1, 32, 'mapping', 'storage')
constructor (underlyingTypes, location, fullType) {
super(1, 32, fullType, 'storage')
this.keyType = underlyingTypes.keyType
this.valueType = underlyingTypes.valueType
}

async decodeFromStorage (location, storageResolver) {
try {
var mappingsPreimages = await storageResolver.mappingPreimages()
} catch (e) {
return {
value: '<error> ' + e.message,
type: this.type
}
}
var mapSlot = util.normalizeHex(ethutil.bufferToHex(location.slot))
console.log(mapSlot, mappingsPreimages)
var mappingPreimages = mappingsPreimages[mapSlot]
var ret = {}
for (var i in mappingPreimages) {
var mapLocation = getMappingLocation(i, location.slot)
var globalLocation = {
offset: location.offset,
slot: mapLocation
}
ret[i] = await this.valueType.decodeFromStorage(globalLocation, storageResolver)
}

return {
value: '<not implemented>',
length: '0x',
value: ret,
type: this.typeName
}
}

decodeFromMemoryInternal (offset, memory) {
// mappings can only exist in storage and not in memory
// so this should never be called
return {
value: '<not implemented>',
length: '0x',
Expand All @@ -23,4 +49,27 @@ class Mapping extends RefType {
}
}

function getMappingLocation (key, position) {
// mapping storage location decribed at http://solidity.readthedocs.io/en/develop/miscellaneous.html#layout-of-state-variables-in-storage
// > the value corresponding to a mapping key k is located at keccak256(k . p) where . is concatenation.

// key should be a hex string, and position an int
var mappingK = ethutil.toBuffer('0x' + key)
mappingK = ethutil.setLengthLeft(mappingK, 32)
var mappingP = ethutil.intToBuffer(position)
mappingP = ethutil.setLengthLeft(mappingP, 32)
var mappingKeyBuf = concatTypedArrays(mappingK, mappingP)
var mappingKeyPreimage = '0x' + mappingKeyBuf.toString('hex')
var mappingStorageLocation = ethutil.sha3(mappingKeyPreimage)
mappingStorageLocation = new ethutil.BN(mappingStorageLocation, 16)
return mappingStorageLocation
}

function concatTypedArrays (a, b) { // a, b TypedArray of same type
let c = new (a.constructor)(a.length + b.length)
c.set(a, 0)
c.set(b, a.length)
return c
}

module.exports = Mapping
3 changes: 2 additions & 1 deletion src/solidity/types/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ module.exports = {
toBN: toBN,
add: add,
extractLocation: extractLocation,
removeLocation: removeLocation
removeLocation: removeLocation,
normalizeHex: normalizeHex
}

function decodeIntFromHex (value, byteLength, signed) {
Expand Down
79 changes: 79 additions & 0 deletions src/storage/mappingPreimages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
var global = require('../helpers/global')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please document all functions here.


module.exports = {
extractMappingPreimages: extractMappingPreimages
}

/**
* Uses the storageViewer to retrieve the storage and returns a mapping containing possible solidity mappping type location.
* like { "<mapping_slot>" : { "<mapping-key1>": preimageOf1 }, { "<mapping-key2>": preimageOf2 }, ... }
*
* @param {Object} address - storageViewer
* @return {Map} - solidity mapping location
*/
async function extractMappingPreimages (storageViewer) {
return new Promise((resolve, reject) => {
storageViewer.storageRange(function (error, storage) {
if (!error) {
decodeMappingsKeys(storage, (error, mappings) => {
if (error) {
reject(error)
} else {
resolve(mappings)
}
})
} else {
reject(error)
}
})
})
}

/**
* Uses the storageViewer to retrieve the storage and returns a mapping containing possible solidity mappping type location.
* like { "<mapping_slot>" : { "<mapping-key1>": preimageOf1 }, { "<mapping-key2>": preimageOf2 }, ... }
*
* @param {Object} storage - storage given by storage Viewer (basically a mapping hashedkey : {key, value})
* @param {Function} callback - calback
* @return {Map} - solidity mapping location (e.g { "<mapping_slot>" : { "<mapping-key1>": preimageOf1 }, { "<mapping-key2>": preimageOf2 }, ... })
*/
async function decodeMappingsKeys (storage, callback) {
var ret = {}
for (var hashedLoc in storage) {
var preimage
try {
preimage = await getPreimage(storage[hashedLoc].key)
} catch (e) {
}
if (preimage) {
// got preimage!
// get mapping position (i.e. storage slot), its the last 32 bytes
var slotByteOffset = preimage.length - 64
var mappingSlot = preimage.substr(slotByteOffset)
var mappingKey = preimage.substr(0, slotByteOffset)
if (!ret[mappingSlot]) {
ret[mappingSlot] = {}
}
ret[mappingSlot][mappingKey] = preimage
}
}
callback(null, ret)
}

/**
* Uses web3 to return preimage of a key
*
* @param {String} key - key to retrieve the preimage of
* @return {String} - preimage of the given key
*/
function getPreimage (key) {
return new Promise((resolve, reject) => {
global.web3.debug.preimage(key, function (error, preimage) {
if (error) {
reject(error)
} else {
resolve(preimage)
}
})
})
}
12 changes: 12 additions & 0 deletions src/storage/storageViewer.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
'use strict'
var helper = require('../helpers/util')
var mappingPreimagesExtractor = require('./mappingPreimages')

class StorageViewer {
constructor (_context, _storageResolver, _traceManager) {
this.context = _context
this.storageResolver = _storageResolver
// contains [mappingSlot][mappingkey] = preimage
// this map is renewed for each execution step

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should only do it once at the beginning of the debugging session and use the trace otherwise.

// this map is shared among all the mapping types
this.mappingsPreimages = null
_traceManager.accumulateStorageChanges(this.context.stepIndex, this.context.address, {}, (error, storageChanges) => {
if (!error) {
this.storageChanges = storageChanges
Expand Down Expand Up @@ -58,6 +63,13 @@ class StorageViewer {
isComplete (address) {
return this.storageResolver.isComplete(address)
}

async mappingPreimages () {
if (!this.mappingsPreimages) {
this.mappingsPreimages = await mappingPreimagesExtractor.extractMappingPreimages(this)
}
return this.mappingsPreimages
}
}

module.exports = StorageViewer
9 changes: 7 additions & 2 deletions src/ui/SolidityTypeFormatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ function extractData (item, parent, key) {
})
ret.self = item.type
ret.isStruct = true
} else if (item.type.indexOf('mapping') === 0) {
ret.children = Object.keys((item.value || {})).map(function (key) {
return {key: key, value: item.value[key]}
})
ret.isMapping = true
ret.self = item.type
} else {
ret.children = []
ret.self = item.value
Expand All @@ -51,7 +57,7 @@ function extractData (item, parent, key) {

function fontColor (data) {
var color = '#124B46'
if (data.isArray || data.isStruct) {
if (data.isArray || data.isStruct || data.isMapping) {
color = '#847979'
} else if (data.type.indexOf('uint') === 0 ||
data.type.indexOf('int') === 0 ||
Expand All @@ -63,4 +69,3 @@ function fontColor (data) {
}
return 'color:' + color
}

9 changes: 9 additions & 0 deletions src/util/web3Admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ module.exports = {
}
// DEBUG
var methods = []
if (!(web3.debug && web3.debug.preimage)) {
methods.push(new web3._extend.Method({
name: 'preimage',
call: 'debug_preimage',
inputFormatter: [null],
params: 1
}))
}

if (!(web3.debug && web3.debug.traceTransaction)) {
methods.push(new web3._extend.Method({
name: 'traceTransaction',
Expand Down
33 changes: 33 additions & 0 deletions src/web3Provider/web3VmProvider.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var util = require('../helpers/util')
var uiutil = require('../helpers/ui')
var traceHelper = require('../helpers/traceHelper')
var ethutil = require('ethereumjs-util')
var Web3 = require('web3')

function web3VmProvider () {
Expand All @@ -22,9 +23,11 @@ function web3VmProvider () {
this.eth.getBlockNumber = function (cb) { return self.getBlockNumber(cb) }
this.debug.traceTransaction = function (hash, options, cb) { return self.traceTransaction(hash, options, cb) }
this.debug.storageRangeAt = function (blockNumber, txIndex, address, start, end, maxLength, cb) { return self.storageRangeAt(blockNumber, txIndex, address, start, end, maxLength, cb) }
this.debug.preimage = function (hashedKey, cb) { return self.preimage(hashedKey, cb) }
this.providers = { 'HttpProvider': function (url) {} }
this.currentProvider = {'host': 'vm provider'}
this.storageCache = {}
this.sha3Preimages = {}
}

web3VmProvider.prototype.setVM = function (vm) {
Expand Down Expand Up @@ -128,6 +131,14 @@ web3VmProvider.prototype.pushTrace = function (self, data) {
}
}
}
if (traceHelper.isSHA3Instruction(step)) {
var sha3Input = getSha3Input(step.stack, step.memory)
var preimage = sha3Input
var imageHash = ethutil.sha3('0x' + sha3Input).toString('hex')
self.sha3Preimages[imageHash] = {
'preimage': preimage
}
}
this.processingIndex++
this.previousDepth = depth
}
Expand Down Expand Up @@ -189,4 +200,26 @@ web3VmProvider.prototype.getTransactionFromBlock = function (blockNumber, txInde
}
}

web3VmProvider.prototype.preimage = function (hashedKey, cb) {
hashedKey = hashedKey.replace('0x', '')
cb(null, this.sha3Preimages[hashedKey] !== undefined ? this.sha3Preimages[hashedKey].preimage : null)
}

function getSha3Input (stack, memory) {
var memoryStart = stack[stack.length - 1]
var memoryLength = stack[stack.length - 2]
var memStartDec = (new ethutil.BN(memoryStart.replace('0x', ''), 16)).toString(10)
memoryStart = parseInt(memStartDec) * 2
var memLengthDec = (new ethutil.BN(memoryLength.replace('0x', ''), 16).toString(10))
memoryLength = parseInt(memLengthDec) * 2

var subMemoryIndex = Math.floor(memoryStart / 32)
var sha3Input = ''
while (sha3Input.length < memoryLength) {
sha3Input += memory[subMemoryIndex]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I think I misunderstood your question earlier. Unfortunately this won't work. Types in memory most of the time have a length of 32 bytes but

  1. they might be offset by some amount
  2. this does not apply to sha3

subMemoryIndex++
}
return sha3Input
}

module.exports = web3VmProvider