You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
342 lines
10 KiB
342 lines
10 KiB
'use strict'
|
|
import { hash } from '@remix-project/remix-lib'
|
|
import { bytesToHex, setLengthLeft, toBytes, addHexPrefix } from '@ethereumjs/util'
|
|
import stringSimilarity from 'string-similarity'
|
|
import { BN } from 'bn.js'
|
|
import { isBigInt } from 'web3-validator'
|
|
|
|
/*
|
|
contains misc util: @TODO should be split
|
|
- hex conversion
|
|
- binary search
|
|
- CALL related look up
|
|
- sha3 calculation
|
|
- swarm hash extraction
|
|
- bytecode comparison
|
|
*/
|
|
/*
|
|
ints: IntArray
|
|
*/
|
|
|
|
/**
|
|
* Converts a hex string to an array of integers.
|
|
*/
|
|
export function hexToIntArray (hexString) {
|
|
if (hexString.slice(0, 2) === '0x') {
|
|
hexString = hexString.slice(2)
|
|
}
|
|
const integers = []
|
|
for (let i = 0; i < hexString.length; i += 2) {
|
|
integers.push(parseInt(hexString.slice(i, i + 2), 16))
|
|
}
|
|
return integers
|
|
}
|
|
|
|
/*
|
|
ints: list of BNs
|
|
*/
|
|
export function hexListFromBNs (bnList) {
|
|
const ret = []
|
|
for (const k in bnList) {
|
|
const v = bnList[k].toString(16)
|
|
ret.push('0x' + v.padStart(64, '0'))
|
|
}
|
|
return ret
|
|
}
|
|
|
|
export function toHexPaddedString(v: bigint | string): string {
|
|
if (v) {
|
|
if (typeof v === 'string') {
|
|
return v.startsWith('0x') ? v : '0x' + v
|
|
} else {
|
|
return '0x' + v.toString(16).padStart(64, '0')
|
|
}
|
|
}
|
|
else
|
|
return '0x' + '0'.padStart(64, '0')
|
|
}
|
|
|
|
/*
|
|
ints: ints: IntArray
|
|
*/
|
|
export function formatMemory (mem) {
|
|
const hexMem = bytesToHex(mem).substr(2)
|
|
const ret = []
|
|
for (let k = 0; k < hexMem.length; k += 32) {
|
|
const row = hexMem.substr(k, 32)
|
|
ret.push(row)
|
|
}
|
|
return ret
|
|
}
|
|
|
|
/*
|
|
Binary Search:
|
|
Assumes that @arg array is sorted increasingly
|
|
return largest i such that array[i] <= target; return -1 if array[0] > target || array is empty
|
|
*/
|
|
export function findLowerBound (target, array) {
|
|
let start = 0
|
|
let length = array.length
|
|
while (length > 0) {
|
|
const half = length >> 1
|
|
const middle = start + half
|
|
if (array[middle] <= target) {
|
|
length = length - 1 - half
|
|
start = middle + 1
|
|
} else {
|
|
length = half
|
|
}
|
|
}
|
|
return start - 1
|
|
}
|
|
|
|
/*
|
|
Binary Search:
|
|
Assumes that @arg array is sorted increasingly
|
|
return largest array[i] such that array[i] <= target; return null if array[0] > target || array is empty
|
|
*/
|
|
export function findLowerBoundValue (target, array) {
|
|
const index = findLowerBound(target, array)
|
|
return index >= 0 ? array[index] : null
|
|
}
|
|
|
|
/*
|
|
Binary Search:
|
|
Assumes that @arg array is sorted increasingly
|
|
return Return i such that |array[i] - target| is smallest among all i and -1 for an empty array.
|
|
Returns the smallest i for multiple candidates.
|
|
*/
|
|
export function findClosestIndex (target, array): number {
|
|
if (array.length === 0) {
|
|
return -1
|
|
}
|
|
const index = findLowerBound(target, array)
|
|
if (index < 0) {
|
|
return 0
|
|
} else if (index >= array.length - 1) {
|
|
return array.length - 1
|
|
} else {
|
|
const middle = (array[index] + array[index + 1]) / 2
|
|
return target <= middle ? index : index + 1
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
export function findCall (index, rootCall) {
|
|
const ret = buildCallPath(index, rootCall)
|
|
return ret[ret.length - 1]
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
export function buildCallPath (index, rootCall) {
|
|
const ret = []
|
|
findCallInternal(index, rootCall, ret)
|
|
return ret
|
|
}
|
|
|
|
/**
|
|
* sha3 the given @arg value (left pad to 32 bytes)
|
|
*
|
|
* @param {String} value - value to sha3
|
|
* @return {Object} - return sha3ied value
|
|
*/
|
|
// eslint-disable-next-line camelcase
|
|
export function sha3_256 (value) {
|
|
if ((value.constructor && value.constructor.name === 'BigNumber') || BN.isBN(value) || isBigInt(value)) {
|
|
value = value.toString(16)
|
|
}
|
|
if (typeof value === 'number') {
|
|
value = value.toString(16)
|
|
}
|
|
value = toBytes(addHexPrefix(value))
|
|
const retInBuffer: Uint8Array = hash.keccak(Buffer.from(setLengthLeft(value, 32)))
|
|
return bytesToHex(retInBuffer)
|
|
}
|
|
|
|
/**
|
|
* return a regex which extract the swarmhash from the bytecode.
|
|
*
|
|
* @return {RegEx}
|
|
*/
|
|
export function swarmHashExtraction () {
|
|
return /a165627a7a72305820([0-9a-f]{64})0029$/
|
|
}
|
|
|
|
/**
|
|
* return a regex which extract the swarmhash from the bytecode, from POC 0.3
|
|
*
|
|
* @return {RegEx}
|
|
*/
|
|
export function swarmHashExtractionPOC31 () {
|
|
return /a265627a7a72315820([0-9a-f]{64})64736f6c6343([0-9a-f]{6})0032$/
|
|
}
|
|
|
|
/**
|
|
* return a regex which extract the swarmhash from the bytecode, from POC 0.3
|
|
*
|
|
* @return {RegEx}
|
|
*/
|
|
export function swarmHashExtractionPOC32 () {
|
|
return /a265627a7a72305820([0-9a-f]{64})64736f6c6343([0-9a-f]{6})0032$/
|
|
}
|
|
|
|
/**
|
|
* return a regex which extract the cbor encoded metadata : {"ipfs": <IPFS hash>, "solc": <compiler version>} from the bytecode.
|
|
* ref https://solidity.readthedocs.io/en/v0.6.6/metadata.html?highlight=ipfs#encoding-of-the-metadata-hash-in-the-bytecode
|
|
* @return {RegEx}
|
|
*/
|
|
export function cborEncodedValueExtraction () {
|
|
return /64697066735822([0-9a-f]{68})64736f6c6343([0-9a-f]{6})0033$/
|
|
}
|
|
|
|
/**
|
|
* return a regex which extract the input parameters from the bytecode
|
|
*
|
|
* @return {RegEx}
|
|
*/
|
|
export function inputParametersExtraction () {
|
|
return /64697066735822[0-9a-f]{68}64736f6c6343[0-9a-f]{6}0033(.*)$/
|
|
}
|
|
|
|
export function extractcborMetadata (value) {
|
|
const cbor = value.match(cborEncodedValueExtraction())
|
|
if (cbor && cbor[0]) value = value.replace(cbor[0], '')
|
|
return value
|
|
}
|
|
|
|
export function extractSwarmHash (value) {
|
|
value = value.replace(swarmHashExtraction(), '')
|
|
value = value.replace(swarmHashExtractionPOC31(), '')
|
|
value = value.replace(swarmHashExtractionPOC32(), '')
|
|
return value
|
|
}
|
|
|
|
export function extractinputParameters (value) {
|
|
const inputsParam = getinputParameters(value)
|
|
if (inputsParam) value = value.replace(inputsParam, '')
|
|
return value
|
|
}
|
|
|
|
export function getinputParameters (value) {
|
|
const regex = value.match(inputParametersExtraction())
|
|
if (regex && regex[1]) {
|
|
return regex[1]
|
|
} else
|
|
return ''
|
|
}
|
|
|
|
/**
|
|
* Compare bytecode. return true if the code is equal (handle swarm hash and library references)
|
|
* @param {String} code1 - the bytecode that is actually deployed (contains resolved library reference and a potentially different swarmhash)
|
|
* @param {String} code2 - the bytecode generated by the compiler (contains unresolved library reference and a potentially different swarmhash)
|
|
this will return false if the generated bytecode is empty (abstract contract cannot be deployed)
|
|
*
|
|
* @return {bool}
|
|
*/
|
|
export function compareByteCode (code1, code2) {
|
|
if (code1 === code2) return true
|
|
if (code2 === '0x') return false // abstract contract. see comment
|
|
if (code1 === '0x00' || code2 === '0x00' && code1 !== code2) return false // // This can be removed some time once YUL returns correct bytecode
|
|
|
|
if (code2.substr(2, 46) === '7300000000000000000000000000000000000000003014') {
|
|
// testing the following signature: PUSH20 00..00 ADDRESS EQ
|
|
// in the context of a library, that slot contains the address of the library (pushed by the compiler to avoid calling library other than with a DELEGATECALL)
|
|
// if code2 is not a library, well we still suppose that the comparison remain relevant even if we remove some information from `code1`
|
|
code1 = replaceLibReference(code1, 4)
|
|
}
|
|
let pos = -1
|
|
while ((pos = code2.search(/__(.*)__/)) !== -1) {
|
|
code2 = replaceLibReference(code2, pos)
|
|
code1 = replaceLibReference(code1, pos)
|
|
}
|
|
|
|
code1 = removeImmutableReference(code1, code2)
|
|
code1 = extractinputParameters(code1)
|
|
code1 = extractSwarmHash(code1)
|
|
code1 = extractcborMetadata(code1)
|
|
code2 = extractinputParameters(code2)
|
|
code2 = extractSwarmHash(code2)
|
|
code2 = extractcborMetadata(code2)
|
|
|
|
if (code1 && code2) {
|
|
if (code1.length !== code2.length) {
|
|
// if the length isn't the same, we have an issue with extracting the metadata hash.
|
|
const minLength = code1.length > code2.length ? code2.length: code1.length
|
|
code1 = code1.substr(0, minLength - 10)
|
|
code2 = code2.substr(0, minLength - 10)
|
|
}
|
|
const compare = stringSimilarity.compareTwoStrings(code1, code2)
|
|
return compare == 1
|
|
}
|
|
|
|
return false
|
|
}
|
|
/* util extracted out from remix-ide. @TODO split this file, cause it mix real util fn with solidity related stuff ... */
|
|
export function groupBy (arr, key) {
|
|
return arr.reduce((sum, item) => {
|
|
const groupByVal = item[key]
|
|
const groupedItems = sum[groupByVal] || []
|
|
groupedItems.push(item)
|
|
sum[groupByVal] = groupedItems
|
|
return sum
|
|
}, {})
|
|
}
|
|
|
|
export function concatWithSeparator (list, separator) {
|
|
return list.reduce((sum, item) => sum + item + separator, '').slice(0, -separator.length)
|
|
}
|
|
|
|
export function escapeRegExp (str) {
|
|
return str.replace(/[-[\]/{}()+?.\\^$|]/g, '\\$&')
|
|
}
|
|
|
|
function replaceLibReference (code, pos) {
|
|
return code.substring(0, pos) + '0000000000000000000000000000000000000000' + code.substring(pos + 40)
|
|
}
|
|
|
|
function removeByIndex (code, index, length, emptyRef) {
|
|
if (!code) return code
|
|
return code.slice(0, index) + emptyRef + code.slice(index + length)
|
|
}
|
|
|
|
function removeImmutableReference (code1, code2) {
|
|
try {
|
|
const refOccurrence = code2.match(/7f0000000000000000000000000000000000000000000000000000000000000000/g)
|
|
if (!refOccurrence) return code1
|
|
let offset = 0
|
|
refOccurrence.map((value) => {
|
|
offset = code2.indexOf(value, offset)
|
|
code1 = removeByIndex(code1, offset, value.length, '7f0000000000000000000000000000000000000000000000000000000000000000')
|
|
offset = offset + 1
|
|
})
|
|
} catch (e) {
|
|
console.log('error removeImmutableReference', e)
|
|
}
|
|
return code1
|
|
}
|
|
|
|
function findCallInternal (index, rootCall, callsPath) {
|
|
const calls = Object.keys(rootCall.calls)
|
|
const ret = rootCall
|
|
callsPath.push(rootCall)
|
|
for (const k in calls) {
|
|
const subCall = rootCall.calls[calls[k]]
|
|
if (index >= subCall.start && index <= subCall.return) {
|
|
findCallInternal(index, subCall, callsPath)
|
|
break
|
|
}
|
|
}
|
|
return ret
|
|
}
|
|
|