diff --git a/apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts b/apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts index cebc84bc93..943292cc82 100644 --- a/apps/remix-ide-e2e/src/tests/runAndDeploy.test.ts +++ b/apps/remix-ide-e2e/src/tests/runAndDeploy.test.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() } } diff --git a/apps/remix-ide/src/app/tabs/locales/en/settings.json b/apps/remix-ide/src/app/tabs/locales/en/settings.json index aa8f67449c..2ea6de0d4b 100644 --- a/apps/remix-ide/src/app/tabs/locales/en/settings.json +++ b/apps/remix-ide/src/app/tabs/locales/en/settings.json @@ -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" } diff --git a/apps/remix-ide/src/app/tabs/locales/es/settings.json b/apps/remix-ide/src/app/tabs/locales/es/settings.json index 899e2787cc..5d7ddda6ab 100644 --- a/apps/remix-ide/src/app/tabs/locales/es/settings.json +++ b/apps/remix-ide/src/app/tabs/locales/es/settings.json @@ -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" } diff --git a/apps/remix-ide/src/app/tabs/locales/fr/settings.json b/apps/remix-ide/src/app/tabs/locales/fr/settings.json index 5d1859dfe9..3b61ab68c2 100644 --- a/apps/remix-ide/src/app/tabs/locales/fr/settings.json +++ b/apps/remix-ide/src/app/tabs/locales/fr/settings.json @@ -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" } diff --git a/apps/remix-ide/src/app/tabs/locales/it/settings.json b/apps/remix-ide/src/app/tabs/locales/it/settings.json index 416f338b64..251089f6cb 100644 --- a/apps/remix-ide/src/app/tabs/locales/it/settings.json +++ b/apps/remix-ide/src/app/tabs/locales/it/settings.json @@ -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" } diff --git a/apps/remix-ide/src/app/tabs/locales/zh/settings.json b/apps/remix-ide/src/app/tabs/locales/zh/settings.json index 5504874efd..8a27f4a83d 100644 --- a/apps/remix-ide/src/app/tabs/locales/zh/settings.json +++ b/apps/remix-ide/src/app/tabs/locales/zh/settings.json @@ -36,5 +36,6 @@ "settings.port": "端口", "settings.projectID": "项目 ID", "settings.projectSecret": "项目密钥", - "settings.analyticsInRemix": "Remix IDE 中的分析功能" + "settings.analyticsInRemix": "Remix IDE 中的分析功能", + "settings.enableSaveEnvState": "Save environment state" } diff --git a/apps/remix-ide/src/app/tabs/web3-provider.js b/apps/remix-ide/src/app/tabs/web3-provider.js index 16c3a3dd17..d4efb04a77 100644 --- a/apps/remix-ide/src/app/tabs/web3-provider.js +++ b/apps/remix-ide/src/app/tabs/web3-provider.js @@ -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) diff --git a/apps/remix-ide/src/blockchain/blockchain.tsx b/apps/remix-ide/src/blockchain/blockchain.tsx index fe9b6598f1..8095777a00 100644 --- a/apps/remix-ide/src/blockchain/blockchain.tsx +++ b/apps/remix-ide/src/blockchain/blockchain.tsx @@ -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 ) - } + } }) }) 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 = (
diff --git a/apps/remix-ide/src/blockchain/execution-context.js b/apps/remix-ide/src/blockchain/execution-context.js index 7c33bb4f27..89ad383c7c 100644 --- a/apps/remix-ide/src/blockchain/execution-context.js +++ b/apps/remix-ide/src/blockchain/execution-context.js @@ -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 + } } diff --git a/apps/remix-ide/src/blockchain/providers/vm.ts b/apps/remix-ide/src/blockchain/providers/vm.ts index 67357b347e..5156206c1c 100644 --- a/apps/remix-ide/src/blockchain/providers/vm.ts +++ b/apps/remix-ide/src/blockchain/providers/vm.ts @@ -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) { diff --git a/apps/remix-ide/src/blockchain/providers/worker-vm.ts b/apps/remix-ide/src/blockchain/providers/worker-vm.ts index 64a8d0255b..e91e30dfd4 100644 --- a/apps/remix-ide/src/blockchain/providers/worker-vm.ts +++ b/apps/remix-ide/src/blockchain/providers/worker-vm.ts @@ -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', diff --git a/libs/remix-lib/src/execution/logsManager.ts b/libs/remix-lib/src/execution/logsManager.ts index 7d9bba2c6b..fbb0d1cf8c 100644 --- a/libs/remix-lib/src/execution/logsManager.ts +++ b/libs/remix-lib/src/execution/logsManager.ts @@ -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}) diff --git a/libs/remix-lib/src/execution/txRunnerVM.ts b/libs/remix-lib/src/execution/txRunnerVM.ts index 7604bee0b2..74e42feb51 100644 --- a/libs/remix-lib/src/execution/txRunnerVM.ts +++ b/libs/remix-lib/src/execution/txRunnerVM.ts @@ -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 { diff --git a/libs/remix-simulator/src/methods/transactions.ts b/libs/remix-simulator/src/methods/transactions.ts index 6018f9fc99..41e502fe06 100644 --- a/libs/remix-simulator/src/methods/transactions.ts +++ b/libs/remix-simulator/src/methods/transactions.ts @@ -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) { diff --git a/libs/remix-simulator/src/provider.ts b/libs/remix-simulator/src/provider.ts index 59066c0ab6..95b7cf6632 100644 --- a/libs/remix-simulator/src/provider.ts +++ b/libs/remix-simulator/src/provider.ts @@ -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 + +export type ProviderOptions = { + fork?: string, + nodeUrl?: string, + blockNumber?: number | 'latest', + stateDb?: State, + logDetails?: boolean + blocks?: string[], + coinbase?: string +} + export class Provider { - options: Record + options: ProviderOptions vmContext Accounts Transactions @@ -37,10 +50,10 @@ export class Provider { initialized: boolean pendingRequests: Array - constructor (options: Record = {}) { + 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: [] + }) + } } diff --git a/libs/remix-simulator/src/vm-context.ts b/libs/remix-simulator/src/vm-context.ts index 0bf58c3ad8..9acf1128c1 100644 --- a/libs/remix-simulator/src/vm-context.ts +++ b/libs/remix-simulator/src/vm-context.ts @@ -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 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) { diff --git a/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx b/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx index 28c6ec1f6c..57f0cc7dd1 100644 --- a/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx +++ b/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx @@ -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 (
@@ -333,6 +342,18 @@ export const RemixUiSettings = (props: RemixUiSettingsProps) => {
+
+ + +
) diff --git a/libs/remix-ui/settings/src/lib/settingsAction.ts b/libs/remix-ui/settings/src/lib/settingsAction.ts index 6e59d25ffb..22110f8cb9 100644 --- a/libs/remix-ui/settings/src/lib/settingsAction.ts +++ b/libs/remix-ui/settings/src/lib/settingsAction.ts @@ -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 } }) +} diff --git a/libs/remix-ui/settings/src/lib/settingsReducer.ts b/libs/remix-ui/settings/src/lib/settingsReducer.ts index abad510431..cfd8572f9e 100644 --- a/libs/remix-ui/settings/src/lib/settingsReducer.ts +++ b/libs/remix-ui/settings/src/lib/settingsReducer.ts @@ -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 }