From 75d2368d59bc1e243da00bb67d20dbefe7129df5 Mon Sep 17 00:00:00 2001 From: samlior Date: Thu, 5 Aug 2021 18:03:00 +0800 Subject: [PATCH] feat: add debug api for vm --- packages/vm/src/runBlock.ts | 115 +++++++++++++++++---- packages/vm/src/runCall.ts | 64 +++++++++++- packages/vm/src/types.ts | 39 +++++++ packages/vm/tests/BlockchainTestsRunner.ts | 2 +- 4 files changed, 193 insertions(+), 27 deletions(-) diff --git a/packages/vm/src/runBlock.ts b/packages/vm/src/runBlock.ts index 5aa462a11b..7fbfa4e862 100644 --- a/packages/vm/src/runBlock.ts +++ b/packages/vm/src/runBlock.ts @@ -1,15 +1,16 @@ import { debug as createDebugLogger } from 'debug' import { encode } from 'rlp' import { BaseTrie as Trie } from 'merkle-patricia-tree' -import { Account, Address, BN, intToBuffer } from 'ethereumjs-util' +import { Account, Address, BN, intToBuffer, generateAddress } from 'ethereumjs-util' import { Block } from '@gxchain2-ethereumjs/block' import VM from './index' import Bloom from './bloom' import { StateManager } from './state' import { short } from './evm/opcodes' -import { Capability, TypedTransaction } from '@gxchain2-ethereumjs/tx' +import { Capability, TypedTransaction, FeeMarketEIP1559Transaction } from '@gxchain2-ethereumjs/tx' import type { RunTxResult } from './runTx' -import type { TxReceipt } from './types' +import type { TxReceipt, IDebug } from './types' +import { InterpreterStep } from './evm/interpreter' import * as DAOConfig from './config/dao_fork_accounts_config.json' // For backwards compatibility from v5.3.0, @@ -58,6 +59,14 @@ export interface RunBlockOpts { * If true, skips the balance check */ skipBalance?: boolean + /** + * Debug callback + */ + debug?: IDebug + /** + * Clique signer for generating new block + */ + cliqueSigner?: Buffer } /** @@ -88,6 +97,10 @@ export interface RunBlockResult { * The receipt root after executing the block */ receiptRoot: Buffer + /** + * The generated block + */ + block?: Block } export interface AfterBlockEvent extends RunBlockResult { @@ -193,7 +206,10 @@ export default async function runBlock(this: VM, opts: RunBlockOpts): Promise void) => void) + if (opts.debug) { + handler = async (step: InterpreterStep, next: () => void) => { + await opts.debug!.captureState(step) + next() + } + } + /* * Process transactions */ + let catchedErr: any for (let txIdx = 0; txIdx < block.transactions.length; txIdx++) { const tx = block.transactions[txIdx] @@ -329,37 +355,82 @@ async function applyTransactions(this: VM, block: Block, opts: RunBlockOpts) { const gasLimitIsHigherThanBlock = maxGasLimit.lt(tx.gasLimit.add(gasUsed)) if (gasLimitIsHigherThanBlock) { + if (handler) { + this.removeListener('step', handler) + } throw new Error('tx has a higher gas limit than the block') } + // Call tx exec start + let time: undefined | number + if (opts.debug && (!opts.debug.hash || opts.debug.hash.equals(tx.hash()))) { + time = Date.now() + await opts.debug.captureStart( + tx.getSenderAddress().buf, + tx?.to?.buf || generateAddress(tx.getSenderAddress().buf, tx.nonce.toArrayLike(Buffer)), + tx.toCreationAddress(), + tx.data, + tx.gasLimit, + tx instanceof FeeMarketEIP1559Transaction ? new BN(0) : tx.gasPrice, + tx.value, + block.header.number, + this.stateManager + ) + this.on('step', handler) + } + // Run the tx through the VM const { skipBalance, skipNonce } = opts - const txRes = await this.runTx({ - tx, - block, - skipBalance, - skipNonce, - blockGasUsed: gasUsed, - }) - txResults.push(txRes) + let txRes: undefined | RunTxResult + try { + txRes = await this.runTx({ + tx, + block, + skipBalance, + skipNonce, + blockGasUsed: gasUsed, + }) + txResults.push(txRes) + } catch (err) { + catchedErr = err + } + if (this.DEBUG) { debug('-'.repeat(100)) } - // Add to total block gas usage - gasUsed = gasUsed.add(txRes.gasUsed) - if (this.DEBUG) { - debug(`Add tx gas used (${txRes.gasUsed}) to total block gas usage (-> ${gasUsed})`) + if (txRes) { + // Add to total block gas usage + gasUsed = gasUsed.add(txRes.gasUsed) + if (this.DEBUG) { + debug(`Add tx gas used (${txRes.gasUsed}) to total block gas usage (-> ${gasUsed})`) + } + + // Combine blooms via bitwise OR + bloom.or(txRes.bloom) + + // Add receipt to trie to later calculate receipt root + receipts.push(txRes.receipt) + const encodedReceipt = encodeReceipt(tx, txRes.receipt) + await receiptTrie.put(encode(txIdx), encodedReceipt) } - // Combine blooms via bitwise OR - bloom.or(txRes.bloom) + // Call tx exec over + if (opts.debug && (!opts.debug.hash || opts.debug.hash.equals(tx.hash()))) { + if (txRes) { + await opts.debug.captureEnd(txRes.execResult.returnValue, txRes.gasUsed, Date.now() - time!) + } else { + await opts.debug.captureEnd(Buffer.alloc(0), new BN(0), Date.now() - time!) + } + if (handler) { + this.removeListener('step', handler) + } + } - // Add receipt to trie to later calculate receipt root - receipts.push(txRes.receipt) - const encodedReceipt = encodeReceipt(tx, txRes.receipt) - await receiptTrie.put(encode(txIdx), encodedReceipt) + if (catchedErr) { + break + } } return { diff --git a/packages/vm/src/runCall.ts b/packages/vm/src/runCall.ts index 65e7426b97..dbe3c196cb 100644 --- a/packages/vm/src/runCall.ts +++ b/packages/vm/src/runCall.ts @@ -1,8 +1,10 @@ -import { Address, BN } from 'ethereumjs-util' +import { Address, BN, generateAddress } from 'ethereumjs-util' import { Block } from '@gxchain2-ethereumjs/block' import VM from './index' import TxContext from './evm/txContext' import Message from './evm/message' +import { InterpreterStep } from './evm/interpreter' +import type { IDebug } from './types' import { default as EVM, EVMResult } from './evm/evm' /** @@ -27,12 +29,13 @@ export interface RunCallOpts { salt?: Buffer selfdestruct?: { [k: string]: boolean } delegatecall?: boolean + debug?: IDebug } /** * @ignore */ -export default function runCall(this: VM, opts: RunCallOpts): Promise { +export default async function runCall(this: VM, opts: RunCallOpts): Promise { const block = opts.block ?? Block.fromBlockData({}, { common: this._common }) const txContext = new TxContext( @@ -55,7 +58,60 @@ export default function runCall(this: VM, opts: RunCallOpts): Promise delegatecall: opts.delegatecall ?? false, }) - const evm = new EVM(this, txContext, block) + // Update from account's nonce and balance + const state = this.stateManager + let fromAccount = await state.getAccount(message.caller) + fromAccount.nonce.iaddn(1) + await state.putAccount(message.caller, fromAccount) - return evm.executeMessage(message) + let time: undefined | number + let handler: undefined | ((step: InterpreterStep, next: () => void) => void) + if (opts.debug) { + handler = async (step: InterpreterStep, next: () => void) => { + await opts.debug!.captureState(step) + next() + } + this.on('step', handler) + time = Date.now() + await opts.debug.captureStart( + message?.caller?.buf, + message?.to?.buf || + generateAddress(message.caller.buf, fromAccount.nonce.subn(1).toArrayLike(Buffer)), + message.to === undefined, + message.data, + message.gasLimit, + new BN(0), + message.value, + block.header.number, + this.stateManager + ) + } + + let result: undefined | EVMResult + let catchedErr: any + try { + const evm = new EVM(this, txContext, block) + result = await evm.executeMessage(message) + } catch (err) { + catchedErr = err + } + + // Remove Listener + if (handler) { + this.removeListener('step', handler) + } + + // Call tx exec over + if (opts.debug) { + if (result) { + await opts.debug.captureEnd(result.execResult.returnValue, result.gasUsed, Date.now() - time!) + } else { + await opts.debug.captureEnd(Buffer.alloc(0), new BN(0), Date.now() - time!) + } + } + + if (!result) { + throw catchedErr + } + return result } diff --git a/packages/vm/src/types.ts b/packages/vm/src/types.ts index 92b62cddd1..4d22384cdc 100644 --- a/packages/vm/src/types.ts +++ b/packages/vm/src/types.ts @@ -1,4 +1,7 @@ import { Log } from './evm/types' +import { BN } from 'ethereumjs-util' +import { InterpreterStep } from './evm/interpreter' +import { StateManager } from './state' export type TxReceipt = PreByzantiumTxReceipt | PostByzantiumTxReceipt | EIP2930Receipt @@ -55,3 +58,39 @@ export interface EIP2930Receipt extends PostByzantiumTxReceipt {} * @deprecated Please use PostByzantiumTxReceipt instead */ export interface EIP1559Receipt extends PostByzantiumTxReceipt {} + +/** + * Options for debugging. + */ +export interface IDebug { + /** + * Target transaction hash + */ + hash?: Buffer + /** + * Called when the transaction starts processing + */ + captureStart( + from: undefined | Buffer, + to: undefined | Buffer, + create: boolean, + input: Buffer, + gas: BN, + gasPrice: BN, + value: BN, + number: BN, + stateManager: StateManager + ): Promise + /** + * Called at every step of processing a transaction + */ + captureState(step: InterpreterStep): Promise + /** + * Called when a transaction error occurs + */ + captureFault(step: InterpreterStep, err: any): Promise + /** + * Called when processing a transaction + */ + captureEnd(output: Buffer, gasUsed: BN, time: number): Promise +} diff --git a/packages/vm/tests/BlockchainTestsRunner.ts b/packages/vm/tests/BlockchainTestsRunner.ts index 41816922c4..89954111d5 100644 --- a/packages/vm/tests/BlockchainTestsRunner.ts +++ b/packages/vm/tests/BlockchainTestsRunner.ts @@ -58,7 +58,7 @@ export default async function runBlockchainTest(options: any, testData: any, t: }) if (validatePow) { - blockchain._ethash!.cacheDB = cacheDB + // blockchain._ethash!.cacheDB = cacheDB } let VM