index.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. const BN = require('bn.js')
  2. const ethUtil = require('ethereumjs-util')
  3. const opcodes = require('./opcodes.js')
  4. const wastSyncInterface = require('./wasm/wast.json')
  5. const wastAsyncInterface = require('./wasm/wast-async.json')
  6. const wabt = require('wabt')
  7. // map to track dependent WASM functions
  8. const depMap = new Map([
  9. ['callback_256', ['bswap_m256']],
  10. ['callback_160', ['bswap_m160']],
  11. ['callback_128', ['bswap_m128']],
  12. ['bswap_m256', ['bswap_i64']],
  13. ['bswap_m128', ['bswap_i64']],
  14. ['bswap_m160', ['bswap_i64', 'bswap_i32']],
  15. ['keccak', ['memcpy', 'memset']],
  16. ['mod_320', ['iszero_320', 'gte_320']],
  17. ['mod_512', ['iszero_512', 'gte_512']],
  18. ['MOD', ['iszero_256', 'gte_256']],
  19. ['ADDMOD', ['mod_320']],
  20. ['MULMOD', ['mod_512']],
  21. ['SDIV', ['iszero_256', 'gte_256']],
  22. ['SMOD', ['iszero_256', 'gte_256']],
  23. ['DIV', ['iszero_256', 'gte_256']],
  24. ['EXP', ['iszero_256', 'mul_256']],
  25. ['MUL', ['mul_256']],
  26. ['ISZERO', ['iszero_256']],
  27. ['MSTORE', ['memusegas', 'bswap_m256', 'check_overflow']],
  28. ['MLOAD', ['memusegas', 'bswap_m256', 'check_overflow']],
  29. ['MSTORE8', ['memusegas', 'check_overflow']],
  30. ['CODECOPY', ['callback', 'memusegas', 'check_overflow', 'memset']],
  31. ['CALLDATALOAD', ['bswap_m256', 'bswap_i64', 'check_overflow']],
  32. ['CALLDATACOPY', ['memusegas', 'check_overflow', 'memset']],
  33. ['CALLVALUE', ['bswap_m128']],
  34. ['EXTCODECOPY', ['bswap_m256', 'callback', 'memusegas', 'check_overflow', 'memset']],
  35. ['EXTCODESIZE', ['callback_32', 'bswap_m256']],
  36. ['LOG', ['memusegas', 'check_overflow']],
  37. ['BLOCKHASH', ['check_overflow', 'callback_256']],
  38. ['SHA3', ['memusegas', 'bswap_m256', 'check_overflow', 'keccak']],
  39. ['CALL', ['bswap_m256', 'memusegas', 'check_overflow_i64', 'check_overflow', 'memset', 'callback_32']],
  40. ['DELEGATECALL', ['callback', 'memusegas', 'check_overflow_i64', 'check_overflow', 'memset']],
  41. ['CALLCODE', ['bswap_m256', 'callback', 'memusegas', 'check_overflow_i64', 'check_overflow', 'check_overflow_i64', 'memset', 'callback_32']],
  42. ['CREATE', ['bswap_m256', 'bswap_m160', 'callback_160', 'memusegas', 'check_overflow']],
  43. ['RETURN', ['memusegas', 'check_overflow']],
  44. ['BALANCE', ['bswap_m256', 'callback_128']],
  45. ['SELFDESTRUCT', ['bswap_m256']],
  46. ['SSTORE', ['bswap_m256', 'callback']],
  47. ['SLOAD', ['callback_256']],
  48. ['CODESIZE', ['callback_32']],
  49. ['DIFFICULTY', ['bswap_m256']],
  50. ['COINBASE', ['bswap_m160']],
  51. ['ORIGIN', ['bswap_m160']],
  52. ['ADDRESS', ['bswap_m160']],
  53. ['CALLER', ['bswap_m160']]
  54. ])
  55. // maps the async ops to their call back function
  56. const callbackFuncs = new Map([
  57. ['SSTORE', '$callback'],
  58. ['SLOAD', '$callback_256'],
  59. ['CREATE', '$callback_160'],
  60. ['CALL', '$callback_32'],
  61. ['DELEGATECALL', '$callback'],
  62. ['CALLCODE', '$callback_32'],
  63. ['EXTCODECOPY', '$callback'],
  64. ['EXTCODESIZE', '$callback_32'],
  65. ['CODECOPY', '$callback'],
  66. ['CODESIZE', '$callback_32'],
  67. ['BALANCE', '$callback_128'],
  68. ['BLOCKHASH', '$callback_256']
  69. ])
  70. /**
  71. * compiles evmCode to wasm in the binary format
  72. * @param {Array} evmCode
  73. * @param {Object} opts
  74. * @param {boolean} opts.stackTrace if `true` generates an runtime EVM stack trace (default: false)
  75. * @param {boolean} opts.inlineOps if `true` inlines the EVM1 operations (default: true)
  76. * @param {String} opts.testName is the name used for the wast file (default: 'temp')
  77. * @param {boolean} opts.chargePerOp if `true` adds metering statements for the wasm code section corresponding to each EVM opcode as opposed to metering once per branch segment (default: false).
  78. * @return {string}
  79. */
  80. exports.evm2wasm = function (evmCode, opts = {
  81. 'stackTrace': false,
  82. 'useAsyncAPI': false,
  83. 'inlineOps': true,
  84. 'testName': 'temp',
  85. 'chargePerOp': false
  86. }) {
  87. const wast = exports.evm2wast(evmCode, opts)
  88. const mod = wabt.parseWat('arbitraryModuleName', wast)
  89. mod.resolveNames()
  90. mod.validate()
  91. const bin = mod.toBinary({log: false, write_debug_names: false}).buffer
  92. mod.destroy()
  93. return Promise.resolve(bin)
  94. }
  95. /**
  96. * Transcompiles EVM code to ewasm in the sexpression text format. The EVM code
  97. * is broken into segments and each instruction in those segments is replaced
  98. * with a `call` to wasm function that does the equivalent operation. Each
  99. * opcode function takes in and returns the stack pointer.
  100. *
  101. * Segments are sections of EVM code in between flow control
  102. * opcodes (JUMPI. JUMP).
  103. * All segments start at
  104. * * the beginning for EVM code
  105. * * a GAS opcode
  106. * * a JUMPDEST opcode
  107. * * After a JUMPI opcode
  108. * @param {Integer} evmCode the evm byte code
  109. * @param {Object} opts
  110. * @param {boolean} opts.stackTrace if `true` generates a stack trace (default: false)
  111. * @param {boolean} opts.inlineOps if `true` inlines the EVM1 operations (default: true)
  112. * @param {boolean} opts.chargePerOp if `true` adds metering statements for the wasm code section corresponding to each EVM opcode as opposed to metering once per branch segment (default: false).
  113. * @return {string}
  114. */
  115. exports.evm2wast = function (evmCode, opts = {
  116. 'stackTrace': false,
  117. 'useAsyncAPI': false,
  118. 'inlineOps': true,
  119. 'chargePerOp': false
  120. }) {
  121. // adds stack height checks to the beginning of a segment
  122. function addStackCheck () {
  123. let check = ''
  124. if (segmentStackHigh !== 0) {
  125. check = `(if (i32.gt_s (get_global $sp) (i32.const ${(1023 - segmentStackHigh) * 32}))
  126. (then (unreachable)))`
  127. }
  128. if (segmentStackLow !== 0) {
  129. check += `(if (i32.lt_s (get_global $sp) (i32.const ${-segmentStackLow * 32 - 32}))
  130. (then (unreachable)))`
  131. }
  132. segment = check + segment
  133. segmentStackHigh = 0
  134. segmentStackLow = 0
  135. segmentStackDelta = 0
  136. }
  137. // add a metering statment at the beginning of a segment
  138. function addMetering () {
  139. if (!opts.chargePerOp) {
  140. wast += `(call $useGas (i64.const ${gasCount})) `
  141. }
  142. wast += segment
  143. segment = ''
  144. gasCount = 0
  145. }
  146. // finishes off a segment
  147. function endSegment () {
  148. segment += ')'
  149. addStackCheck()
  150. addMetering()
  151. }
  152. // this keep track of the opcode we have found so far. This will be used to
  153. // to figure out what .wast files to include
  154. const opcodesUsed = new Set()
  155. const ignoredOps = new Set(['JUMP', 'JUMPI', 'JUMPDEST', 'POP', 'STOP', 'INVALID'])
  156. let callbackTable = []
  157. // an array of found segments
  158. const jumpSegments = []
  159. // the transcompiled EVM code
  160. let wast = ''
  161. let segment = ''
  162. // keeps track of the gas that each section uses
  163. let gasCount = 0
  164. // used for pruning dead code
  165. let jumpFound = false
  166. // the accumlitive stack difference for the current segmnet
  167. let segmentStackDelta = 0
  168. let segmentStackHigh = 0
  169. let segmentStackLow = 0
  170. for (let pc = 0; pc < evmCode.length; pc++) {
  171. const opint = evmCode[pc]
  172. const op = opcodes(opint)
  173. // creates a stack trace
  174. if (opts.stackTrace) {
  175. segment += `(call $stackTrace (i32.const ${pc}) (i32.const ${opint}) (i32.const ${op.fee}) (get_global $sp))\n`
  176. }
  177. let bytes
  178. if (opts.chargePerOp) {
  179. segment += `(call $useGas (i64.const ${op.fee})) `
  180. }
  181. // do not charge gas for interface methods
  182. // TODO: implement proper gas charging and enable this here
  183. if (opint < 0x30 || (opint > 0x45 && opint < 0xa0)) {
  184. gasCount += op.fee
  185. }
  186. segmentStackDelta += op.on
  187. if (segmentStackDelta > segmentStackHigh) {
  188. segmentStackHigh = segmentStackDelta
  189. }
  190. segmentStackDelta -= op.off
  191. if (segmentStackDelta < segmentStackLow) {
  192. segmentStackLow = segmentStackDelta
  193. }
  194. switch (op.name) {
  195. case 'JUMP':
  196. jumpFound = true
  197. segment += `;; jump
  198. (set_local $jump_dest (call $check_overflow
  199. (i64.load (get_global $sp))
  200. (i64.load (i32.add (get_global $sp) (i32.const 8)))
  201. (i64.load (i32.add (get_global $sp) (i32.const 16)))
  202. (i64.load (i32.add (get_global $sp) (i32.const 24)))))
  203. (set_global $sp (i32.sub (get_global $sp) (i32.const 32)))
  204. (br $loop)`
  205. opcodesUsed.add('check_overflow')
  206. pc = findNextJumpDest(evmCode, pc)
  207. break
  208. case 'JUMPI':
  209. jumpFound = true
  210. segment += `(set_local $jump_dest (call $check_overflow
  211. (i64.load (get_global $sp))
  212. (i64.load (i32.add (get_global $sp) (i32.const 8)))
  213. (i64.load (i32.add (get_global $sp) (i32.const 16)))
  214. (i64.load (i32.add (get_global $sp) (i32.const 24)))))
  215. (set_global $sp (i32.sub (get_global $sp) (i32.const 64)))
  216. (br_if $loop (i32.eqz (i64.eqz (i64.or
  217. (i64.load (i32.add (get_global $sp) (i32.const 32)))
  218. (i64.or
  219. (i64.load (i32.add (get_global $sp) (i32.const 40)))
  220. (i64.or
  221. (i64.load (i32.add (get_global $sp) (i32.const 48)))
  222. (i64.load (i32.add (get_global $sp) (i32.const 56)))
  223. )
  224. )
  225. ))))\n`
  226. opcodesUsed.add('check_overflow')
  227. addStackCheck()
  228. addMetering()
  229. break
  230. case 'JUMPDEST':
  231. endSegment()
  232. jumpSegments.push({
  233. number: pc,
  234. type: 'jump_dest'
  235. })
  236. gasCount = 1
  237. break
  238. case 'GAS':
  239. segment += `(call $GAS)\n`
  240. // addMetering() // this causes an unreachable error in stackOverflowM1 -d 14
  241. break
  242. case 'LOG':
  243. segment += `(call $LOG (i32.const ${op.number}))\n`
  244. break
  245. case 'DUP':
  246. case 'SWAP':
  247. // adds the number on the stack to SWAP
  248. segment += `(call $${op.name} (i32.const ${op.number - 1}))\n`
  249. break
  250. case 'PC':
  251. segment += `(call $PC (i32.const ${pc}))\n`
  252. break
  253. case 'PUSH':
  254. pc++
  255. bytes = ethUtil.setLength(evmCode.slice(pc, pc += op.number), 32)
  256. const bytesRounded = Math.ceil(op.number / 8)
  257. let push = ''
  258. let q = 0
  259. // pad the remaining of the word with 0
  260. for (; q < 4 - bytesRounded; q++) {
  261. push = '(i64.const 0)' + push
  262. }
  263. for (; q < 4; q++) {
  264. const int64 = bytes2int64(bytes.slice(q * 8, q * 8 + 8))
  265. push = push + `(i64.const ${int64})`
  266. }
  267. segment += `(call $PUSH ${push})`
  268. pc--
  269. break
  270. case 'POP':
  271. // do nothing
  272. break
  273. case 'STOP':
  274. segment += '(br $done)'
  275. if (jumpFound) {
  276. pc = findNextJumpDest(evmCode, pc)
  277. } else {
  278. // the rest is dead code
  279. pc = evmCode.length
  280. }
  281. break
  282. case 'SELFDESTRUCT':
  283. case 'RETURN':
  284. segment += `(call $${op.name}) (br $done)\n`
  285. if (jumpFound) {
  286. pc = findNextJumpDest(evmCode, pc)
  287. } else {
  288. // the rest is dead code
  289. pc = evmCode.length
  290. }
  291. break
  292. case 'INVALID':
  293. segment = '(unreachable)'
  294. pc = findNextJumpDest(evmCode, pc)
  295. break
  296. default:
  297. if (opts.useAsyncAPI && callbackFuncs.has(op.name)) {
  298. const cbFunc = callbackFuncs.get(op.name)
  299. let index = callbackTable.indexOf(cbFunc)
  300. if (index === -1) {
  301. index = callbackTable.push(cbFunc) - 1
  302. }
  303. segment += `(call $${op.name} (i32.const ${index}))\n`
  304. } else {
  305. // use synchronous API
  306. segment += `(call $${op.name})\n`
  307. }
  308. }
  309. if (!ignoredOps.has(op.name)) {
  310. opcodesUsed.add(op.name)
  311. }
  312. const stackDelta = op.on - op.off
  313. // update the stack pointer
  314. if (stackDelta !== 0) {
  315. segment += `(set_global $sp (i32.add (get_global $sp) (i32.const ${stackDelta * 32})))\n`
  316. }
  317. // adds the logic to save the stack pointer before exiting to wiat to for a callback
  318. // note, this must be done before the sp is updated above^
  319. if (opts.useAsyncAPI && callbackFuncs.has(op.name)) {
  320. segment += `(set_global $cb_dest (i32.const ${jumpSegments.length + 1}))
  321. (br $done))`
  322. jumpSegments.push({
  323. type: 'cb_dest'
  324. })
  325. }
  326. }
  327. endSegment()
  328. wast = assembleSegments(jumpSegments) + wast + '))'
  329. let wastFiles = wastSyncInterface // default to synchronous interface
  330. if (opts.useAsyncAPI) {
  331. wastFiles = wastAsyncInterface
  332. }
  333. let imports = []
  334. let funcs = []
  335. // inline EVM opcode implemention
  336. if (opts.inlineOps) {
  337. [funcs, imports] = exports.resolveFunctions(opcodesUsed, wastFiles)
  338. }
  339. // import stack trace function
  340. if (opts.stackTrace) {
  341. imports.push('(import "debug" "printMemHex" (func $printMem (param i32 i32)))')
  342. imports.push('(import "debug" "print" (func $print (param i32)))')
  343. imports.push('(import "debug" "evmTrace" (func $stackTrace (param i32 i32 i32 i32)))')
  344. }
  345. imports.push('(import "ethereum" "useGas" (func $useGas (param i64)))')
  346. funcs.push(wast)
  347. wast = exports.buildModule(funcs, imports, callbackTable)
  348. return wast
  349. }
  350. // given an array for segments builds a wasm module from those segments
  351. // @param {Array} segments
  352. // @return {String}
  353. function assembleSegments (segments) {
  354. let wasm = buildJumpMap(segments)
  355. segments.forEach((seg, index) => {
  356. wasm = `(block $${index + 1} ${wasm}`
  357. })
  358. return `
  359. (func $main
  360. (export "main")
  361. (local $jump_dest i32) (local $jump_map_switch i32)
  362. (set_local $jump_dest (i32.const -1))
  363. (block $done
  364. (loop $loop
  365. ${wasm}`
  366. }
  367. // Builds the Jump map, which maps EVM jump location to a block label
  368. // @param {Array} segments
  369. // @return {String}
  370. function buildJumpMap (segments) {
  371. let wasm = '(unreachable)'
  372. let brTable = ''
  373. segments.forEach((seg, index) => {
  374. brTable += ' $' + (index + 1)
  375. if (seg.type === 'jump_dest') {
  376. wasm = `(if (i32.eq (get_local $jump_dest) (i32.const ${seg.number}))
  377. (then (br $${index + 1}))
  378. (else ${wasm}))`
  379. }
  380. })
  381. wasm = `
  382. (block $0
  383. (if
  384. (i32.eqz (get_global $init))
  385. (then
  386. (set_global $init (i32.const 1))
  387. (br $0))
  388. (else
  389. ;; the callback dest can never be in the first block
  390. (if (i32.eq (get_global $cb_dest) (i32.const 0))
  391. (then
  392. ${wasm}
  393. )
  394. (else
  395. ;; return callback destination and zero out $cb_dest
  396. (set_local $jump_map_switch (get_global $cb_dest))
  397. (set_global $cb_dest (i32.const 0))
  398. (br_table $0 ${brTable} (get_local $jump_map_switch))
  399. )))))`
  400. return wasm
  401. }
  402. // returns the index of the next jump destination opcode in given EVM code in an
  403. // array and a starting index
  404. // @param {Array} evmCode
  405. // @param {Integer} index
  406. // @return {Integer}
  407. function findNextJumpDest (evmCode, i) {
  408. for (; i < evmCode.length; i++) {
  409. const opint = evmCode[i]
  410. const op = opcodes(opint)
  411. switch (op.name) {
  412. case 'PUSH':
  413. // skip add how many bytes where pushed
  414. i += op.number
  415. break
  416. case 'JUMPDEST':
  417. return --i
  418. }
  419. }
  420. return --i
  421. }
  422. // converts 8 bytes into a int 64
  423. // @param {Integer}
  424. // @return {String}
  425. function bytes2int64 (bytes) {
  426. return new BN(bytes).fromTwos(64).toString()
  427. }
  428. // Ensure that dependencies are only imported once (use the Set)
  429. // @param {Set} funcSet a set of wasm function that need to be linked to their dependencies
  430. // @return {Set}
  431. function resolveFunctionDeps (funcSet) {
  432. let funcs = funcSet
  433. for (let func of funcSet) {
  434. const deps = depMap.get(func)
  435. if (deps) {
  436. for (const dep of deps) {
  437. funcs.add(dep)
  438. }
  439. }
  440. }
  441. return funcs
  442. }
  443. /**
  444. * given a Set of wasm function this return an array for wasm equivalents
  445. * @param {Set} funcSet
  446. * @return {Array}
  447. */
  448. exports.resolveFunctions = function (funcSet, wastFiles) {
  449. let funcs = []
  450. let imports = []
  451. for (let func of resolveFunctionDeps(funcSet)) {
  452. funcs.push(wastFiles[func].wast)
  453. imports.push(wastFiles[func].imports)
  454. }
  455. return [funcs, imports]
  456. }
  457. /**
  458. * builds a wasm module
  459. * @param {Array} funcs the function to include in the module
  460. * @param {Array} imports the imports for the module's import table
  461. * @return {string}
  462. */
  463. exports.buildModule = function (funcs, imports = [], callbacks = []) {
  464. let funcStr = ''
  465. for (let func of funcs) {
  466. funcStr += func
  467. }
  468. let callbackTableStr = ''
  469. if (callbacks.length) {
  470. callbackTableStr = `
  471. (table
  472. (export "callback") ;; name of table
  473. anyfunc
  474. (elem ${callbacks.join(' ')}) ;; elements will have indexes in order
  475. )`
  476. }
  477. return `
  478. (module
  479. ${imports.join('\n')}
  480. (global $cb_dest (mut i32) (i32.const 0))
  481. (global $sp (mut i32) (i32.const -32))
  482. (global $init (mut i32) (i32.const 0))
  483. ;; memory related global
  484. (global $memstart i32 (i32.const 33832))
  485. ;; the number of 256 words stored in memory
  486. (global $wordCount (mut i64) (i64.const 0))
  487. ;; what was charged for the last memory allocation
  488. (global $prevMemCost (mut i64) (i64.const 0))
  489. ;; TODO: memory should only be 1, but can't resize right now
  490. (memory 500)
  491. (export "memory" (memory 0))
  492. ${callbackTableStr}
  493. ${funcStr}
  494. )`
  495. }