From 8b76861318b402923c77b2f55f64be54734a8cc9 Mon Sep 17 00:00:00 2001 From: cdetrio Date: Tue, 2 May 2017 12:18:08 -0400 Subject: [PATCH] initial support for mapping types --- src/helpers/traceHelper.js | 4 ++ src/solidity/decodeInfo.js | 19 +++++++-- src/solidity/types/Mapping.js | 52 ++++++++++++++++++++++-- src/solidity/types/StringType.js | 2 +- src/trace/traceAnalyser.js | 16 ++++++++ src/trace/traceCache.js | 14 +++++++ src/ui/SolidityState.js | 68 ++++++++++++++++++++++++++++++-- src/ui/SolidityTypeFormatter.js | 9 ++++- 8 files changed, 170 insertions(+), 14 deletions(-) diff --git a/src/helpers/traceHelper.js b/src/helpers/traceHelper.js index 8d2a560d30..6e66c6cbd1 100644 --- a/src/helpers/traceHelper.js +++ b/src/helpers/traceHelper.js @@ -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' }, diff --git a/src/solidity/decodeInfo.js b/src/solidity/decodeInfo.js index aaee05cd76..4dbd1d341d 100644 --- a/src/solidity/decodeInfo.js +++ b/src/solidity/decodeInfo.js @@ -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)) } /** @@ -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 { @@ -222,7 +233,7 @@ function getEnum (type, stateDefinitions, contractName) { * @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) { +function getStructMembers (type, stateDefinitions, contractName) { var split = type.split('.') if (!split.length) { type = contractName + '.' + type diff --git a/src/solidity/types/Mapping.js b/src/solidity/types/Mapping.js index e61df47201..0e62012af0 100644 --- a/src/solidity/types/Mapping.js +++ b/src/solidity/types/Mapping.js @@ -1,20 +1,41 @@ 'use strict' var RefType = require('./RefType') +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 + } + + setMappingElements (mappingKeyPreimages) { + this.preimages = mappingKeyPreimages } async decodeFromStorage (location, storageResolver) { + // location.offset should always be 0 for a mapping (?? double check) + + var ret = {} + for (var i in this.preimages) { + var preimage = this.preimages[i] + var mapLocation = getMappingLocation(preimage, location.slot) + var globalLocation = { + offset: location.offset, + slot: mapLocation + } + ret[preimage] = await this.valueType.decodeFromStorage(globalLocation, storageResolver) + } + return { - value: '', - 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: '', length: '0x', @@ -23,4 +44,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 diff --git a/src/solidity/types/StringType.js b/src/solidity/types/StringType.js index f21eac4a51..a74b6745a3 100644 --- a/src/solidity/types/StringType.js +++ b/src/solidity/types/StringType.js @@ -40,7 +40,7 @@ function format (decoded) { var value = decoded.value value = value.replace('0x', '').replace(/(..)/g, '%$1') var ret = { - length: decoded.length, + // length: decoded.length, // unneeded, only dynamicBytes uses length raw: decoded.value, type: 'string' } diff --git a/src/trace/traceAnalyser.js b/src/trace/traceAnalyser.js index 66aefa2c51..2dd60fbadb 100644 --- a/src/trace/traceAnalyser.js +++ b/src/trace/traceAnalyser.js @@ -1,5 +1,6 @@ 'use strict' var traceHelper = require('../helpers/traceHelper') +var ethutil = require('ethereumjs-util') function TraceAnalyser (_cache) { this.traceCache = _cache @@ -71,6 +72,18 @@ TraceAnalyser.prototype.buildMemory = function (index, step) { } } +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 memoryHex = memory.join('') + var sha3Input = memoryHex.substr(memoryStart, memoryLength) + return sha3Input +} + TraceAnalyser.prototype.buildStorage = function (index, step, context) { if (traceHelper.newContextStorage(step) && !traceHelper.isCallToPrecompiledContract(index, this.trace)) { var calledAddress = traceHelper.resolveCalledAddress(index, this.trace) @@ -80,6 +93,9 @@ TraceAnalyser.prototype.buildStorage = function (index, step, context) { console.log('unable to build storage changes. ' + index + ' does not match with a CALL. storage changes will be corrupted') } this.traceCache.pushStoreChanges(index + 1, context.storageContext[context.storageContext.length - 1]) + } else if (traceHelper.isSHA3Instruction(step)) { + var sha3Input = getSha3Input(step.stack, step.memory) + this.traceCache.pushSha3Preimage(sha3Input, context.storageContext[context.storageContext.length - 1]) } else if (traceHelper.isSSTOREInstruction(step)) { this.traceCache.pushStoreChanges(index + 1, context.storageContext[context.storageContext.length - 1], step.stack[step.stack.length - 1], step.stack[step.stack.length - 2]) } else if (traceHelper.isReturnInstruction(step)) { diff --git a/src/trace/traceCache.js b/src/trace/traceCache.js index 71f14be8ee..edff2aa984 100644 --- a/src/trace/traceCache.js +++ b/src/trace/traceCache.js @@ -1,5 +1,6 @@ 'use strict' var helper = require('../helpers/util') +var ethutil = require('ethereumjs-util') function TraceCache () { this.init() @@ -20,6 +21,9 @@ TraceCache.prototype.init = function () { this.memoryChanges = [] this.storageChanges = [] this.sstore = {} // all sstore occurence in the trace + if (!this.sha3Preimages) { // need to accumulate the preimages over multiple tx's, so dont clear + this.sha3Preimages = {} + } } TraceCache.prototype.pushSteps = function (index, currentCallIndex) { @@ -92,6 +96,16 @@ TraceCache.prototype.pushStoreChanges = function (index, address, key, value) { this.storageChanges.push(index) } +TraceCache.prototype.pushSha3Preimage = function (sha3Input, address) { + console.log('pushSha3Preimage sha3Input:', sha3Input) + var preimage = sha3Input + var imageHash = ethutil.sha3('0x' + sha3Input).toString('hex') + this.sha3Preimages[imageHash] = { + 'address': address, + 'preimage': preimage + } +} + TraceCache.prototype.accumulateStorageChanges = function (index, address, storage) { var ret = Object.assign({}, storage) for (var k in this.storageChanges) { diff --git a/src/ui/SolidityState.js b/src/ui/SolidityState.js index ed684f1b01..691e91131a 100644 --- a/src/ui/SolidityState.js +++ b/src/ui/SolidityState.js @@ -3,6 +3,8 @@ var DropdownPanel = require('./DropdownPanel') var stateDecoder = require('../solidity/stateDecoder') var solidityTypeFormatter = require('./SolidityTypeFormatter') var StorageViewer = require('../storage/storageViewer') +var util = require('../solidity/types/util') +var ethutil = require('ethereumjs-util') var yo = require('yo-yo') function SolidityState (_parent, _traceManager, _codeManager, _solidityProxy) { @@ -64,9 +66,28 @@ SolidityState.prototype.init = function () { tx: self.parent.tx, address: address }, self.storageResolver, self.traceManager) - stateDecoder.decodeState(stateVars, storageViewer).then((result) => { - if (!result.error) { - self.basicPanel.update(result) + + var storageJSON = {} + storageViewer.storageRange(function (error, result) { + if (!error) { + storageJSON = result + var sha3Preimages = self.traceManager.traceCache.sha3Preimages + var mappingPreimages = getMappingPreimages(stateVars, storageJSON, sha3Preimages) + + for (var k in stateVars) { + var stateVar = stateVars[k] + if (stateVar.type.typeName.indexOf('mapping') === 0) { + var mapSlot = util.toBN(stateVar.storagelocation.slot).toString(16) + mapSlot = ethutil.setLengthLeft('0x' + mapSlot, 32).toString('hex') + stateVar.type.setMappingElements(mappingPreimages[mapSlot]) + } + } + + stateDecoder.decodeState(stateVars, storageViewer).then((result) => { + if (!result.error) { + self.basicPanel.update(result) + } + }) } }) } @@ -76,4 +97,45 @@ SolidityState.prototype.init = function () { }) } +function getMappingPreimages (stateVars, storage, preimages) { + // loop over stateVars and get the locations of all the mappings + // then on each mapping, pass its specific preimage keys + + // first filter out all non-mapping storage slots + + var ignoreSlots = [] + for (var k in stateVars) { + var stateVar = stateVars[k] + if (stateVar.type.typeName.indexOf('mapping') !== 0) { + ignoreSlots.push(stateVar.storagelocation.slot.toString()) + } + } + + var possibleMappings = [] + for (var hashedLoc in storage) { + var slotNum = util.toBN(storage[hashedLoc].key).toString(10) + if (ignoreSlots.indexOf(slotNum) === -1) { + possibleMappings.push(storage[hashedLoc].key) + } + } + + var storageMappings = {} + for (var pk in possibleMappings) { + var possMapKey = possibleMappings[pk].replace('0x', '') + if (preimages[possMapKey]) { + // got preimage! + var preimage = preimages[possMapKey].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 (!storageMappings[mappingSlot]) { + storageMappings[mappingSlot] = [] + } + storageMappings[mappingSlot].push(mappingKey) + } + } + return storageMappings +} + module.exports = SolidityState diff --git a/src/ui/SolidityTypeFormatter.js b/src/ui/SolidityTypeFormatter.js index cfb03d99d4..1d4b60af02 100644 --- a/src/ui/SolidityTypeFormatter.js +++ b/src/ui/SolidityTypeFormatter.js @@ -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 @@ -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 || @@ -63,4 +69,3 @@ function fontColor (data) { } return 'color:' + color } -