From 6eec573339d0bc79c3cd43e7a19b61868cf6e038 Mon Sep 17 00:00:00 2001 From: yann300 Date: Thu, 6 Feb 2025 10:50:18 +0100 Subject: [PATCH] VM state fork with live net --- .../src/app/providers/vm-provider.tsx | 6 ++- apps/remix-ide/src/app/udapp/run-tab.tsx | 43 ++++++++++--------- .../src/blockchain/execution-context.js | 5 ++- apps/remix-ide/src/blockchain/providers/vm.ts | 1 + .../src/blockchain/providers/worker-vm.ts | 2 +- .../src/methods/transactions.ts | 3 +- libs/remix-simulator/src/provider.ts | 2 + libs/remix-simulator/src/vm-context.ts | 39 ++++++++++++----- .../src/lib/types/index.ts | 8 ++-- .../src/lib/components/environment.tsx | 23 ++++++++-- .../src/lib/types/execution-context.d.ts | 1 + 11 files changed, 90 insertions(+), 43 deletions(-) diff --git a/apps/remix-ide/src/app/providers/vm-provider.tsx b/apps/remix-ide/src/app/providers/vm-provider.tsx index 37649480454..d7f5ea2298b 100644 --- a/apps/remix-ide/src/app/providers/vm-provider.tsx +++ b/apps/remix-ide/src/app/providers/vm-provider.tsx @@ -133,9 +133,13 @@ export class CancunVMProvider extends BasicVMProvider { } export class ForkedVMStateProvider extends BasicVMProvider { - constructor(profile, blockchain, fork) { + nodeUrl?: string + blockNumber?: string + constructor(profile, blockchain, fork: string, nodeUrl?: string, blockNumber?: string) { super(profile, blockchain) this.blockchain = blockchain this.fork = fork + this.nodeUrl = nodeUrl + this.blockNumber = blockNumber } } diff --git a/apps/remix-ide/src/app/udapp/run-tab.tsx b/apps/remix-ide/src/app/udapp/run-tab.tsx index 188ea9d7955..98199e209e2 100644 --- a/apps/remix-ide/src/app/udapp/run-tab.tsx +++ b/apps/remix-ide/src/app/udapp/run-tab.tsx @@ -180,7 +180,7 @@ export class RunTab extends ViewPlugin { 'foundry-provider': ['assets/img/foundry.png'] } - const addProvider = async (position: number, name: string, displayName: string, providerConfig: ProviderConfig, fork = '', dataId = '', title = '') => { + const addProvider = async (position: number, name: string, displayName: string, providerConfig: ProviderConfig, dataId = '', title = '') => { await this.call('blockchain', 'addProvider', { position, options: {}, @@ -189,14 +189,15 @@ export class RunTab extends ViewPlugin { displayName, description: descriptions[name] || displayName, logos: logos[name], - fork, config: providerConfig, title, init: async function () { - const options = await udapp.call(name, 'init') + const options = await udapp.call(name, 'init') if (options) { this.options = options - if (options['fork']) this.fork = options['fork'] + if (options['fork']) this.config.fork = options['fork'] + if (options['nodeUrl']) this.config.nodeUrl = options['nodeUrl'] + if (options['blockNumber']) this.config.blockNumber = options['blockNumber'] } }, provider: new Provider(udapp, name) @@ -206,13 +207,13 @@ export class RunTab extends ViewPlugin { const addCustomInjectedProvider = async (position, event, name, displayName, networkId, urls, nativeCurrency?) => { // name = `${name} through ${event.detail.info.name}` await this.engine.register([new InjectedCustomProvider(event.detail.provider, name, displayName, networkId, urls, nativeCurrency)]) - await addProvider(position, name, displayName + ' - ' + event.detail.info.name, { isInjected: true, isVM: false, isRpcForkedState: false }) + await addProvider(position, name, displayName + ' - ' + event.detail.info.name, { isInjected: true, isVM: false, isRpcForkedState: false, fork: ''}) } const registerInjectedProvider = async (event) => { const name = 'injected-' + event.detail.info.name const displayName = 'Injected Provider - ' + event.detail.info.name await this.engine.register([new InjectedProviderDefault(event.detail.provider, name)]) - await addProvider(0, name, displayName, { isInjected: true, isVM: false, isRpcForkedState: false }) + await addProvider(0, name, displayName, { isInjected: true, isVM: false, isRpcForkedState: false, fork: '' }) if (event.detail.info.name === 'MetaMask') { await addCustomInjectedProvider(7, event, 'injected-metamask-optimism', 'L2 - Optimism', '0xa', ['https://mainnet.optimism.io']) @@ -249,14 +250,14 @@ export class RunTab extends ViewPlugin { // VM const titleVM = 'Execution environment is local to Remix. Data is only saved to browser memory and will vanish upon reload.' - await addProvider(1, 'vm-cancun', 'Remix VM (Cancun)', { isInjected: false, isVM: true, isRpcForkedState: false, statePath: '.states/vm-cancun/state.json' }, 'cancun', 'settingsVMCancunMode', titleVM) - await addProvider(50, 'vm-shanghai', 'Remix VM (Shanghai)', { isInjected: false, isVM: true, isRpcForkedState: false, statePath: '.states/vm-shanghai/state.json' }, 'shanghai', 'settingsVMShanghaiMode', titleVM) - await addProvider(51, 'vm-paris', 'Remix VM (Paris)', { isInjected: false, isVM: true, isRpcForkedState: false, statePath: '.states/vm-paris/state.json' }, 'paris', 'settingsVMParisMode', titleVM) - await addProvider(52, 'vm-london', 'Remix VM (London)', { isInjected: false, isVM: true, isRpcForkedState: false, statePath: '.states/vm-london/state.json' }, 'london', 'settingsVMLondonMode', titleVM) - await addProvider(53, 'vm-berlin', 'Remix VM (Berlin)', { isInjected: false, isVM: true, isRpcForkedState: false, statePath: '.states/vm-berlin/state.json' }, 'berlin', 'settingsVMBerlinMode', titleVM) - await addProvider(2, 'vm-mainnet-fork', 'Remix VM - Mainnet fork', { isInjected: false, isVM: true, isRpcForkedState: true }, 'cancun', 'settingsVMMainnetMode', titleVM) - await addProvider(3, 'vm-sepolia-fork', 'Remix VM - Sepolia fork', { isInjected: false, isVM: true, isRpcForkedState: true }, 'cancun', 'settingsVMSepoliaMode', titleVM) - await addProvider(4, 'vm-custom-fork', 'Remix VM - Custom fork', { isInjected: false, isVM: true, isRpcForkedState: true }, '', 'settingsVMCustomMode', titleVM) + await addProvider(1, 'vm-cancun', 'Remix VM (Cancun)', { isInjected: false, isVM: true, isRpcForkedState: false, statePath: '.states/vm-cancun/state.json', fork: 'cancun' }, 'settingsVMCancunMode', titleVM) + await addProvider(50, 'vm-shanghai', 'Remix VM (Shanghai)', { isInjected: false, isVM: true, isRpcForkedState: false, statePath: '.states/vm-shanghai/state.json', fork: 'shanghai' }, 'settingsVMShanghaiMode', titleVM) + await addProvider(51, 'vm-paris', 'Remix VM (Paris)', { isInjected: false, isVM: true, isRpcForkedState: false, statePath: '.states/vm-paris/state.json', fork: 'paris' }, 'settingsVMParisMode', titleVM) + await addProvider(52, 'vm-london', 'Remix VM (London)', { isInjected: false, isVM: true, isRpcForkedState: false, statePath: '.states/vm-london/state.json', fork: 'london' }, 'settingsVMLondonMode', titleVM) + await addProvider(53, 'vm-berlin', 'Remix VM (Berlin)', { isInjected: false, isVM: true, isRpcForkedState: false, statePath: '.states/vm-berlin/state.json', fork: 'berlin' }, 'settingsVMBerlinMode', titleVM) + await addProvider(2, 'vm-mainnet-fork', 'Remix VM - Mainnet fork', { isInjected: false, isVM: true, isRpcForkedState: true, fork: 'cancun' }, 'settingsVMMainnetMode', titleVM) + await addProvider(3, 'vm-sepolia-fork', 'Remix VM - Sepolia fork', { isInjected: false, isVM: true, isRpcForkedState: true, fork: 'cancun' }, 'settingsVMSepoliaMode', titleVM) + await addProvider(4, 'vm-custom-fork', 'Remix VM - Custom fork', { isInjected: false, isVM: true, isRpcForkedState: true, fork: '' }, 'settingsVMCustomMode', titleVM) // Forked VM States const addFVSProvider = async(stateFilePath, pos) => { @@ -276,9 +277,9 @@ export class RunTab extends ViewPlugin { description: descriptions[providerName], methods: ['sendAsync', 'init'], version: packageJson.version - }, this.blockchain, stateDetail.forkName) + }, this.blockchain, stateDetail.forkName, stateDetail.nodeUrl, stateDetail.blockNumber) this.engine.register(fvsProvider) - await addProvider(pos, providerName, stateDetail.stateName, { isInjected: false, isVM: true, isRpcForkedState: false, isVMStateForked: true, statePath: `.states/forked_states/${stateDetail.stateName}.json` }, stateDetail.forkName) + await addProvider(pos, providerName, stateDetail.stateName, { isInjected: false, isVM: true, isRpcForkedState: false, isVMStateForked: true, statePath: `.states/forked_states/${stateDetail.stateName}.json`, fork: stateDetail.forkName }) } this.on('filePanel', 'workspaceInitializationCompleted', async () => { @@ -300,13 +301,13 @@ export class RunTab extends ViewPlugin { }) // wallet connect - await addProvider(6, 'walletconnect', 'WalletConnect', { isInjected: false, isVM: false, isRpcForkedState: false }) + await addProvider(6, 'walletconnect', 'WalletConnect', { isInjected: false, isVM: false, isRpcForkedState: false, fork: '' }) // external provider - await addProvider(10, 'basic-http-provider', 'Custom - External Http Provider', { isInjected: false, isVM: false, isRpcForkedState: false }) - await addProvider(20, 'hardhat-provider', 'Dev - Hardhat Provider', { isInjected: false, isVM: false, isRpcForkedState: false }) - await addProvider(21, 'ganache-provider', 'Dev - Ganache Provider', { isInjected: false, isVM: false, isRpcForkedState: false }) - await addProvider(22, 'foundry-provider', 'Dev - Foundry Provider', { isInjected: false, isVM: false, isRpcForkedState: false }) + await addProvider(10, 'basic-http-provider', 'Custom - External Http Provider', { isInjected: false, isVM: false, isRpcForkedState: false, fork: '' }) + await addProvider(20, 'hardhat-provider', 'Dev - Hardhat Provider', { isInjected: false, isVM: false, isRpcForkedState: false, fork: '' }) + await addProvider(21, 'ganache-provider', 'Dev - Ganache Provider', { isInjected: false, isVM: false, isRpcForkedState: false, fork: '' }) + await addProvider(22, 'foundry-provider', 'Dev - Foundry Provider', { isInjected: false, isVM: false, isRpcForkedState: false, fork: '' }) // register injected providers diff --git a/apps/remix-ide/src/blockchain/execution-context.js b/apps/remix-ide/src/blockchain/execution-context.js index cf82b96ad82..df1c20059db 100644 --- a/apps/remix-ide/src/blockchain/execution-context.js +++ b/apps/remix-ide/src/blockchain/execution-context.js @@ -148,7 +148,7 @@ export class ExecutionContext { if (this.customNetWorks[context]) { var network = this.customNetWorks[context] await network.init() - this.currentFork = network.fork + this.currentFork = network.config.fork this.executionContext = context // injected web3.setProvider(network.provider) @@ -215,7 +215,8 @@ export class ExecutionContext { const state = { db: Object.fromEntries(stateDb.db._database), blocks: blocksData.blocks, - latestBlockNumber: blocksData.latestBlockNumber + latestBlockNumber: blocksData.latestBlockNumber, + baseBlockNumber: blocksData.baseBlockNumber } const stringifyed = JSON.stringify(state, (key, value) => { if (key === 'db') { diff --git a/apps/remix-ide/src/blockchain/providers/vm.ts b/apps/remix-ide/src/blockchain/providers/vm.ts index f079c3d4673..4cf925d92a2 100644 --- a/apps/remix-ide/src/blockchain/providers/vm.ts +++ b/apps/remix-ide/src/blockchain/providers/vm.ts @@ -101,6 +101,7 @@ export class VMProvider { nodeUrl: provider?.options['nodeUrl'], blockNumber, stateDb, + baseBlockNumer: blockchainState.baseBlockNumber, blocks: blockchainState.blocks }) } catch (e) { diff --git a/apps/remix-ide/src/blockchain/providers/worker-vm.ts b/apps/remix-ide/src/blockchain/providers/worker-vm.ts index a72e272acb6..a53010b71fa 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, stateDb: data.stateDb, blocks: data.blocks }) + provider = new Provider({ baseBlockNumber: data.baseBlockNumer, 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-simulator/src/methods/transactions.ts b/libs/remix-simulator/src/methods/transactions.ts index d2bf5f6b2b9..0dedbaa936f 100644 --- a/libs/remix-simulator/src/methods/transactions.ts +++ b/libs/remix-simulator/src/methods/transactions.ts @@ -240,7 +240,8 @@ export class Transactions { eth_getBlocksData (_, cb) { cb(null, { blocks: this.txRunnerVMInstance.blocks, - latestBlockNumber: this.txRunnerVMInstance.blocks.length - 1 + latestBlockNumber: this.txRunnerVMInstance.blocks.length - 1, + baseBlockNumber: this.vmContext.currentVm.baseBlockNumber }) } diff --git a/libs/remix-simulator/src/provider.ts b/libs/remix-simulator/src/provider.ts index a9be59dacb5..df06a01d2ae 100644 --- a/libs/remix-simulator/src/provider.ts +++ b/libs/remix-simulator/src/provider.ts @@ -36,6 +36,7 @@ export type ProviderOptions = { fork?: string, nodeUrl?: string, blockNumber?: number | 'latest', + baseBlockNumber: number, stateDb?: State, details?: boolean blocks?: string[], @@ -54,6 +55,7 @@ export class Provider { pendingRequests: Array constructor (options: ProviderOptions = {} as ProviderOptions) { + console.log(options) this.options = options this.connected = true this.vmContext = new VMContext(options['fork'], options['nodeUrl'], options['blockNumber'], options['stateDb'], options['blocks']) diff --git a/libs/remix-simulator/src/vm-context.ts b/libs/remix-simulator/src/vm-context.ts index 3b8fa05929a..7f9926a8b04 100644 --- a/libs/remix-simulator/src/vm-context.ts +++ b/libs/remix-simulator/src/vm-context.ts @@ -254,7 +254,8 @@ export type CurrentVm = { vm: VM, web3vm: VmProxy, stateManager: EVMStateManagerInterface, - common: Common + common: Common, + baseBlockNumber: number | 'latest' } export class VMCommon extends Common { @@ -276,6 +277,7 @@ export class VMContext { blockGasLimit: number blocks: Record latestBlockNumber: string + baseBlockNumber: number blockByTxHash: Record txByHash: Record currentVm: CurrentVm @@ -288,13 +290,14 @@ export class VMContext { rawBlocks: string[] serializedBlocks: Uint8Array[] - constructor (fork?: string, nodeUrl?: string, blockNumber?: number | 'latest', stateDb?: State, blocksData?: string[]) { + constructor (fork?: string, nodeUrl?: string, blockNumber?: number | 'latest', stateDb?: State, blocksData?: string[], baseBlockNumber?: number) { this.blockGasLimitDefault = 4300000 this.blockGasLimit = this.blockGasLimitDefault this.currentFork = fork || 'cancun' this.nodeUrl = nodeUrl this.stateDb = stateDb this.blockNumber = blockNumber + this.baseBlockNumber = baseBlockNumber || -1 this.blocks = {} this.latestBlockNumber = "0x0" this.blockByTxHash = {} @@ -311,27 +314,40 @@ export class VMContext { async createVm (hardfork) { let stateManager: EVMStateManagerInterface + // state trie + let trie = null + if (this.stateDb) { + const db = this.stateDb ? new Map(Object.entries(this.stateDb).map(([k, v]) => [k, hexToBytes(v)])) : new Map() + const mapDb = new MapDB(db) + trie = await Trie.create({ useKeyHashing: true, db: mapDb, useRootPersistence: true }) + } + if (this.nodeUrl) { let block = this.blockNumber - if (this.blockNumber === 'latest') { + if (this.baseBlockNumber !== -1) { + stateManager = new CustomEthersStateManager({ + provider: this.nodeUrl, + blockTag: '0x' + this.baseBlockNumber.toString(16), + trie + }) + this.blockNumber = this.baseBlockNumber + } else if (this.blockNumber === 'latest') { const provider = new ethers.providers.StaticJsonRpcProvider(this.nodeUrl) block = await provider.getBlockNumber() stateManager = new CustomEthersStateManager({ provider: this.nodeUrl, - blockTag: '0x' + block.toString(16) + blockTag: '0x' + block.toString(16), + trie }) this.blockNumber = block } else { stateManager = new CustomEthersStateManager({ provider: this.nodeUrl, - blockTag: '0x' + block.toString(16) + blockTag: '0x' + block.toString(16), + trie }) } } else { - const db = this.stateDb ? new Map(Object.entries(this.stateDb).map(([k, v]) => [k, hexToBytes(v)])) : new Map() - const mapDb = new MapDB(db) - const trie = await Trie.create({ useKeyHashing: true, db: mapDb, useRootPersistence: true }) - stateManager = new StateManagerCommonStorageDump({ trie }) } @@ -374,7 +390,10 @@ export class VMContext { await blockchain.putBlock(block) this.addBlock(block, false, false, web3vm) } - return { vm, web3vm, stateManager, common, blocks } + + console.log('creating vm', hardfork, this.nodeUrl, this.blockNumber, this.stateDb, this.rawBlocks) + + return { vm, web3vm, stateManager, common, blocks, baseBlockNumber: this.blockNumber } } getCurrentFork () { diff --git a/libs/remix-ui/environment-explorer/src/lib/types/index.ts b/libs/remix-ui/environment-explorer/src/lib/types/index.ts index 37c57362c46..fa502b54196 100644 --- a/libs/remix-ui/environment-explorer/src/lib/types/index.ts +++ b/libs/remix-ui/environment-explorer/src/lib/types/index.ts @@ -31,7 +31,10 @@ export type ProviderConfig = { isInjected: boolean isRpcForkedState?: boolean isVMStateForked?: boolean - statePath?: string + fork: string + statePath?: string, + blockNumber?: string + nodeUrl?: string } export type Provider = { @@ -41,8 +44,7 @@ export type Provider = { name: string displayName: string logo?: string, - logos?: string[], - fork: string + logos?: string[], description?: string config: ProviderConfig title: string diff --git a/libs/remix-ui/run-tab/src/lib/components/environment.tsx b/libs/remix-ui/run-tab/src/lib/components/environment.tsx index ecf0d53ba4b..44a10d7a6ef 100644 --- a/libs/remix-ui/run-tab/src/lib/components/environment.tsx +++ b/libs/remix-ui/run-tab/src/lib/components/environment.tsx @@ -64,26 +64,41 @@ export function EnvironmentUI(props: EnvironmentProps) { const forkState = async () => { _paq.push(['trackEvent', 'udapp', 'forkState', `forkState clicked`]) + + let stateTemp = `.states/forked_states/state_temp.json` + let statePath = currentProvider.config.statePath + if (!statePath) { + // if the current provider doesn't have a saved state, we dump the current state from memory. + // state_temp.json is removed after the operation completes + const state = await props.runTabPlugin.blockchain.executionContext.getStateDetails() + statePath = stateTemp + await props.runTabPlugin.call('fileManager', 'writeFile', statePath, state) + } + const context = currentProvider.name vmStateName.current = `${context}_${Date.now()}` - const contextExists = await props.runTabPlugin.call('fileManager', 'exists', currentProvider.config.statePath) + const contextExists = await props.runTabPlugin.call('fileManager', 'exists', statePath) if (contextExists) { props.modal( intl.formatMessage({ id: 'udapp.forkStateTitle' }), forkStatePrompt(vmStateName.current), intl.formatMessage({ id: 'udapp.fork' }), async () => { - let currentStateDb = await props.runTabPlugin.call('fileManager', 'readFile', currentProvider.config.statePath) + let currentStateDb = await props.runTabPlugin.call('fileManager', 'readFile', statePath) currentStateDb = JSON.parse(currentStateDb) currentStateDb.stateName = vmStateName.current - currentStateDb.forkName = currentProvider.fork + currentStateDb.forkName = currentProvider.config.fork + currentStateDb.nodeUrl = currentProvider.config.nodeUrl currentStateDb.savingTimestamp = Date.now() await props.runTabPlugin.call('fileManager', 'writeFile', `.states/forked_states/${vmStateName.current}.json`, JSON.stringify(currentStateDb, null, 2)) props.runTabPlugin.emit('vmStateForked', vmStateName.current) + await props.runTabPlugin.call('fileManager', 'remove', stateTemp) _paq.push(['trackEvent', 'udapp', 'forkState', `forked from ${context}`]) }, intl.formatMessage({ id: 'udapp.cancel' }), - null + () => { + props.runTabPlugin.call('fileManager', 'remove', stateTemp) + } ) } else props.runTabPlugin.call('notification', 'toast', `State not available to fork, as no transactions have been made for selected environment & selected workspace.`) } diff --git a/libs/remix-ui/run-tab/src/lib/types/execution-context.d.ts b/libs/remix-ui/run-tab/src/lib/types/execution-context.d.ts index 598c4f8f7d8..bf159b108fc 100644 --- a/libs/remix-ui/run-tab/src/lib/types/execution-context.d.ts +++ b/libs/remix-ui/run-tab/src/lib/types/execution-context.d.ts @@ -32,4 +32,5 @@ export class ExecutionContext { _updateChainContext(): Promise; listenOnLastBlock(): void; txDetailsLink(network: any, hash: any): any; + getStateDetails(): Promise }