UNPKG

@polygon-hermez/vm

Version:
552 lines (503 loc) 16.3 kB
import { debug as createDebugLogger } from 'debug' import { BaseTrie as Trie } from 'merkle-patricia-tree' import { Account, Address, BN, intToBuffer, rlp } from 'ethereumjs-util' import { Block } from '@ethereumjs/block' import { ConsensusType } from '@ethereumjs/common' import VM from './index' import Bloom from './bloom' import { StateManager } from './state' import { short } from './evm/opcodes' import { Capability, TypedTransaction } from '@ethereumjs/tx' import type { RunTxResult } from './runTx' import type { TxReceipt } from './types' import * as DAOConfig from './config/dao_fork_accounts_config.json' // For backwards compatibility from v5.3.0, // TxReceipts are exported. These exports are // deprecated and may be removed soon, please // update your imports to the new types file. import { PreByzantiumTxReceipt, PostByzantiumTxReceipt, EIP2930Receipt } from './types' export { PreByzantiumTxReceipt, PostByzantiumTxReceipt, EIP2930Receipt } const debug = createDebugLogger('vm:block') /* DAO account list */ const DAOAccountList = DAOConfig.DAOAccounts const DAORefundContract = DAOConfig.DAORefundContract /** * Options for running a block. */ export interface RunBlockOpts { /** * The @ethereumjs/block to process */ block: Block /** * Root of the state trie */ root?: Buffer /** * Whether to generate the stateRoot and other related fields. * If `true`, `runBlock` will set the fields `stateRoot`, `receiptsTrie`, `gasUsed`, and `bloom` (logs bloom) after running the block. * If `false`, `runBlock` throws if any fields do not match. * Defaults to `false`. */ generate?: boolean /** * If true, will skip "Block validation": * Block validation validates the header (with respect to the blockchain), * the transactions, the transaction trie and the uncle hash. */ skipBlockValidation?: boolean /** * If true, skips the nonce check */ skipNonce?: boolean /** * If true, skips the balance check */ skipBalance?: boolean } /** * Result of {@link runBlock} */ export interface RunBlockResult { /** * Receipts generated for transactions in the block */ receipts: TxReceipt[] /** * Results of executing the transactions in the block */ results: RunTxResult[] /** * The stateRoot after executing the block */ stateRoot: Buffer /** * The gas used after executing the block */ gasUsed: BN /** * The bloom filter of the LOGs (events) after executing the block */ logsBloom: Buffer /** * The receipt root after executing the block */ receiptRoot: Buffer } export interface AfterBlockEvent extends RunBlockResult { // The block which just finished processing block: Block } /** * @ignore */ export default async function runBlock(this: VM, opts: RunBlockOpts): Promise<RunBlockResult> { const state = this.stateManager const { root } = opts let { block } = opts const generateFields = !!opts.generate /** * The `beforeBlock` event. * * @event Event: beforeBlock * @type {Object} * @property {Block} block emits the block that is about to be processed */ await this._emit('beforeBlock', block) if (this._hardforkByBlockNumber || this._hardforkByTD) { this._common.setHardforkByBlockNumber(block.header.number.toNumber(), this._hardforkByTD) } if (this.DEBUG) { debug('-'.repeat(100)) debug( `Running block hash=${block.hash().toString('hex')} number=${ block.header.number } hardfork=${this._common.hardfork()}` ) } // Set state root if provided if (root) { if (this.DEBUG) { debug(`Set provided state root ${root.toString('hex')}`) } await state.setStateRoot(root) } // check for DAO support and if we should apply the DAO fork if ( this._common.hardforkIsActiveOnChain('dao') && block.header.number.eq(this._common.hardforkBlockBN('dao')!) ) { if (this.DEBUG) { debug(`Apply DAO hardfork`) } await _applyDAOHardfork(state) } // Checkpoint state await state.checkpoint() if (this.DEBUG) { debug(`block checkpoint`) } let result try { result = await applyBlock.bind(this)(block, opts) if (this.DEBUG) { debug( `Received block results gasUsed=${result.gasUsed} bloom=${short(result.bloom.bitvector)} (${ result.bloom.bitvector.length } bytes) receiptRoot=${result.receiptRoot.toString('hex')} receipts=${ result.receipts.length } txResults=${result.results.length}` ) } } catch (err: any) { await state.revert() if (this.DEBUG) { debug(`block checkpoint reverted`) } throw err } // Persist state await state.commit() if (this.DEBUG) { debug(`block checkpoint committed`) } const stateRoot = await state.getStateRoot(false) // Given the generate option, either set resulting header // values to the current block, or validate the resulting // header values against the current block. if (generateFields) { const bloom = result.bloom.bitvector const gasUsed = result.gasUsed const receiptTrie = result.receiptRoot const transactionsTrie = await _genTxTrie(block) const generatedFields = { stateRoot, bloom, gasUsed, receiptTrie, transactionsTrie } const blockData = { ...block, header: { ...block.header, ...generatedFields }, } block = Block.fromBlockData(blockData, { common: this._common }) } else { if (result.receiptRoot && !result.receiptRoot.equals(block.header.receiptTrie)) { if (this.DEBUG) { debug( `Invalid receiptTrie received=${result.receiptRoot.toString( 'hex' )} expected=${block.header.receiptTrie.toString('hex')}` ) } const msg = _errorMsg('invalid receiptTrie', this, block) throw new Error(msg) } if (!result.bloom.bitvector.equals(block.header.logsBloom)) { if (this.DEBUG) { debug( `Invalid bloom received=${result.bloom.bitvector.toString( 'hex' )} expected=${block.header.logsBloom.toString('hex')}` ) } const msg = _errorMsg('invalid bloom', this, block) throw new Error(msg) } if (!result.gasUsed.eq(block.header.gasUsed)) { if (this.DEBUG) { debug(`Invalid gasUsed received=${result.gasUsed} expected=${block.header.gasUsed}`) } const msg = _errorMsg('invalid gasUsed', this, block) throw new Error(msg) } if (!stateRoot.equals(block.header.stateRoot)) { if (this.DEBUG) { debug( `Invalid stateRoot received=${stateRoot.toString( 'hex' )} expected=${block.header.stateRoot.toString('hex')}` ) } const msg = _errorMsg('invalid block stateRoot', this, block) throw new Error(msg) } } const results: RunBlockResult = { receipts: result.receipts, results: result.results, stateRoot, gasUsed: result.gasUsed, logsBloom: result.bloom.bitvector, receiptRoot: result.receiptRoot, } const afterBlockEvent: AfterBlockEvent = { ...results, block } /** * The `afterBlock` event * * @event Event: afterBlock * @type {AfterBlockEvent} * @property {AfterBlockEvent} result emits the results of processing a block */ await this._emit('afterBlock', afterBlockEvent) if (this.DEBUG) { debug( `Running block finished hash=${block.hash().toString('hex')} number=${ block.header.number } hardfork=${this._common.hardfork()}` ) } return results } /** * Validates and applies a block, computing the results of * applying its transactions. This method doesn't modify the * block itself. It computes the block rewards and puts * them on state (but doesn't persist the changes). * @param {Block} block * @param {RunBlockOpts} opts */ async function applyBlock(this: VM, block: Block, opts: RunBlockOpts) { // Validate block if (!opts.skipBlockValidation) { if (block.header.gasLimit.gte(new BN('8000000000000000', 16))) { const msg = _errorMsg('Invalid block with gas limit greater than (2^63 - 1)', this, block) throw new Error(msg) } else { if (this.DEBUG) { debug(`Validate block`) } await block.validate(this.blockchain) } } // Apply transactions if (this.DEBUG) { debug(`Apply transactions`) } const blockResults = await applyTransactions.bind(this)(block, opts) // Pay ommers and miners if (block._common.consensusType() === ConsensusType.ProofOfWork) { await assignBlockRewards.bind(this)(block) } return blockResults } /** * Applies the transactions in a block, computing the receipts * as well as gas usage and some relevant data. This method is * side-effect free (it doesn't modify the block nor the state). * @param {Block} block * @param {RunBlockOpts} opts */ async function applyTransactions(this: VM, block: Block, opts: RunBlockOpts) { const bloom = new Bloom() // the total amount of gas used processing these transactions let gasUsed = new BN(0) const receiptTrie = new Trie() const receipts = [] const txResults = [] /* * Process transactions */ for (let txIdx = 0; txIdx < block.transactions.length; txIdx++) { const tx = block.transactions[txIdx] let maxGasLimit if (this._common.isActivatedEIP(1559)) { maxGasLimit = block.header.gasLimit.muln( this._common.param('gasConfig', 'elasticityMultiplier') ) } else { maxGasLimit = block.header.gasLimit } const gasLimitIsHigherThanBlock = maxGasLimit.lt(tx.gasLimit.add(gasUsed)) if (gasLimitIsHigherThanBlock) { const msg = _errorMsg('tx has a higher gas limit than the block', this, block) throw new Error(msg) } // Run the tx through the VM const { skipBalance, skipNonce } = opts const txRes = await this.runTx({ tx, block, skipBalance, skipNonce, blockGasUsed: gasUsed, }) txResults.push(txRes) 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})`) } // 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(rlp.encode(txIdx), encodedReceipt) } return { bloom, gasUsed, receiptRoot: receiptTrie.root, receipts, results: txResults, } } /** * Calculates block rewards for miner and ommers and puts * the updated balances of their accounts to state. */ async function assignBlockRewards(this: VM, block: Block): Promise<void> { if (this.DEBUG) { debug(`Assign block rewards`) } const state = this.stateManager const minerReward = new BN(this._common.param('pow', 'minerReward')) const ommers = block.uncleHeaders // Reward ommers for (const ommer of ommers) { const reward = calculateOmmerReward(ommer.number, block.header.number, minerReward) const account = await rewardAccount(state, ommer.coinbase, reward) if (this.DEBUG) { debug(`Add uncle reward ${reward} to account ${ommer.coinbase} (-> ${account.balance})`) } } // Reward miner const reward = calculateMinerReward(minerReward, ommers.length) const account = await rewardAccount(state, block.header.coinbase, reward) if (this.DEBUG) { debug(`Add miner reward ${reward} to account ${block.header.coinbase} (-> ${account.balance})`) } } function calculateOmmerReward(ommerBlockNumber: BN, blockNumber: BN, minerReward: BN): BN { const heightDiff = blockNumber.sub(ommerBlockNumber) let reward = new BN(8).sub(heightDiff).mul(minerReward.divn(8)) if (reward.ltn(0)) { reward = new BN(0) } return reward } export function calculateMinerReward(minerReward: BN, ommersNum: number): BN { // calculate nibling reward const niblingReward = minerReward.divn(32) const totalNiblingReward = niblingReward.muln(ommersNum) const reward = minerReward.add(totalNiblingReward) return reward } export async function rewardAccount( state: StateManager, address: Address, reward: BN ): Promise<Account> { const account = await state.getAccount(address) account.balance.iadd(reward) await state.putAccount(address, account) return account } /** * Returns the encoded tx receipt. */ export function encodeReceipt(tx: TypedTransaction, receipt: TxReceipt) { const encoded = rlp.encode([ (receipt as PreByzantiumTxReceipt).stateRoot ?? (receipt as PostByzantiumTxReceipt).status, receipt.gasUsed, receipt.bitvector, receipt.logs, ]) if (!tx.supports(Capability.EIP2718TypedTransaction)) { return encoded } const type = intToBuffer(tx.type) return Buffer.concat([type, encoded]) } /** * Generates the tx receipt and returns { txReceipt, encodedReceipt, receiptLog } * @deprecated Please use the new `generateTxReceipt` located in runTx. */ export async function generateTxReceipt( this: VM, tx: TypedTransaction, txRes: RunTxResult, blockGasUsed: BN ) { const abstractTxReceipt = { gasUsed: blockGasUsed.toArrayLike(Buffer), bitvector: txRes.bloom.bitvector, logs: txRes.execResult.logs ?? [], } let txReceipt let encodedReceipt let receiptLog = `Generate tx receipt transactionType=${ tx.type } gasUsed=${blockGasUsed} bitvector=${short(abstractTxReceipt.bitvector)} (${ abstractTxReceipt.bitvector.length } bytes) logs=${abstractTxReceipt.logs.length}` if (!tx.supports(999)) { // Legacy transaction if (this._common.gteHardfork('byzantium')) { // Post-Byzantium txReceipt = { status: txRes.execResult.exceptionError ? 0 : 1, // Receipts have a 0 as status on error ...abstractTxReceipt, } as PostByzantiumTxReceipt const statusInfo = txRes.execResult.exceptionError ? 'error' : 'ok' receiptLog += ` status=${txReceipt.status} (${statusInfo}) (>= Byzantium)` } else { // Pre-Byzantium const stateRoot = await this.stateManager.getStateRoot(true) txReceipt = { stateRoot: stateRoot, ...abstractTxReceipt, } as PreByzantiumTxReceipt receiptLog += ` stateRoot=${txReceipt.stateRoot.toString('hex')} (< Byzantium)` } encodedReceipt = rlp.encode(Object.values(txReceipt)) } else { // EIP2930 Transaction txReceipt = { status: txRes.execResult.exceptionError ? 0 : 1, ...abstractTxReceipt, } as PostByzantiumTxReceipt encodedReceipt = Buffer.concat([intToBuffer(tx.type), rlp.encode(Object.values(txReceipt))]) } return { txReceipt, encodedReceipt, receiptLog, } } // apply the DAO fork changes to the VM async function _applyDAOHardfork(state: StateManager) { const DAORefundContractAddress = new Address(Buffer.from(DAORefundContract, 'hex')) if (!state.accountExists(DAORefundContractAddress)) { await state.putAccount(DAORefundContractAddress, new Account()) } const DAORefundAccount = await state.getAccount(DAORefundContractAddress) for (const addr of DAOAccountList) { // retrieve the account and add it to the DAO's Refund accounts' balance. const address = new Address(Buffer.from(addr, 'hex')) const account = await state.getAccount(address) DAORefundAccount.balance.iadd(account.balance) // clear the accounts' balance account.balance = new BN(0) await state.putAccount(address, account) } // finally, put the Refund Account await state.putAccount(DAORefundContractAddress, DAORefundAccount) } async function _genTxTrie(block: Block) { const trie = new Trie() for (const [i, tx] of block.transactions.entries()) { await trie.put(rlp.encode(i), tx.serialize()) } return trie.root } /** * Internal helper function to create an annotated error message * * @param msg Base error message * @hidden */ function _errorMsg(msg: string, vm: VM, block: Block) { const blockErrorStr = 'errorStr' in block ? block.errorStr() : 'block' const errorMsg = `${msg} (${vm.errorStr()} -> ${blockErrorStr})` return errorMsg }