diff --git a/packages/client/bin/cli.ts b/packages/client/bin/cli.ts index 0ba9b4757d..bb02889190 100755 --- a/packages/client/bin/cli.ts +++ b/packages/client/bin/cli.ts @@ -174,6 +174,11 @@ const args = yargs(hideBin(process.argv)) describe: 'EIP-1459 ENR tree urls to query for peer discovery targets', array: true, }) + .option('executeBlocks', { + describe: + 'Debug mode for reexecuting existing blocks (no services will be started), allowed input formats: 5,5-10', + string: true, + }) .option('debugCode', { describe: 'Generate code for local debugging (internal usage mostly)', boolean: true, @@ -240,8 +245,38 @@ async function runNode(config: Config) { client.config.logger.info(`Synchronized blockchain at height ${height}`) }) await client.open() - config.logger.info('Connecting to network and synchronizing blockchain...') - await client.start() + + if (args.executeBlocks) { + // Special block execution debug mode (not changing any state) + let first = 0 + let last = 0 + let txHashes = [] + try { + const blockRange = (args.executeBlocks as string).split('-').map((val) => { + const reNum = /([0-9]+)/.exec(val) + const num = reNum ? parseInt(reNum[1]) : 0 + const reTxs = /[0-9]+\[(.*)\]/.exec(val) + const txs = reTxs ? reTxs[1].split(',') : [] + return [num, txs] + }) + first = blockRange[0][0] as number + last = blockRange.length === 2 ? (blockRange[1][0] as number) : first + txHashes = blockRange[0][1] as string[] + + if ((blockRange[0][1] as string[]).length > 0 && blockRange.length === 2) { + throw new Error('wrong input') + } + } catch (e: any) { + client.config.logger.error( + 'Wrong input format for block execution, allowed format types: 5, 5-10, 5[0xba4b5fd92a26badad3cad22eb6f7c7e745053739b5f5d1e8a3afb00f8fb2a280,[TX_HASH_2],...]' + ) + process.exit() + } + await client.executeBlocks(first, last, txHashes) + } else { + // Regular client start + await client.start() + } return client } diff --git a/packages/client/lib/client.ts b/packages/client/lib/client.ts index 871f18d397..46063489dc 100644 --- a/packages/client/lib/client.ts +++ b/packages/client/lib/client.ts @@ -1,10 +1,12 @@ import events from 'events' import { MultiaddrLike } from './types' import { Config } from './config' -import { FullEthereumService, LightEthereumService } from './service' +import { EthereumService, FullEthereumService, LightEthereumService } from './service' import { Event } from './types' // eslint-disable-next-line implicit-dependencies/no-implicit import type { LevelUp } from 'levelup' +import { FullSynchronizer } from './sync' +import { bufferToHex } from 'ethereumjs-util' export interface EthereumClientOptions { /* Client configuration */ @@ -97,6 +99,8 @@ export default class EthereumClient extends events.EventEmitter { if (this.started) { return false } + this.config.logger.info('Connecting to network and synchronizing blockchain...') + await Promise.all(this.services.map((s) => s.start())) await Promise.all(this.config.servers.map((s) => s.start())) await Promise.all(this.config.servers.map((s) => s.bootstrap())) @@ -130,4 +134,57 @@ export default class EthereumClient extends events.EventEmitter { server(name: string) { return this.config.servers.find((s) => s.name === name) } + + /** + * Execute a range of blocks on a copy of the VM + * without changing any chain or client state + * + * Possible input formats: + * + * - Single block, '5' + * - Range of blocks, '5-10' + * + */ + async executeBlocks(first: number, last: number, txHashes: string[]) { + this.config.logger.info('Preparing for block execution (debug mode, no services started)...') + + const service = this.services.find((s) => s.name === 'eth') as EthereumService + const synchronizer = service.synchronizer as FullSynchronizer + const vm = synchronizer.execution.vm.copy() + + for (let blockNumber = first; blockNumber <= last; blockNumber++) { + const block = await vm.blockchain.getBlock(blockNumber) + const parentBlock = await vm.blockchain.getBlock(block.header.parentHash) + + // Set the correct state root + await vm.stateManager.setStateRoot(parentBlock.header.stateRoot) + + const td = await vm.blockchain.getTotalDifficulty(block.header.parentHash) + vm._common.setHardforkByBlockNumber(blockNumber, td) + + if (txHashes.length === 0) { + const res = await vm.runBlock({ block }) + this.config.logger.info( + `Executed block num=${blockNumber} hash=0x${block.hash().toString('hex')} txs=${ + block.transactions.length + } gasUsed=${res.gasUsed} ` + ) + } else { + let count = 0 + for (const tx of block.transactions) { + const txHash = bufferToHex(tx.hash()) + if (txHashes.includes(txHash)) { + const res = await vm.runTx({ block, tx }) + this.config.logger.info( + `Executed tx hash=${txHash} gasUsed=${res.gasUsed} from block num=${blockNumber}` + ) + count += 1 + } + } + if (count === 0) { + this.config.logger.warn(`Block number ${first} contains no txs with provided hashes`) + } + } + } + } } diff --git a/packages/client/lib/net/peerpool.ts b/packages/client/lib/net/peerpool.ts index 39bbdebcdc..1f42aecfd9 100644 --- a/packages/client/lib/net/peerpool.ts +++ b/packages/client/lib/net/peerpool.ts @@ -22,6 +22,8 @@ export class PeerPool { private pool: Map private noPeerPeriods: number private opened: boolean + public running: boolean + private _statusCheckInterval: NodeJS.Timeout | undefined /* global NodeJS */ /** @@ -33,6 +35,7 @@ export class PeerPool { this.pool = new Map() this.noPeerPeriods = 0 this.opened = false + this.running = false this.init() } @@ -57,8 +60,32 @@ export class PeerPool { }) this.opened = true + } + + /** + * Start peer pool + */ + async start(): Promise { + if (this.running) { + return false + } // eslint-disable-next-line @typescript-eslint/await-thenable this._statusCheckInterval = setInterval(await this._statusCheck.bind(this), 20000) + + this.running = true + return true + } + + /** + * Stop peer pool + */ + async stop(): Promise { + if (this.opened) { + await this.close() + } + clearInterval(this._statusCheckInterval as NodeJS.Timeout) + this.running = false + return true } /** @@ -67,7 +94,6 @@ export class PeerPool { async close() { this.pool.clear() this.opened = false - clearInterval(this._statusCheckInterval as NodeJS.Timeout) } /** diff --git a/packages/client/lib/service/service.ts b/packages/client/lib/service/service.ts index bde2806872..027438c4b9 100644 --- a/packages/client/lib/service/service.ts +++ b/packages/client/lib/service/service.ts @@ -105,6 +105,7 @@ export class Service { if (this.running) { return false } + await this.pool.start() this.running = true this.config.logger.info(`Started ${this.name} service.`) return true @@ -117,6 +118,7 @@ export class Service { if (this.opened) { await this.close() } + await this.pool.stop() this.running = false this.config.logger.info(`Stopped ${this.name} service.`) return true