Merge pull request #4529 from ethereum/vm-state

Save VM State and Blocks
pull/4551/head
David Disu 8 months ago committed by GitHub
commit 574c1c3254
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 115
      apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts
  2. 3
      apps/remix-ide/src/app/tabs/locales/en/settings.json
  3. 3
      apps/remix-ide/src/app/tabs/locales/es/settings.json
  4. 3
      apps/remix-ide/src/app/tabs/locales/fr/settings.json
  5. 3
      apps/remix-ide/src/app/tabs/locales/it/settings.json
  6. 3
      apps/remix-ide/src/app/tabs/locales/zh/settings.json
  7. 7
      apps/remix-ide/src/app/tabs/web3-provider.js
  8. 31
      apps/remix-ide/src/blockchain/blockchain.tsx
  9. 86
      apps/remix-ide/src/blockchain/execution-context.js
  10. 32
      apps/remix-ide/src/blockchain/providers/vm.ts
  11. 2
      apps/remix-ide/src/blockchain/providers/worker-vm.ts
  12. 1
      libs/remix-lib/src/execution/logsManager.ts
  13. 27
      libs/remix-lib/src/execution/txRunnerVM.ts
  14. 25
      libs/remix-simulator/src/methods/transactions.ts
  15. 35
      libs/remix-simulator/src/provider.ts
  16. 54
      libs/remix-simulator/src/vm-context.ts
  17. 23
      libs/remix-ui/settings/src/lib/remix-ui-settings.tsx
  18. 5
      libs/remix-ui/settings/src/lib/settingsAction.ts
  19. 18
      libs/remix-ui/settings/src/lib/settingsReducer.ts

