Skip to content

Commit

Permalink
VM state fork with live net
Browse files Browse the repository at this point in the history
  • Loading branch information
yann300 committed Feb 11, 2025
1 parent ab225a6 commit 6eec573
Show file tree
Hide file tree
Showing 11 changed files with 90 additions and 43 deletions.
6 changes: 5 additions & 1 deletion apps/remix-ide/src/app/providers/vm-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
43 changes: 22 additions & 21 deletions apps/remix-ide/src/app/udapp/run-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {},
Expand All @@ -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)
Expand All @@ -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'])
Expand Down Expand Up @@ -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) => {
Expand All @@ -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 () => {
Expand All @@ -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

Expand Down
5 changes: 3 additions & 2 deletions apps/remix-ide/src/blockchain/execution-context.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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') {
Expand Down
1 change: 1 addition & 0 deletions apps/remix-ide/src/blockchain/providers/vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export class VMProvider {
nodeUrl: provider?.options['nodeUrl'],
blockNumber,
stateDb,
baseBlockNumer: blockchainState.baseBlockNumber,
blocks: blockchainState.blocks
})
} catch (e) {
Expand Down
2 changes: 1 addition & 1 deletion apps/remix-ide/src/blockchain/providers/worker-vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 2 additions & 1 deletion libs/remix-simulator/src/methods/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
}

Expand Down
2 changes: 2 additions & 0 deletions libs/remix-simulator/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export type ProviderOptions = {
fork?: string,
nodeUrl?: string,
blockNumber?: number | 'latest',
baseBlockNumber: number,
stateDb?: State,
details?: boolean
blocks?: string[],
Expand All @@ -54,6 +55,7 @@ export class Provider {
pendingRequests: Array<any>

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'])
Expand Down
39 changes: 29 additions & 10 deletions libs/remix-simulator/src/vm-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -276,6 +277,7 @@ export class VMContext {
blockGasLimit: number
blocks: Record<string, Block>
latestBlockNumber: string
baseBlockNumber: number
blockByTxHash: Record<string, Block>
txByHash: Record<string, TypedTransaction>
currentVm: CurrentVm
Expand All @@ -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 = {}
Expand All @@ -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 })
}

Expand Down Expand Up @@ -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 () {
Expand Down
8 changes: 5 additions & 3 deletions libs/remix-ui/environment-explorer/src/lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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
Expand Down
23 changes: 19 additions & 4 deletions libs/remix-ui/run-tab/src/lib/components/environment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.`)
}
Expand Down
1 change: 1 addition & 0 deletions libs/remix-ui/run-tab/src/lib/types/execution-context.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ export class ExecutionContext {
_updateChainContext(): Promise<void>;
listenOnLastBlock(): void;
txDetailsLink(network: any, hash: any): any;
getStateDetails(): Promise<string>
}

0 comments on commit 6eec573

Please sign in to comment.