simulator in a web worker

pull/3262/head^2
yann300 2 years ago committed by Aniket
parent 4405ac39ef
commit ae2bb4a41d
  1. 1
      apps/debugger/webpack.config.js
  2. 2
      apps/remix-ide/.babelrc
  3. 27
      apps/remix-ide/src/blockchain/providers/vm.js
  4. 37
      apps/remix-ide/src/blockchain/providers/worker-vm.ts
  5. 1
      babel.config.js
  6. 8
      libs/remix-debug/src/code/codeUtils.ts
  7. 55
      libs/remix-lib/src/execution/txRunnerVM.ts
  8. 8
      libs/remix-lib/src/execution/txRunnerWeb3.ts
  9. 5
      libs/remix-simulator/src/genesis.ts
  10. 10
      libs/remix-simulator/src/methods/blocks.ts
  11. 29
      libs/remix-simulator/src/vm-context.ts
  12. 17
      package.json
  13. 821
      yarn.lock

@ -2,6 +2,7 @@ const nxWebpack = require('@nrwl/react/plugins/webpack')
module.exports = config => { module.exports = config => {
const nxWebpackConfig = nxWebpack(config) const nxWebpackConfig = nxWebpack(config)
const webpackConfig = { const webpackConfig = {
...nxWebpackConfig, ...nxWebpackConfig,
resolve : { resolve : {

@ -2,7 +2,7 @@
"presets": ["@babel/preset-env", ["@babel/preset-react", "presets": ["@babel/preset-env", ["@babel/preset-react",
{"runtime": "automatic"} {"runtime": "automatic"}
]], ]],
"plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-transform-runtime"], "plugins": ["@babel/plugin-proposal-class-properties", "@babel/plugin-transform-runtime", "@babel/plugin-proposal-nullish-coalescing-operator"],
"ignore": [ "ignore": [
"**/node_modules/**" "**/node_modules/**"
] ]

@ -1,10 +1,10 @@
const Web3 = require('web3') const Web3 = require('web3')
const { BN, privateToAddress, hashPersonalMessage } = require('ethereumjs-util') const { BN, privateToAddress, hashPersonalMessage } = require('ethereumjs-util')
const { Provider, extend } = require('@remix-project/remix-simulator') const { extend } = require('@remix-project/remix-simulator')
class VMProvider { class VMProvider {
constructor (executionContext) { constructor (executionContext) {
this.executionContext = executionContext this.executionContext = executionContext
this.worker = null
} }
getAccounts (cb) { getAccounts (cb) {
@ -17,10 +17,27 @@ class VMProvider {
} }
resetEnvironment () { resetEnvironment () {
if (this.worker) this.worker.terminate()
this.accounts = {} this.accounts = {}
this.RemixSimulatorProvider = new Provider({ fork: this.executionContext.getCurrentFork() }) this.worker = new Worker(new URL('./worker-vm', import.meta.url))
this.RemixSimulatorProvider.init() this.worker.postMessage({ cmd: 'init', fork: this.executionContext.getCurrentFork() })
this.web3 = new Web3(this.RemixSimulatorProvider)
let incr = 0
const stamps = {}
this.worker.addEventListener('message', (msg) => {
if (stamps[msg.data.stamp]) {
stamps[msg.data.stamp](msg.data.error, msg.data.result)
}
})
const provider = {
sendAsync: (query, callback) => {
const stamp = Date.now() + incr
incr++
stamps[stamp] = callback
this.worker.postMessage({ cmd: 'sendAsync', query, stamp })
}
}
this.web3 = new Web3(provider)
extend(this.web3) extend(this.web3)
this.accounts = {} this.accounts = {}
this.executionContext.setWeb3('vm', this.web3) this.executionContext.setWeb3('vm', this.web3)

@ -0,0 +1,37 @@
import { Provider } from '@remix-project/remix-simulator'
let provider: Provider = null
self.onmessage = (e: MessageEvent) => {
const data = e.data
switch (data.cmd) {
case 'init':
{
provider = new Provider({ fork: data.fork })
if (provider) provider.init()
break
}
case 'sendAsync':
{
if (provider) {
provider.sendAsync(data.query, (error, result) => {
result = JSON.parse(JSON.stringify(result))
self.postMessage({
cmd: 'sendAsyncResult',
error,
result,
stamp: data.stamp
})
})
} else {
self.postMessage({
cmd: 'sendAsyncResult',
error: 'Provider not instanciated',
result: null,
stamp: data.stamp
})
}
break
}
}
}

@ -4,6 +4,7 @@ module.exports = {
"babel-plugin-replace-ts-export-assignment", "babel-plugin-replace-ts-export-assignment",
"@babel/plugin-transform-modules-commonjs", "@babel/plugin-transform-modules-commonjs",
"@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-nullish-coalescing-operator",
["@babel/plugin-proposal-private-property-in-object", { "loose": false }], ["@babel/plugin-proposal-private-property-in-object", { "loose": false }],
["@babel/plugin-proposal-private-methods", { "loose": false }]] ["@babel/plugin-proposal-private-methods", { "loose": false }]]
} }

@ -1,11 +1,11 @@
'use strict' 'use strict'
import Common from '@ethereumjs/common' import { Common } from '@ethereumjs/common'
import { getOpcodesForHF } from '@ethereumjs/vm/dist/evm/opcodes' import { getOpcodesForHF, OpcodeList } from '@ethereumjs/evm/dist/opcodes/codes'
import getOpcodes from './opcodes' import getOpcodes from './opcodes'
export function nameOpCodes (raw, hardfork) { export function nameOpCodes (raw, hardfork) {
const common = new Common({ chain: 'mainnet', hardfork }) const common = new Common({ chain: 'mainnet', hardfork })
const opcodes = getOpcodesForHF(common) const opcodes = getOpcodesForHF(common).opcodes
let pushData = '' let pushData = ''
const codeMap = {} const codeMap = {}
@ -47,7 +47,7 @@ type Opcode = {
*/ */
export function parseCode (raw) { export function parseCode (raw) {
const common = new Common({ chain: 'mainnet', hardfork: 'london' }) const common = new Common({ chain: 'mainnet', hardfork: 'london' })
const opcodes = getOpcodesForHF(common) const opcodes = getOpcodesForHF(common).opcodes
const code = [] const code = []
for (let i = 0; i < raw.length; i++) { for (let i = 0; i < raw.length; i++) {

@ -1,10 +1,21 @@
'use strict' 'use strict'
import { RunBlockResult, RunTxResult } from '@ethereumjs/vm'
import { Transaction, FeeMarketEIP1559Transaction } from '@ethereumjs/tx' import { Transaction, FeeMarketEIP1559Transaction } from '@ethereumjs/tx'
import { Block } from '@ethereumjs/block' import { Block } from '@ethereumjs/block'
import { BN, bufferToHex, Address } from 'ethereumjs-util' import { BN, bufferToHex, Address } from 'ethereumjs-util'
import type { Account } from '@ethereumjs/util'
import { EventManager } from '../eventManager' import { EventManager } from '../eventManager'
import { LogsManager } from './logsManager' import { LogsManager } from './logsManager'
export type VMxecutionResult = {
result: RunTxResult,
transactionHash: string
block: Block,
tx: Transaction
}
export type VMExecutionCallBack = (error: string | Error, result?: VMxecutionResult) => void
export class TxRunnerVM { export class TxRunnerVM {
event event
blockNumber blockNumber
@ -48,13 +59,13 @@ export class TxRunnerVM {
} }
try { try {
this.runInVm(args.from, args.to, data, args.value, args.gasLimit, args.useCall, args.timestamp, callback) this.runInVm(args.from, args.to, data, args.value, args.gasLimit, args.useCall, callback)
} catch (e) { } catch (e) {
callback(e, null) callback(e, null)
} }
} }
runInVm (from, to, data, value, gasLimit, useCall, timestamp, callback) { runInVm (from: string, to: string, data: string, value: string, gasLimit: number, useCall: boolean, callback: VMExecutionCallBack) {
const self = this const self = this
let account let account
if (!from && useCall && Object.keys(self.vmaccounts).length) { if (!from && useCall && Object.keys(self.vmaccounts).length) {
@ -65,30 +76,13 @@ export class TxRunnerVM {
if (!account) { if (!account) {
return callback('Invalid account selected') return callback('Invalid account selected')
} }
if (Number.isInteger(gasLimit)) {
gasLimit = '0x' + gasLimit.toString(16) this.getVMObject().stateManager.getAccount(Address.fromString(from)).then((res: Account) => {
}
this.getVMObject().stateManager.getAccount(Address.fromString(from)).then((res) => {
// See https://github.com/ethereumjs/ethereumjs-tx/blob/master/docs/classes/transaction.md#constructor
// for initialization fields and their types
if (!value) value = 0
if (typeof value === 'string') {
if (value.startsWith('0x')) value = new BN(value.replace('0x', ''), 'hex')
else {
try {
value = new BN(value, 10)
} catch (e) {
return callback('Unable to parse the value ' + e.message)
}
}
}
const EIP1559 = this.commonContext.hardfork() !== 'berlin' // berlin is the only pre eip1559 fork that we handle. const EIP1559 = this.commonContext.hardfork() !== 'berlin' // berlin is the only pre eip1559 fork that we handle.
let tx let tx
if (!EIP1559) { if (!EIP1559) {
tx = Transaction.fromTxData({ tx = Transaction.fromTxData({
nonce: useCall ? this.nextNonceForCall : new BN(res.nonce), nonce: useCall ? this.nextNonceForCall : res.nonce,
gasPrice: '0x1', gasPrice: '0x1',
gasLimit: gasLimit, gasLimit: gasLimit,
to: to, to: to,
@ -97,7 +91,7 @@ export class TxRunnerVM {
}, { common: this.commonContext }).sign(account.privateKey) }, { common: this.commonContext }).sign(account.privateKey)
} else { } else {
tx = FeeMarketEIP1559Transaction.fromTxData({ tx = FeeMarketEIP1559Transaction.fromTxData({
nonce: useCall ? this.nextNonceForCall : new BN(res.nonce), nonce: useCall ? this.nextNonceForCall : res.nonce,
maxPriorityFeePerGas: '0x01', maxPriorityFeePerGas: '0x01',
maxFeePerGas: '0x1', maxFeePerGas: '0x1',
gasLimit: gasLimit, gasLimit: gasLimit,
@ -109,15 +103,15 @@ export class TxRunnerVM {
if (useCall) this.nextNonceForCall++ if (useCall) this.nextNonceForCall++
const coinbases = ['0x0e9281e9c6a0808672eaba6bd1220e144c9bb07a', '0x8945a1288dc78a6d8952a92c77aee6730b414778', '0x94d76e24f818426ae84aa404140e8d5f60e10e7e'] const coinbases = ['0x0e9281e9c6a0808672eaba6bd1220e144c9bb07a', '0x8945a1288dc78a6d8952a92c77aee6730b414778', '0x94d76e24f818426ae84aa404140e8d5f60e10e7e']
const difficulties = [new BN('69762765929000', 10), new BN('70762765929000', 10), new BN('71762765929000', 10)] const difficulties = [69762765929000, 70762765929000, 71762765929000]
const block = Block.fromBlockData({ const block = Block.fromBlockData({
header: { header: {
timestamp: timestamp || (new Date().getTime() / 1000 | 0), timestamp: new Date().getTime() / 1000 | 0,
number: self.blockNumber, number: self.blockNumber,
coinbase: coinbases[self.blockNumber % coinbases.length], coinbase: coinbases[self.blockNumber % coinbases.length],
difficulty: difficulties[self.blockNumber % difficulties.length], difficulty: difficulties[self.blockNumber % difficulties.length],
gasLimit: new BN(gasLimit.replace('0x', ''), 16).imuln(2), gasLimit,
baseFeePerGas: EIP1559 ? '0x1' : undefined baseFeePerGas: EIP1559 ? '0x1' : undefined
}, },
transactions: [tx] transactions: [tx]
@ -141,14 +135,15 @@ export class TxRunnerVM {
} }
runBlockInVm (tx, block, callback) { runBlockInVm (tx, block, callback) {
this.getVMObject().vm.runBlock({ block: block, generate: true, skipBlockValidation: true, skipBalance: false, skipNonce: true }).then((results) => { this.getVMObject().vm.runBlock({ block: block, generate: true, skipBlockValidation: true, skipBalance: false, skipNonce: true }).then((results: RunBlockResult) => {
const result = results.results[0] const result = results.results[0]
if (result) { /*if (result) {
const status = result.execResult.exceptionError ? 0 : 1 const status = result.execResult.exceptionError ? 0 : 1
result.receipt.status
result.status = `0x${status}` result.status = `0x${status}`
} }*/
callback(null, { callback(null, {
result: result, result,
transactionHash: bufferToHex(Buffer.from(tx.hash())), transactionHash: bufferToHex(Buffer.from(tx.hash())),
block, block,
tx tx

@ -47,6 +47,10 @@ export class TxRunnerWeb3 {
} }
_sendTransaction (sendTx, tx, pass, callback) { _sendTransaction (sendTx, tx, pass, callback) {
var currentDateTime = new Date();
const start = currentDateTime.getTime() / 1000
console.log('start', start)
const cb = (err, resp) => { const cb = (err, resp) => {
if (err) { if (err) {
return callback(err, resp) return callback(err, resp)
@ -57,6 +61,10 @@ export class TxRunnerWeb3 {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
const receipt = await tryTillReceiptAvailable(resp, this.getWeb3()) const receipt = await tryTillReceiptAvailable(resp, this.getWeb3())
tx = await tryTillTxAvailable(resp, this.getWeb3()) tx = await tryTillTxAvailable(resp, this.getWeb3())
currentDateTime = new Date();
const end = currentDateTime.getTime() / 1000
console.log('end', end)
console.log('stopwatch', end - start)
resolve({ resolve({
receipt, receipt,
tx, tx,

@ -1,5 +1,4 @@
import { Block } from '@ethereumjs/block' import { Block } from '@ethereumjs/block'
import { BN } from 'ethereumjs-util'
export function generateBlock (vmContext) { export function generateBlock (vmContext) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -8,8 +7,8 @@ export function generateBlock (vmContext) {
timestamp: (new Date().getTime() / 1000 | 0), timestamp: (new Date().getTime() / 1000 | 0),
number: 0, number: 0,
coinbase: '0x0e9281e9c6a0808672eaba6bd1220e144c9bb07a', coinbase: '0x0e9281e9c6a0808672eaba6bd1220e144c9bb07a',
difficulty: new BN('69762765929000', 10), difficulty: 69762765929000,
gasLimit: new BN('8000000').imuln(1) gasLimit: 8000000
} }
}, { common: vmContext.vmObject().common }) }, { common: vmContext.vmObject().common })

@ -1,5 +1,7 @@
import Web3 from 'web3' import Web3 from 'web3'
import { VMContext } from '../vm-context' import { VMContext } from '../vm-context'
import { bigIntToHex } from '@ethereumjs/util'
export class Blocks { export class Blocks {
vmContext: VMContext vmContext: VMContext
coinbase: string coinbase: string
@ -47,14 +49,14 @@ export class Blocks {
if (receipt) { if (receipt) {
return { return {
blockHash: '0x' + block.hash().toString('hex'), blockHash: '0x' + block.hash().toString('hex'),
blockNumber: '0x' + block.header.number.toString('hex'), blockNumber: bigIntToHex(block.header.number),
from: receipt.from, from: receipt.from,
gas: Web3.utils.toHex(receipt.gas), gas: Web3.utils.toHex(receipt.gas),
chainId: '0xd05', chainId: '0xd05',
gasPrice: '0x4a817c800', // 20000000000 gasPrice: '0x4a817c800', // 20000000000
hash: receipt.transactionHash, hash: receipt.transactionHash,
input: receipt.input, input: receipt.input,
nonce: '0x' + tx.nonce.toString('hex'), nonce: bigIntToHex(tx.nonce),
transactionIndex: this.TX_INDEX, transactionIndex: this.TX_INDEX,
value: receipt.value === '0x' ? '0x0' : receipt.value, value: receipt.value === '0x' ? '0x0' : receipt.value,
to: receipt.to ? receipt.to : null to: receipt.to ? receipt.to : null
@ -101,14 +103,14 @@ export class Blocks {
if (receipt) { if (receipt) {
return { return {
blockHash: '0x' + block.hash().toString('hex'), blockHash: '0x' + block.hash().toString('hex'),
blockNumber: '0x' + block.header.number.toString('hex'), blockNumber: bigIntToHex(block.header.number),
from: receipt.from, from: receipt.from,
gas: Web3.utils.toHex(receipt.gas), gas: Web3.utils.toHex(receipt.gas),
chainId: '0xd05', chainId: '0xd05',
gasPrice: '0x4a817c800', // 20000000000 gasPrice: '0x4a817c800', // 20000000000
hash: receipt.transactionHash, hash: receipt.transactionHash,
input: receipt.input, input: receipt.input,
nonce: '0x' + tx.nonce.toString('hex'), nonce: bigIntToHex(tx.nonce),
transactionIndex: this.TX_INDEX, transactionIndex: this.TX_INDEX,
value: receipt.value === '0x' ? '0x0' : receipt.value, value: receipt.value === '0x' ? '0x0' : receipt.value,
to: receipt.to ? receipt.to : null to: receipt.to ? receipt.to : null

@ -5,18 +5,19 @@ import { rlp, keccak, bufferToHex } from 'ethereumjs-util'
import { execution } from '@remix-project/remix-lib' import { execution } from '@remix-project/remix-lib'
const { LogsManager } = execution const { LogsManager } = execution
import { VmProxy } from './VmProxy' import { VmProxy } from './VmProxy'
import VM from '@ethereumjs/vm' import { VM } from '@ethereumjs/vm'
import Common from '@ethereumjs/common' import { Common } from '@ethereumjs/common'
import StateManager from '@ethereumjs/vm/dist/state/stateManager' import { DefaultStateManager } from '@ethereumjs/statemanager'
import { StorageDump } from '@ethereumjs/vm/dist/state/interface' import { StorageDump } from '@ethereumjs/statemanager/dist/interface'
import { Block } from '@ethereumjs/block' import { Block } from '@ethereumjs/block'
import { Transaction } from '@ethereumjs/tx' import { Transaction } from '@ethereumjs/tx'
import { bigIntToHex } from '@ethereumjs/util'
/* /*
extend vm state manager and instanciate VM extend vm state manager and instanciate VM
*/ */
class StateManagerCommonStorageDump extends StateManager { class StateManagerCommonStorageDump extends DefaultStateManager {
keyHashes: { [key: string]: string } keyHashes: { [key: string]: string }
constructor () { constructor () {
super() super()
@ -52,17 +53,19 @@ class StateManagerCommonStorageDump extends StateManager {
}) })
} }
/*
async getStateRoot (force = false) { async getStateRoot (force = false) {
await this._cache.flush() await this._cache.flush()
const stateRoot = this._trie.root const stateRoot = this._trie.root
return stateRoot return stateRoot
} }
*/
async setStateRoot (stateRoot) { async setStateRoot (stateRoot) {
if (this._checkpointCount !== 0) { /*if (this._checkpointCount !== 0) {
throw new Error('Cannot set state root with uncommitted checkpoints') throw new Error('Cannot set state root with uncommitted checkpoints')
} }*/
await this._cache.flush() await this._cache.flush()
@ -106,7 +109,7 @@ export class VMContext {
this.blockGasLimitDefault = 4300000 this.blockGasLimitDefault = 4300000
this.blockGasLimit = this.blockGasLimitDefault this.blockGasLimit = this.blockGasLimitDefault
this.currentFork = fork || 'london' this.currentFork = fork || 'london'
this.currentVm = this.createVm(this.currentFork) this.createVm(this.currentFork).then((vm) => this.currentVm = vm )
this.blocks = {} this.blocks = {}
this.latestBlockNumber = "0x0" this.latestBlockNumber = "0x0"
this.blockByTxHash = {} this.blockByTxHash = {}
@ -115,14 +118,14 @@ export class VMContext {
this.logsManager = new LogsManager() this.logsManager = new LogsManager()
} }
createVm (hardfork) { async createVm (hardfork) {
const stateManager = new StateManagerCommonStorageDump() const stateManager = new StateManagerCommonStorageDump()
const common = new Common({ chain: 'mainnet', hardfork }) const common = new Common({ chain: 'mainnet', hardfork })
const vm = new VM({ const vm = await VM.create({
common, common,
activatePrecompiles: true, activatePrecompiles: true,
stateManager, stateManager
allowUnlimitedContractSize: true // allowUnlimitedContractSize: true
}) })
// VmProxy and VMContext are very intricated. // VmProxy and VMContext are very intricated.
@ -153,7 +156,7 @@ export class VMContext {
} }
addBlock (block: Block) { addBlock (block: Block) {
let blockNumber = '0x' + block.header.number.toString('hex') let blockNumber = bigIntToHex(block.header.number)
if (blockNumber === '0x') { if (blockNumber === '0x') {
blockNumber = '0x0' blockNumber = '0x0'
} }

@ -114,12 +114,16 @@
"build-contracts": "find ./node_modules/@openzeppelin/contracts | grep -i '.sol' > libs/remix-ui/editor/src/lib/providers/completion/contracts/contracts.txt && find ./node_modules/@uniswap/v3-core/contracts | grep -i '.sol' >> libs/remix-ui/editor/src/lib/providers/completion/contracts/contracts.txt" "build-contracts": "find ./node_modules/@openzeppelin/contracts | grep -i '.sol' > libs/remix-ui/editor/src/lib/providers/completion/contracts/contracts.txt && find ./node_modules/@uniswap/v3-core/contracts | grep -i '.sol' >> libs/remix-ui/editor/src/lib/providers/completion/contracts/contracts.txt"
}, },
"dependencies": { "dependencies": {
"@babel/plugin-proposal-class-properties": "^7.18.6", "@babel/plugin-proposal-class-properties": "^7.16.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6",
"@erebos/bzz-node": "^0.13.0", "@erebos/bzz-node": "^0.13.0",
"@ethereumjs/block": "^3.5.1", "@ethereumjs/block": "^4.0.0",
"@ethereumjs/common": "^2.5.0", "@ethereumjs/common": "^3.0.0",
"@ethereumjs/tx": "^3.3.2", "@ethereumjs/evm": "^1.0.0",
"@ethereumjs/vm": "^5.5.3", "@ethereumjs/statemanager": "^1.0.0",
"@ethereumjs/tx": "^4.0.0",
"@ethereumjs/util": "^8.0.0",
"@ethereumjs/vm": "^6.0.0",
"@ethersphere/bee-js": "^3.2.0", "@ethersphere/bee-js": "^3.2.0",
"@isomorphic-git/lightning-fs": "^4.4.1", "@isomorphic-git/lightning-fs": "^4.4.1",
"@monaco-editor/react": "4.4.5", "@monaco-editor/react": "4.4.5",
@ -146,7 +150,6 @@
"deep-equal": "^1.0.1", "deep-equal": "^1.0.1",
"document-register-element": "1.13.1", "document-register-element": "1.13.1",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"ethereumjs-util": "^7.0.10",
"ethers": "^5.4.2", "ethers": "^5.4.2",
"ethjs-util": "^0.1.6", "ethjs-util": "^0.1.6",
"express": "^4.18.2", "express": "^4.18.2",
@ -331,7 +334,7 @@
"style-loader": "^3.3.1", "style-loader": "^3.3.1",
"tap-spec": "^5.0.0", "tap-spec": "^5.0.0",
"tape": "^4.13.3", "tape": "^4.13.3",
"terser-webpack-plugin": "^4.2.3", "terser-webpack-plugin": "^5.3.6",
"timers-browserify": "^2.0.12", "timers-browserify": "^2.0.12",
"ts-jest": "^29.0.3", "ts-jest": "^29.0.3",
"ts-node": "10.9.1", "ts-node": "10.9.1",

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save