@ -82,21 +82,21 @@ module.exports = {
instanceAddress = address
console.log('instanceAddress', instanceAddress)
browser
.waitForElementVisible(`#instance${instanceAddress} [data-id="instanceContractBal"]`)
.waitForElementVisible(`#instance${instanceAddress} [data-id="instanceContractBal"]`)
//*[@id="instance0xbBF289D846208c16EDc8474705C748aff07732dB" and contains(.,"Balance") and contains(.,'0.000000000000000111')]
.waitForElementVisible({
locateStrategy: 'xpath',
selector: `//*[@id="instance${instanceAddress}" and contains(.,"Balance") and contains(.,'0.000000000000000111')]`,
timeout: 60000
})
.waitForElementVisible({
locateStrategy: 'xpath',
selector: `//*[@id="instance${instanceAddress}" and contains(.,"Balance") and contains(.,'0.000000000000000111')]`,
timeout: 60000
})
//.waitForElementContainsText(`#instance${instanceAddress} [data-id="instanceContractBal"]`, 'Balance: 0.000000000000000111 ETH', 60000)
.clickFunction('sendSomeEther - transact (not payable)', { types: 'uint256 num', values: '2' })
.pause(1000)
.waitForElementVisible({
locateStrategy: 'xpath',
selector: `//*[@id="instance${instanceAddress}" and contains(.,"Balance") and contains(.,'0.000000000000000109')]`,
timeout: 60000
})
.clickFunction('sendSomeEther - transact (not payable)', { types: 'uint256 num', values: '2' })
.pause(1000)
.waitForElementVisible({
locateStrategy: 'xpath',
selector: `//*[@id="instance${instanceAddress}" and contains(.,"Balance") and contains(.,'0.000000000000000109')]`,
timeout: 60000
})
})
},
@ -238,6 +238,95 @@ module.exports = {
.executeScriptInTerminal('web3.eth.getAccounts()')
.journalLastChildIncludes('[ "0x76a3ABb5a12dcd603B52Ed22195dED17ee82708f" ]')
.end()
},
'Should ensure that save environment state is checked by default #group4 #group5': function (browser: NightwatchBrowser) {
browser.waitForElementPresent('*[data-id="remixIdeSidePanel"]')
.clickLaunchIcon('settings')
.waitForElementPresent('[data-id="settingsEnableSaveEnvStateLabel"]')
.scrollInto('[data-id="settingsEnableSaveEnvStateLabel"]')
.verify.elementPresent('[data-id="settingsEnableSaveEnvState"]:checked')
},
'Should deploy default storage contract; store value and ensure that state is saved. #group4 #group5': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('filePanel')
.click('*[data-id="treeViewLitreeViewItemcontracts"]')
.openFile('contracts/1_Storage.sol')
.pause(5000)
.clickLaunchIcon('udapp')
.waitForElementPresent('*[data-id="Deploy - transact (not payable)"]')
.click('*[data-id="Deploy - transact (not payable)"]')
.waitForElementPresent('#instance0xd9145CCE52D386f254917e481eB44e9943F39138')
.clickInstance(0)
.clickFunction('store - transact (not payable)', { types: 'uint256 num', values: '10' })
.clickFunction('retrieve - call')
.waitForElementContainsText('[data-id="treeViewLi0"]', 'uint256: 10')
.clickLaunchIcon('filePanel')
.openFile('.states/vm-shanghai/state.json')
.getEditorValue((content) => {
browser
.assert.ok(content.includes('"latestBlockNumber": "0x02"'), 'State is saved')
})
},
'Should load state after page refresh #group4': function (browser: NightwatchBrowser) {
browser.refreshPage()
.waitForElementVisible('*[data-id="remixIdeSidePanel"]')
.click('*[data-id="treeViewLitreeViewItemcontracts"]')
.openFile('contracts/1_Storage.sol')
.addAtAddressInstance('0xd9145CCE52D386f254917e481eB44e9943F39138', true, true, false)
.clickInstance(0)
.clickFunction('retrieve - call')
.waitForElementContainsText('[data-id="treeViewLi0"]', 'uint256: 10')
},
'Should save state after running web3 script #group4': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('settings')
.waitForElementPresent('[data-id="settingsTabGenerateContractMetadataLabel"]')
.click('[data-id="settingsTabGenerateContractMetadataLabel"]')
.verify.elementPresent('[data-id="settingsTabGenerateContractMetadata"]:checked')
.clickLaunchIcon('solidity')
.click('.remixui_compilerConfigSection')
.setValue('#evmVersionSelector', 'london')
.click('*[data-id="compilerContainerCompileBtn"]')
.pause(5000)
.clickLaunchIcon('udapp')
.switchEnvironment('vm-london')
.clickLaunchIcon('filePanel')
.click('*[data-id="treeViewLitreeViewItemscripts"]')
.openFile('scripts/deploy_with_web3.ts')
.click('[data-id="play-editor"]')
.waitForElementPresent('[data-id="treeViewDivDraggableItem.states/vm-london/state.json"]')
.click('[data-id="treeViewDivDraggableItem.states/vm-london/state.json"]')
.pause(100000)
.getEditorValue((content) => {
browser
.assert.ok(content.includes('"latestBlockNumber": "0x01"'), 'State is saved')
})
},
'Should ensure that .states is not updated when save env option is unchecked #group5': function (browser: NightwatchBrowser) {
browser
.clickLaunchIcon('settings')
.waitForElementPresent('[data-id="settingsEnableSaveEnvStateLabel"]')
.click('[data-id="settingsEnableSaveEnvStateLabel"]')
.verify.elementNotPresent('[data-id="settingsEnableSaveEnvState"]:checked')
.clickLaunchIcon('filePanel')
.openFile('contracts/1_Storage.sol')
.pause(5000)
.clickLaunchIcon('udapp')
.waitForElementPresent('*[data-id="Deploy - transact (not payable)"]')
.click('*[data-id="Deploy - transact (not payable)"]')
.pause(5000)
.clickLaunchIcon('filePanel')
.openFile('.states/vm-shanghai/state.json')
.getEditorValue((content) => {
browser
.assert.ok(content.includes('"latestBlockNumber": "0x02"'), 'State is unchanged')
})
.end()
}
}

@ -43,5 +43,6 @@
"settings.copilot": "Solidity copilot - Alpha",
"settings.copilot.activate": "Load & Activate copilot",
"settings.copilot.max_new_tokens": "Maximum number of words to generate",
"settings.copilot.temperature": "Temperature"
"settings.copilot.temperature": "Temperature",
"settings.enableSaveEnvState": "Save environment state"
}

@ -36,5 +36,6 @@
"settings.port": "PUERTO",
"settings.projectID": "ID DEL PROYECTO",
"settings.projectSecret": "SECRETO DE PROYECTO",
"settings.analyticsInRemix": "Analíticas en IDE Remix"
"settings.analyticsInRemix": "Analíticas en IDE Remix",
"settings.enableSaveEnvState": "Save environment state"
}

@ -36,5 +36,6 @@
"settings.port": "PORT",
"settings.projectID": "ID du projet",
"settings.projectSecret": "SECRET DU PROJET",
"settings.analyticsInRemix": "Analytics dans l'IDE de Remix"
"settings.analyticsInRemix": "Analytics dans l'IDE de Remix",
"settings.enableSaveEnvState": "Save environment state"
}

@ -36,5 +36,6 @@
"settings.port": "PORTA",
"settings.projectID": "ID PROGETTO",
"settings.projectSecret": "SEGRETO DEL PROGETTO",
"settings.analyticsInRemix": "Analytics nella Remix IDE"
"settings.analyticsInRemix": "Analytics nella Remix IDE",
"settings.enableSaveEnvState": "Save environment state"
}

@ -36,5 +36,6 @@
"settings.port": "端口",
"settings.projectID": "项目 ID",
"settings.projectSecret": "项目密钥",
"settings.analyticsInRemix": "Remix IDE 中的分析功能"
"settings.analyticsInRemix": "Remix IDE 中的分析功能",
"settings.enableSaveEnvState": "Save environment state"
}

@ -63,6 +63,13 @@ export class Web3ProviderModule extends Plugin {
await this.call('compilerArtefacts', 'addResolvedContract', contractAddressStr, data)
}
}, 50)
const isVM = this.blockchain.executionContext.isVM()
if (isVM && this.blockchain.config.get('settings/save-evm-state')) {
await this.blockchain.executionContext.getStateDetails().then((state) => {
this.call('fileManager', 'writeFile', `.states/${this.blockchain.executionContext.getProvider()}/state.json`, state)
})
}
}
}
resolve(message)

@ -135,7 +135,8 @@ export class Blockchain extends Plugin {
setupEvents() {
this.executionContext.event.register('contextChanged', async (context) => {
await this.resetEnvironment()
// reset environment to last known state of the context
await this.loadContext(context)
this._triggerEvent('contextChanged', [context])
this.detectNetwork((error, network) => {
this.networkStatus = {network, error}
@ -643,8 +644,23 @@ export class Blockchain extends Plugin {
})
}
async resetEnvironment() {
await this.getCurrentProvider().resetEnvironment()
async loadContext(context: string) {
const saveEvmState = this.config.get('settings/save-evm-state')
if (saveEvmState) {
const contextExists = await this.call('fileManager', 'exists', `.states/${context}/state.json`)
if (contextExists) {
const stateDb = await this.call('fileManager', 'readFile', `.states/${context}/state.json`)
await this.getCurrentProvider().resetEnvironment(stateDb)
} else {
await this.getCurrentProvider().resetEnvironment()
}
} else {
await this.getCurrentProvider().resetEnvironment()
}
// TODO: most params here can be refactored away in txRunner
const web3Runner = new TxRunnerWeb3(
{
@ -677,7 +693,7 @@ export class Blockchain extends Plugin {
view on etherscan
</a>
)
}
}
})
})
this.txRunner = new TxRunner(web3Runner, {})
@ -889,8 +905,13 @@ export class Blockchain extends Plugin {
let execResult
let returnValue = null
if (isVM) {
const hhlogs = await this.web3().remix.getHHLogsForTx(txResult.transactionHash)
if (!tx.useCall && this.config.get('settings/save-evm-state')) {
await this.executionContext.getStateDetails().then((state) => {
this.call('fileManager', 'writeFile', `.states/${this.executionContext.getProvider()}/state.json`, state)
})
}
const hhlogs = await this.web3().remix.getHHLogsForTx(txResult.transactionHash)
if (hhlogs && hhlogs.length) {
const finalLogs = (
<div>

@ -3,6 +3,7 @@
import Web3 from 'web3'
import { execution } from '@remix-project/remix-lib'
import EventManager from '../lib/events'
import {bufferToHex} from '@ethereumjs/util'
const _paq = window._paq = window._paq || []
let web3
@ -71,35 +72,44 @@ export class ExecutionContext {
}
detectNetwork (callback) {
if (this.isVM()) {
callback(null, { id: '-', name: 'VM' })
} else {
if (!web3.currentProvider) {
return callback('No provider set')
}
const cb = (err, id) => {
let name = null
if (err) name = 'Unknown'
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
else if (id === 1) name = 'Main'
else if (id === 3) name = 'Ropsten'
else if (id === 4) name = 'Rinkeby'
else if (id === 5) name = 'Goerli'
else if (id === 42) name = 'Kovan'
else if (id === 11155111) name = 'Sepolia'
else name = 'Custom'
if (id === 1) {
web3.eth.getBlock(0).then((block) => {
if (block && block.hash !== this.mainNetGenesisHash) name = 'Custom'
callback(err, { id, name, lastBlock: this.lastBlock, currentFork: this.currentFork })
}).catch((error) => callback(error))
} else {
callback(err, { id, name, lastBlock: this.lastBlock, currentFork: this.currentFork })
return new Promise((resolve, reject) => {
if (this.isVM()) {
callback && callback(null, { id: '-', name: 'VM' })
return resolve({ id: '-', name: 'VM' })
} else {
if (!web3.currentProvider) {
callback && callback('No provider set')
return reject('No provider set')
}
const cb = (err, id) => {
let name = null
if (err) name = 'Unknown'
// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
else if (id === 1) name = 'Main'
else if (id === 3) name = 'Ropsten'
else if (id === 4) name = 'Rinkeby'
else if (id === 5) name = 'Goerli'
else if (id === 42) name = 'Kovan'
else if (id === 11155111) name = 'Sepolia'
else name = 'Custom'
if (id === 1) {
web3.eth.getBlock(0).then((block) => {
if (block && block.hash !== this.mainNetGenesisHash) name = 'Custom'
callback && callback(err, { id, name, lastBlock: this.lastBlock, currentFork: this.currentFork })
return resolve({ id, name, lastBlock: this.lastBlock, currentFork: this.currentFork })
}).catch((error) => {
callback && callback(error)
return reject(error)
})
} else {
callback && callback(err, { id, name, lastBlock: this.lastBlock, currentFork: this.currentFork })
return resolve({ id, name, lastBlock: this.lastBlock, currentFork: this.currentFork })
}
}
web3.eth.net.getId().then(id=>cb(null,parseInt(id))).catch(err=>cb(err))
}
web3.eth.net.getId().then(id=>cb(null,parseInt(id))).catch(err=>cb(err))
}
})
}
removeProvider (name) {
@ -195,4 +205,26 @@ export class ExecutionContext {
return transactionDetailsLinks[network] + hash
}
}
async getStateDetails() {
const db = await this.web3().remix.getStateDb()
const blocksData = await this.web3().remix.getBlocksData()
const state = {
db: Object.fromEntries(db._database),
blocks: blocksData.blocks,
latestBlockNumber: blocksData.latestBlockNumber
}
const stringifyed = JSON.stringify(state, (key, value) => {
if (key === 'db') {
return value
} else if (key === 'blocks') {
return value.map(block => bufferToHex(block))
}else if (key === '') {
return value
}
return bufferToHex(value)
}, '\t')
return stringifyed
}
}

@ -2,6 +2,7 @@ import Web3, { FMT_BYTES, FMT_NUMBER, LegacySendAsyncProvider } from 'web3'
import { fromWei, toBigInt } from 'web3-utils'
import { privateToAddress, hashPersonalMessage, isHexString } from '@ethereumjs/util'
import { extend, JSONRPCRequestPayload, JSONRPCResponseCallback } from '@remix-project/remix-simulator'
import {toBuffer} from '@ethereumjs/util'
import { ExecutionContext } from '../execution-context'
export class VMProvider {
@ -12,9 +13,7 @@ export class VMProvider {
sendAsync: (query: JSONRPCRequestPayload, callback: JSONRPCResponseCallback) => void
}
newAccountCallback: {[stamp: number]: (error: Error, address: string) => void}
constructor (executionContext: ExecutionContext) {
this.executionContext = executionContext
this.worker = null
this.provider = null
@ -29,7 +28,7 @@ export class VMProvider {
})
}
async resetEnvironment () {
async resetEnvironment (stringifiedState?: string) {
if (this.worker) this.worker.terminate()
this.worker = new Worker(new URL('./worker-vm', import.meta.url))
const provider = this.executionContext.getProviderObject()
@ -76,10 +75,35 @@ export class VMProvider {
}
}
})
this.worker.postMessage({ cmd: 'init', fork: this.executionContext.getCurrentFork(), nodeUrl: provider?.options['nodeUrl'], blockNumber: provider?.options['blockNumber']})
if (stringifiedState) {
try {
const blockchainState = JSON.parse(stringifiedState)
const blockNumber = parseInt(blockchainState.latestBlockNumber, 16)
const stateDb = blockchainState.db
this.worker.postMessage({
cmd: 'init',
fork: this.executionContext.getCurrentFork(),
nodeUrl: provider?.options['nodeUrl'],
blockNumber,
stateDb,
blocks: blockchainState.blocks
})
} catch (e) {
console.error(e)
}
} else {
this.worker.postMessage({
cmd: 'init',
fork: this.executionContext.getCurrentFork(),
nodeUrl: provider?.options['nodeUrl'],
blockNumber: provider?.options['blockNumber']
})
}
})
}
// TODO: is still here because of the plugin API
// can be removed later when we update the API
createVMAccount (newAccount) {

@ -6,7 +6,7 @@ self.onmessage = (e: MessageEvent) => {
switch (data.cmd) {
case 'init':
{
provider = new Provider({ fork: data.fork, nodeUrl: data.nodeUrl, blockNumber: data.blockNumber })
provider = new Provider({ fork: data.fork, nodeUrl: data.nodeUrl, blockNumber: data.blockNumber, stateDb: data.stateDb, blocks: data.blocks})
provider.init().then(() => {
self.postMessage({
cmd: 'initiateResult',

@ -21,6 +21,7 @@ export class LogsManager {
eachOf(block.transactions, (tx: any, i, next) => {
const txHash = '0x' + tx.hash().toString('hex')
web3.eth.getTransactionReceipt(txHash, (_error, receipt) => {
if (!receipt) return next()
for (const log of receipt.logs) {
this.oldLogs.push({ type: 'block', blockNumber, block, tx, log, txNumber: i, receipt })
const subscriptions = this.getSubscriptionsFor({ type: 'block', blockNumber, block, tx, log, receipt})

@ -24,24 +24,23 @@ export class TxRunnerVM {
pendingTxs
vmaccounts
queusTxs
blocks
blocks: Buffer[]
logsManager
commonContext
blockParentHash
nextNonceForCall: number
getVMObject: () => any
constructor (vmaccounts, api, getVMObject, blockNumber) {
constructor (vmaccounts, api, getVMObject, blocks: Buffer[] = []) {
this.event = new EventManager()
this.logsManager = new LogsManager()
// has a default for now for backwards compatibility
this.getVMObject = getVMObject
this.commonContext = this.getVMObject().common
this.blockNumber = blockNumber || 0
this.blockNumber = Array.isArray(blocks) ? blocks.length : 0 // TODO: this should be set to the fetched block number count
this.pendingTxs = {}
this.vmaccounts = vmaccounts
this.queusTxs = []
this.blocks = []
/*
txHash is generated using the nonce,
in order to have unique transaction hash, we need to keep using different nonce (in case of a call)
@ -51,7 +50,15 @@ export class TxRunnerVM {
this.nextNonceForCall = 0
const vm = this.getVMObject().vm
this.blockParentHash = vm.blockchain.genesisBlock.hash()
if (Array.isArray(blocks) && (blocks || []).length > 0) {
const lastBlock = Block.fromRLPSerializedBlock(blocks[blocks.length - 1], { common: this.commonContext })
this.blockParentHash = lastBlock.hash()
this.blocks = blocks
} else {
this.blockParentHash = vm.blockchain.genesisBlock.hash()
this.blocks = [vm.blockchain.genesisBlock.serialize()]
}
}
execute (args: InternalTransaction, confirmationCb, gasEstimationForceSend, promptCb, callback: VMExecutionCallBack) {
@ -106,8 +113,7 @@ export class TxRunnerVM {
const coinbases = ['0x0e9281e9c6a0808672eaba6bd1220e144c9bb07a', '0x8945a1288dc78a6d8952a92c77aee6730b414778', '0x94d76e24f818426ae84aa404140e8d5f60e10e7e']
const difficulties = [69762765929000, 70762765929000, 71762765929000]
const difficulty = this.commonContext.consensusType() === ConsensusType.ProofOfStake ? 0 : difficulties[this.blockNumber % difficulties.length]
const blocknumber = this.blockNumber + 1
const blocknumber = this.blocks.length
const block = Block.fromBlockData({
header: {
timestamp: new Date().getTime() / 1000 | 0,
@ -122,10 +128,13 @@ export class TxRunnerVM {
}, { common: this.commonContext, hardforkByBlockNumber: false, hardforkByTTD: undefined })
if (!useCall) {
this.blockNumber = this.blockNumber + 1
this.blockNumber = blocknumber
this.blockParentHash = block.hash()
this.runBlockInVm(tx, block, (err, result) => {
if (!err) this.getVMObject().vm.blockchain.putBlock(block)
if (!err) {
this.getVMObject().vm.blockchain.putBlock(block)
this.blocks.push(block.serialize())
}
callback(err, result)
})
} else {

@ -4,7 +4,7 @@ import { processTx } from './txProcess'
import { execution } from '@remix-project/remix-lib'
import { ethers } from 'ethers'
import { VMexecutionResult } from '@remix-project/remix-lib'
import { RunTxResult } from '@ethereumjs/vm'
import { VMContext } from '../vm-context'
import { Log, EvmError } from '@ethereumjs/evm'
const TxRunnerVM = execution.TxRunnerVM
const TxRunner = execution.TxRunner
@ -19,7 +19,7 @@ export type VMExecResult = {
}
export class Transactions {
vmContext
vmContext: VMContext
accounts
tags
txRunnerVMInstance
@ -32,7 +32,7 @@ export class Transactions {
this.tags = {}
}
init (accounts, blockNumber) {
init (accounts, blocksData: Buffer[]) {
this.accounts = accounts
const api = {
logMessage: (msg) => {
@ -55,11 +55,11 @@ export class Transactions {
}
}
this.txRunnerVMInstance = new TxRunnerVM(accounts, api, _ => this.vmContext.vmObject(), blockNumber)
this.txRunnerVMInstance = new TxRunnerVM(accounts, api, _ => this.vmContext.vmObject(), blocksData)
this.txRunnerInstance = new TxRunner(this.txRunnerVMInstance, {})
this.txRunnerInstance.vmaccounts = accounts
}
methods () {
return {
eth_sendTransaction: this.eth_sendTransaction.bind(this),
@ -74,7 +74,9 @@ export class Transactions {
eth_getExecutionResultFromSimulator: this.eth_getExecutionResultFromSimulator.bind(this),
eth_getHHLogsForTx: this.eth_getHHLogsForTx.bind(this),
eth_getHashFromTagBySimulator: this.eth_getHashFromTagBySimulator.bind(this),
eth_registerCallId: this.eth_registerCallId.bind(this)
eth_registerCallId: this.eth_registerCallId.bind(this),
eth_getStateDb: this.eth_getStateDb.bind(this),
eth_getBlocksData: this.eth_getBlocksData.bind(this)
}
}
@ -198,6 +200,17 @@ export class Transactions {
cb()
}
eth_getStateDb (_, cb) {
cb(null, this.vmContext.currentVm.stateManager.getDb())
}
eth_getBlocksData (_, cb) {
cb(null, {
blocks: this.txRunnerVMInstance.blocks,
latestBlockNumber: this.txRunnerVMInstance.blockNumber
})
}
eth_call (payload, cb) {
// from might be lowercased address (web3)
if (payload.params && payload.params.length > 0 && payload.params[0].from) {

@ -11,6 +11,7 @@ import { Transactions } from './methods/transactions'
import { Debug } from './methods/debug'
import { VMContext } from './vm-context'
import { Web3PluginBase } from 'web3'
import { Block } from '@ethereumjs/block'
export interface JSONRPCRequestPayload {
params: any[];
@ -27,8 +28,20 @@ export interface JSONRPCResponsePayload {
export type JSONRPCResponseCallback = (err: Error, result?: JSONRPCResponsePayload) => void
export type State = Record<string, string>
export type ProviderOptions = {
fork?: string,
nodeUrl?: string,
blockNumber?: number | 'latest',
stateDb?: State,
logDetails?: boolean
blocks?: string[],
coinbase?: string
}
export class Provider {
options: Record<string, string | number>
options: ProviderOptions
vmContext
Accounts
Transactions
@ -37,10 +50,10 @@ export class Provider {
initialized: boolean
pendingRequests: Array<any>
constructor (options: Record<string, string | number> = {}) {
constructor (options: ProviderOptions = {} as ProviderOptions) {
this.options = options
this.connected = true
this.vmContext = new VMContext(options['fork'] as string, options['nodeUrl'] as string, options['blockNumber'] as (number | 'latest'))
this.vmContext = new VMContext(options['fork'], options['nodeUrl'], options['blockNumber'], options['stateDb'], options['blocks'])
this.Accounts = new Web3Accounts(this.vmContext)
this.Transactions = new Transactions(this.vmContext)
@ -60,7 +73,7 @@ export class Provider {
this.pendingRequests = []
await this.vmContext.init()
await this.Accounts.resetAccounts()
this.Transactions.init(this.Accounts.accounts, this.vmContext.blockNumber)
this.Transactions.init(this.Accounts.accounts, this.vmContext.serializedBlocks)
this.initialized = true
if (this.pendingRequests.length > 0) {
this.pendingRequests.map((req) => {
@ -168,4 +181,18 @@ class Web3TestPlugin extends Web3PluginBase {
params: [id]
})
}
public getStateDb() {
return this.requestManager.send({
method: 'eth_getStateDb',
params: []
})
}
public getBlocksData() {
return this.requestManager.send({
method: 'eth_getBlocksData',
params: []
})
}
}

@ -2,7 +2,7 @@
'use strict'
import { Cache } from '@ethereumjs/statemanager/dist/cache'
import { hash } from '@remix-project/remix-lib'
import { bufferToHex, Account, toBuffer, bufferToBigInt} from '@ethereumjs/util'
import { bufferToHex, Account, toBuffer, bufferToBigInt, bigIntToHex } from '@ethereumjs/util'
import { keccak256 } from 'ethereum-cryptography/keccak'
import type { Address } from '@ethereumjs/util'
import { decode } from 'rlp'
@ -13,7 +13,7 @@ import { VmProxy } from './VmProxy'
import { VM } from '@ethereumjs/vm'
import type { BigIntLike } from '@ethereumjs/util'
import { Common, ConsensusType } from '@ethereumjs/common'
import { Trie } from '@ethereumjs/trie'
import { Trie, MapDB } from '@ethereumjs/trie'
import { DefaultStateManager, StateManager, EthersStateManager, EthersStateManagerOpts } from '@ethereumjs/statemanager'
import { StorageDump } from '@ethereumjs/statemanager/dist/interface'
import { EVM } from '@ethereumjs/evm'
@ -21,7 +21,7 @@ import { EEI } from '@ethereumjs/vm'
import { Blockchain } from '@ethereumjs/blockchain'
import { Block } from '@ethereumjs/block'
import { Transaction } from '@ethereumjs/tx'
import { bigIntToHex } from '@ethereumjs/util'
import { State } from './provider'
/**
* Options for constructing a {@link StateManager}.
@ -50,6 +50,11 @@ class StateManagerCommonStorageDump extends DefaultStateManager {
this.keyHashes = {}
}
getDb () {
// @ts-ignore
return this._trie.database().db
}
putContractStorage (address, key, value) {
this.keyHashes[hash.keccak(key).toString('hex')] = bufferToHex(key)
return super.putContractStorage(address, key, value)
@ -100,7 +105,6 @@ export interface CustomEthersStateManagerOpts {
class CustomEthersStateManager extends StateManagerCommonStorageDump {
private provider: ethers.providers.StaticJsonRpcProvider | ethers.providers.JsonRpcProvider
private blockTag: string
constructor(opts: CustomEthersStateManagerOpts) {
super(opts)
if (typeof opts.provider === 'string') {
@ -258,7 +262,7 @@ class CustomEthersStateManager extends StateManagerCommonStorageDump {
export type CurrentVm = {
vm: VM,
web3vm: VmProxy,
stateManager: StateManager,
stateManager: StateManagerCommonStorageDump,
common: Common
}
@ -298,12 +302,16 @@ export class VMContext {
exeResults: Record<string, Transaction>
nodeUrl: string
blockNumber: number | 'latest'
stateDb: State
rawBlocks: string[]
serializedBlocks: Buffer[]
constructor (fork?: string, nodeUrl?: string, blockNumber?: number | 'latest') {
constructor (fork?: string, nodeUrl?: string, blockNumber?: number | 'latest', stateDb?: State, blocksData?: string[]) {
this.blockGasLimitDefault = 4300000
this.blockGasLimit = this.blockGasLimitDefault
this.currentFork = fork || 'merge'
this.nodeUrl = nodeUrl
this.stateDb = stateDb
this.blockNumber = blockNumber
this.blocks = {}
this.latestBlockNumber = "0x0"
@ -311,6 +319,8 @@ export class VMContext {
this.txByHash = {}
this.exeResults = {}
this.logsManager = new LogsManager()
this.rawBlocks = blocksData
this.serializedBlocks = []
}
async init () {
@ -318,7 +328,7 @@ export class VMContext {
}
async createVm (hardfork) {
let stateManager: StateManager
let stateManager: StateManagerCommonStorageDump
if (this.nodeUrl) {
let block = this.blockNumber
if (this.blockNumber === 'latest') {
@ -335,15 +345,25 @@ export class VMContext {
blockTag: '0x' + this.blockNumber.toString(16)
})
}
} else{
const db = this.stateDb ? new Map(Object.entries(this.stateDb).map(([k, v]) => [k, toBuffer(v)])) : new Map()
const mapDb = new MapDB(db)
const trie = await Trie.create({ useKeyHashing: true, db: mapDb, useRootPersistence: true })
} else
stateManager = new StateManagerCommonStorageDump()
stateManager = new StateManagerCommonStorageDump({ trie })
}
const consensusType = hardfork === 'berlin' || hardfork === 'london' ? ConsensusType.ProofOfWork : ConsensusType.ProofOfStake
const difficulty = consensusType === ConsensusType.ProofOfStake ? 0 : 69762765929000
const common = new VMCommon({ chain: 'mainnet', hardfork })
const genesisBlock: Block = Block.fromBlockData({
const blocks = (this.rawBlocks || []).map(block => {
const serializedBlock = toBuffer(block)
this.serializedBlocks.push(serializedBlock)
return Block.fromRLPSerializedBlock(serializedBlock, { common })
})
const genesisBlock: Block = blocks.length > 0 && (blocks[0] || {}).isGenesis ? blocks[0] : Block.fromBlockData({
header: {
timestamp: (new Date().getTime() / 1000 | 0),
number: 0,
@ -352,7 +372,6 @@ export class VMContext {
gasLimit: 8000000
}
}, { common, hardforkByBlockNumber: false, hardforkByTTD: undefined })
const blockchain = await Blockchain.create({ common, validateBlocks: false, validateConsensus: false, genesisBlock })
const eei = new EEI(stateManager, common, blockchain)
const evm = new EVM({ common, eei, allowUnlimitedContractSize: true })
@ -365,13 +384,17 @@ export class VMContext {
blockchain,
evm
})
// VmProxy and VMContext are very intricated.
// VmProxy is used to track the EVM execution (to listen on opcode execution, in order for instance to generate the VM trace)
const web3vm = new VmProxy(this)
web3vm.setVM(vm)
this.addBlock(genesisBlock, true)
return { vm, web3vm, stateManager, common }
if (blocks.length > 0) blocks.splice(0, 1)
blocks.forEach(block => {
blockchain.putBlock(block)
this.addBlock(block, false, false, web3vm)
})
return { vm, web3vm, stateManager, common, blocks }
}
getCurrentFork () {
@ -390,7 +413,7 @@ export class VMContext {
return this.currentVm
}
addBlock (block: Block, genesis?: boolean, isCall?: boolean) {
addBlock (block: Block, genesis?: boolean, isCall?: boolean, web3vm?: VmProxy) {
let blockNumber = bigIntToHex(block.header.number)
if (blockNumber === '0x') {
blockNumber = '0x0'
@ -400,7 +423,8 @@ export class VMContext {
this.blocks[blockNumber] = block
this.latestBlockNumber = blockNumber
if (!isCall && !genesis) this.logsManager.checkBlock(blockNumber, block, this.web3())
if (!isCall && !genesis && web3vm) this.logsManager.checkBlock(blockNumber, block, web3vm)
if (!isCall && !genesis && !web3vm) this.logsManager.checkBlock(blockNumber, block, this.web3())
}
trackTx (txHash, block, tx) {

@ -19,7 +19,8 @@ import {
saveIpfsSettingsToast,
useAutoCompletion,
useShowGasInEditor,
useDisplayErrors
useDisplayErrors,
saveEnvState
} from './settingsAction'
import {initialState, toastInitialState, toastReducer, settingReducer} from './settingsReducer'
import {Toaster} from '@remix-ui/toaster' // eslint-disable-line
@ -69,6 +70,9 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
const useShowGas = props.config.get('settings/show-gas')
if (useShowGas === null || useShowGas === undefined) useShowGasInEditor(props.config, true, dispatch)
const enableSaveEnvState = props.config.get('settings/save-evm-state')
if (enableSaveEnvState === null || enableSaveEnvState === undefined) saveEnvState(props.config, true, dispatch)
}
useEffect(() => initValue(), [resetState, props.config])
useEffect(() => initValue(), [])
@ -200,6 +204,10 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
useDisplayErrors(props.config, event.target.checked, dispatch)
}
const onchangeSaveEnvState= (event) => {
saveEnvState(props.config, event.target.checked, dispatch)
}
const getTextClass = (key) => {
if (props.config.get(key)) {
return textDark
@ -217,6 +225,7 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
const isAutoCompleteChecked = props.config.get('settings/auto-completion') || false
const isShowGasInEditorChecked = props.config.get('settings/show-gas') || false
const displayErrorsChecked = props.config.get('settings/display-errors') || false
const isSaveEvmStateChecked = props.config.get('settings/save-evm-state') || false
return (
<div className="$border-top">
<div className="d-flex justify-content-end pr-4">
@ -333,6 +342,18 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
</a>
</label>
</div>
<div className="custom-control custom-checkbox mb-1">
<input onChange={onchangeSaveEnvState} id="settingsEnableSaveEnvState" data-id="settingsEnableSaveEnvState" type="checkbox" className="custom-control-input" checked={isSaveEvmStateChecked} />
<label
className={`form-check-label custom-control-label align-middle ${getTextClass('settings/save-evm-state')}`}
data-id="settingsEnableSaveEnvStateLabel"
htmlFor="settingsEnableSaveEnvState"
>
<span>
<FormattedMessage id="settings.enableSaveEnvState" />
</span>
</label>
</div>
</div>
</div>
)

@ -90,3 +90,8 @@ export const saveIpfsSettingsToast = (config, dispatch, ipfsURL, ipfsProtocol, i
config.set('settings/ipfs-project-secret', ipfsProjectSecret)
dispatch({ type: 'save', payload: { message: 'IPFS settings have been saved' } })
}
export const saveEnvState = (config, checked, dispatch) => {
config.set('settings/save-evm-state', checked)
dispatch({ type: 'save-evm-state', payload: { isChecked: checked, textClass: checked ? textDark : textSecondary } })
}

@ -56,7 +56,12 @@ export const initialState = {
name: 'copilot/suggest/temperature',
value: 0.5,
textClass: textSecondary
}
},
{
name: 'save-evm-state',
isChecked: false,
textClass: textSecondary
},
]
}
@ -173,6 +178,17 @@ export const settingReducer = (state, action) => {
return {
...state
}
case 'save-evm-state':
state.elementState.map(element => {
if (element.name === 'save-evm-state') {
element.isChecked = action.payload.isChecked
element.textClass = action.payload.textClass
}
})
return {
...state
}
default:
return initialState
}

Loading…
Cancel
Save