UNPKG

@nomicfoundation/ethereumjs-vm

Version:
759 lines (692 loc) 23.7 kB
import { Block } from '@nomicfoundation/ethereumjs-block' import { ConsensusType, Hardfork } from '@nomicfoundation/ethereumjs-common' import { BlobEIP4844Transaction, Capability, isBlobEIP4844Tx } from '@nomicfoundation/ethereumjs-tx' import { Account, Address, BIGINT_0, KECCAK256_NULL, bytesToHex, bytesToUnprefixedHex, equalsBytes, hexToBytes, short, } from '@nomicfoundation/ethereumjs-util' import debugDefault from 'debug' import { Bloom } from './bloom/index.js' import type { AfterTxEvent, BaseTxReceipt, EIP4844BlobTxReceipt, PostByzantiumTxReceipt, PreByzantiumTxReceipt, RunTxOpts, RunTxResult, TxReceipt, } from './types.js' import type { VM } from './vm.js' import type { AccessList, AccessListItem, Common } from '@nomicfoundation/ethereumjs-common' import type { EVM } from '@nomicfoundation/ethereumjs-evm' import type { AccessListEIP2930Transaction, FeeMarketEIP1559Transaction, LegacyTransaction, TypedTransaction, } from '@nomicfoundation/ethereumjs-tx' const { debug: createDebugLogger } = debugDefault const debug = createDebugLogger('vm:tx') const debugGas = createDebugLogger('vm:tx:gas') let enableProfiler = false const initLabel = 'EVM journal init, address/slot warming, fee validation' const balanceNonceLabel = 'Balance/Nonce checks and update' const logsGasBalanceLabel = 'Logs, gas usage, account/miner balances' const cleanupAndReceiptsLabel = 'Accounts clean up, access list, journal/cache cleanup, receipts' const entireTxLabel = 'Entire tx' /** * Returns the hardfork excluding the merge hf which has * no effect on the vm execution capabilities. * * This is particularly useful in executing/evaluating the transaction * when chain td is not available at many places to correctly set the * hardfork in for e.g. vm or txs or when the chain is not fully synced yet. * * @returns Hardfork name */ function execHardfork( hardfork: Hardfork | string, preMergeHf: Hardfork | string ): string | Hardfork { return hardfork !== Hardfork.Paris ? hardfork : preMergeHf } /** * @ignore */ export async function runTx(this: VM, opts: RunTxOpts): Promise<RunTxResult> { if (this._opts.profilerOpts?.reportAfterTx === true) { enableProfiler = true } if (enableProfiler) { const title = `Profiler run - Tx ${bytesToHex(opts.tx.hash())}` // eslint-disable-next-line no-console console.log(title) // eslint-disable-next-line no-console console.time(initLabel) // eslint-disable-next-line no-console console.time(entireTxLabel) } // create a reasonable default if no block is given opts.block = opts.block ?? Block.fromBlockData({}, { common: this.common }) if (opts.skipHardForkValidation !== true) { // Find and set preMerge hf for easy access later const hfs = this.common.hardforks() const preMergeIndex = hfs.findIndex((hf) => hf.ttd !== null && hf.ttd !== undefined) - 1 // If no pre merge hf found, set it to first hf even if its merge const preMergeHf = preMergeIndex >= 0 ? hfs[preMergeIndex].name : hfs[0].name // If block and tx don't have a same hardfork, set tx hardfork to block if ( execHardfork(opts.tx.common.hardfork(), preMergeHf) !== execHardfork(opts.block.common.hardfork(), preMergeHf) ) { opts.tx.common.setHardfork(opts.block.common.hardfork()) } if ( execHardfork(opts.block.common.hardfork(), preMergeHf) !== execHardfork(this.common.hardfork(), preMergeHf) ) { // Block and VM's hardfork should match as well const msg = _errorMsg('block has a different hardfork than the vm', this, opts.block, opts.tx) throw new Error(msg) } } if (opts.skipBlockGasLimitValidation !== true && opts.block.header.gasLimit < opts.tx.gasLimit) { const msg = _errorMsg('tx has a higher gas limit than the block', this, opts.block, opts.tx) throw new Error(msg) } // Ensure we start with a clear warmed accounts Map await this.evm.journal.cleanup() if (opts.reportAccessList === true) { this.evm.journal.startReportingAccessList() } await this.evm.journal.checkpoint() if (this.DEBUG) { debug('-'.repeat(100)) debug(`tx checkpoint`) } // Typed transaction specific setup tasks if ( opts.tx.supports(Capability.EIP2718TypedTransaction) && this.common.isActivatedEIP(2718) === true ) { // Is it an Access List transaction? if (this.common.isActivatedEIP(2930) === false) { await this.evm.journal.revert() const msg = _errorMsg( 'Cannot run transaction: EIP 2930 is not activated.', this, opts.block, opts.tx ) throw new Error(msg) } if ( opts.tx.supports(Capability.EIP1559FeeMarket) && this.common.isActivatedEIP(1559) === false ) { await this.evm.journal.revert() const msg = _errorMsg( 'Cannot run transaction: EIP 1559 is not activated.', this, opts.block, opts.tx ) throw new Error(msg) } const castedTx = <AccessListEIP2930Transaction>opts.tx for (const accessListItem of castedTx.AccessListJSON) { this.evm.journal.addAlwaysWarmAddress(accessListItem.address, true) for (const storageKey of accessListItem.storageKeys) { this.evm.journal.addAlwaysWarmSlot(accessListItem.address, storageKey, true) } } } try { const result = await _runTx.bind(this)(opts) await this.evm.journal.commit() if (this.DEBUG) { debug(`tx checkpoint committed`) } return result } catch (e: any) { await this.evm.journal.revert() if (this.DEBUG) { debug(`tx checkpoint reverted`) } throw e } finally { if (this.common.isActivatedEIP(2929) === true) { this.evm.journal.cleanJournal() } this.evm.stateManager.originalStorageCache.clear() if (enableProfiler) { // eslint-disable-next-line no-console console.timeEnd(entireTxLabel) const logs = (<EVM>this.evm).getPerformanceLogs() if (logs.precompiles.length === 0 && logs.opcodes.length === 0) { // eslint-disable-next-line no-console console.log('No precompile or opcode execution.') } this.emitEVMProfile(logs.precompiles, 'Precompile performance') this.emitEVMProfile(logs.opcodes, 'Opcodes performance') ;(<EVM>this.evm).clearPerformanceLogs() } } } async function _runTx(this: VM, opts: RunTxOpts): Promise<RunTxResult> { const state = this.stateManager const { tx, block } = opts if (!block) { throw new Error('block required') } /** * The `beforeTx` event * * @event Event: beforeTx * @type {Object} * @property {Transaction} tx emits the Transaction that is about to be processed */ await this._emit('beforeTx', tx) const caller = tx.getSenderAddress() if (this.DEBUG) { debug( `New tx run hash=${ opts.tx.isSigned() ? bytesToHex(opts.tx.hash()) : 'unsigned' } sender=${caller}` ) } if (this.common.isActivatedEIP(2929) === true) { // Add origin and precompiles to warm addresses const activePrecompiles = this.evm.precompiles for (const [addressStr] of activePrecompiles.entries()) { this.evm.journal.addAlwaysWarmAddress(addressStr) } this.evm.journal.addAlwaysWarmAddress(caller.toString()) if (tx.to !== undefined) { // Note: in case we create a contract, we do this in EVMs `_executeCreate` (this is also correct in inner calls, per the EIP) this.evm.journal.addAlwaysWarmAddress(bytesToUnprefixedHex(tx.to.bytes)) } if (this.common.isActivatedEIP(3651) === true) { this.evm.journal.addAlwaysWarmAddress(bytesToUnprefixedHex(block.header.coinbase.bytes)) } } // Validate gas limit against tx base fee (DataFee + TxFee + Creation Fee) const txBaseFee = tx.getBaseFee() let gasLimit = tx.gasLimit if (gasLimit < txBaseFee) { const msg = _errorMsg( `tx gas limit ${Number(gasLimit)} is lower than the minimum gas limit of ${Number( txBaseFee )}`, this, block, tx ) throw new Error(msg) } gasLimit -= txBaseFee if (this.DEBUG) { debugGas(`Subtracting base fee (${txBaseFee}) from gasLimit (-> ${gasLimit})`) } if (this.common.isActivatedEIP(1559) === true) { // EIP-1559 spec: // Ensure that the user was willing to at least pay the base fee // assert transaction.max_fee_per_gas >= block.base_fee_per_gas const maxFeePerGas = 'maxFeePerGas' in tx ? tx.maxFeePerGas : tx.gasPrice const baseFeePerGas = block.header.baseFeePerGas! if (maxFeePerGas < baseFeePerGas) { const msg = _errorMsg( `Transaction's ${ 'maxFeePerGas' in tx ? 'maxFeePerGas' : 'gasPrice' } (${maxFeePerGas}) is less than the block's baseFeePerGas (${baseFeePerGas})`, this, block, tx ) throw new Error(msg) } } if (enableProfiler) { // eslint-disable-next-line no-console console.timeEnd(initLabel) // eslint-disable-next-line no-console console.time(balanceNonceLabel) } // Check from account's balance and nonce let fromAccount = await state.getAccount(caller) if (fromAccount === undefined) { fromAccount = new Account() } const { nonce, balance } = fromAccount if (this.DEBUG) { debug(`Sender's pre-tx balance is ${balance}`) } // EIP-3607: Reject transactions from senders with deployed code if ( this.common.isActivatedEIP(3607) === true && !equalsBytes(fromAccount.codeHash, KECCAK256_NULL) ) { const msg = _errorMsg('invalid sender address, address is not EOA (EIP-3607)', this, block, tx) throw new Error(msg) } // Check balance against upfront tx cost const upFrontCost = tx.getUpfrontCost(block.header.baseFeePerGas) if (balance < upFrontCost) { if (opts.skipBalance === true && fromAccount.balance < upFrontCost) { if (tx.supports(Capability.EIP1559FeeMarket) === false) { // if skipBalance and not EIP1559 transaction, ensure caller balance is enough to run transaction fromAccount.balance = upFrontCost await this.evm.journal.putAccount(caller, fromAccount) } } else { const msg = _errorMsg( `sender doesn't have enough funds to send tx. The upfront cost is: ${upFrontCost} and the sender's account (${caller}) only has: ${balance}`, this, block, tx ) throw new Error(msg) } } // Check balance against max potential cost (for EIP 1559 and 4844) let maxCost = tx.value let blobGasPrice = BIGINT_0 let totalblobGas = BIGINT_0 if (tx.supports(Capability.EIP1559FeeMarket)) { // EIP-1559 spec: // The signer must be able to afford the transaction // `assert balance >= gas_limit * max_fee_per_gas` maxCost += tx.gasLimit * (tx as FeeMarketEIP1559Transaction).maxFeePerGas } if (tx instanceof BlobEIP4844Transaction) { if (!this.common.isActivatedEIP(4844)) { const msg = _errorMsg('blob transactions are only valid with EIP4844 active', this, block, tx) throw new Error(msg) } // EIP-4844 spec // the signer must be able to afford the transaction // assert signer(tx).balance >= tx.message.gas * tx.message.max_fee_per_gas + get_total_data_gas(tx) * tx.message.max_fee_per_data_gas const castTx = tx as BlobEIP4844Transaction totalblobGas = castTx.common.param('gasConfig', 'blobGasPerBlob') * BigInt(castTx.numBlobs()) maxCost += totalblobGas * castTx.maxFeePerBlobGas // 4844 minimum blobGas price check if (opts.block === undefined) { const msg = _errorMsg( `Block option must be supplied to compute blob gas price`, this, block, tx ) throw new Error(msg) } blobGasPrice = opts.block.header.getBlobGasPrice() if (castTx.maxFeePerBlobGas < blobGasPrice) { const msg = _errorMsg( `Transaction's maxFeePerBlobGas ${castTx.maxFeePerBlobGas}) is less than block blobGasPrice (${blobGasPrice}).`, this, block, tx ) throw new Error(msg) } } if (fromAccount.balance < maxCost) { if (opts.skipBalance === true && fromAccount.balance < maxCost) { // if skipBalance, ensure caller balance is enough to run transaction fromAccount.balance = maxCost await this.evm.journal.putAccount(caller, fromAccount) } else { const msg = _errorMsg( `sender doesn't have enough funds to send tx. The max cost is: ${maxCost} and the sender's account (${caller}) only has: ${balance}`, this, block, tx ) throw new Error(msg) } } if (opts.skipNonce !== true) { if (nonce !== tx.nonce) { const msg = _errorMsg( `the tx doesn't have the correct nonce. account has nonce of: ${nonce} tx has nonce of: ${tx.nonce}`, this, block, tx ) throw new Error(msg) } } let gasPrice: bigint let inclusionFeePerGas: bigint // EIP-1559 tx if (tx.supports(Capability.EIP1559FeeMarket)) { const baseFee = block.header.baseFeePerGas! inclusionFeePerGas = (tx as FeeMarketEIP1559Transaction).maxPriorityFeePerGas < (tx as FeeMarketEIP1559Transaction).maxFeePerGas - baseFee ? (tx as FeeMarketEIP1559Transaction).maxPriorityFeePerGas : (tx as FeeMarketEIP1559Transaction).maxFeePerGas - baseFee gasPrice = inclusionFeePerGas + baseFee } else { // Have to cast as legacy tx since EIP1559 tx does not have gas price gasPrice = (<LegacyTransaction>tx).gasPrice if (this.common.isActivatedEIP(1559) === true) { const baseFee = block.header.baseFeePerGas! inclusionFeePerGas = (<LegacyTransaction>tx).gasPrice - baseFee } } // EIP-4844 tx let blobVersionedHashes if (tx instanceof BlobEIP4844Transaction) { blobVersionedHashes = (tx as BlobEIP4844Transaction).blobVersionedHashes } // Update from account's balance const txCost = tx.gasLimit * gasPrice const blobGasCost = totalblobGas * blobGasPrice fromAccount.balance -= txCost fromAccount.balance -= blobGasCost if (opts.skipBalance === true && fromAccount.balance < BIGINT_0) { fromAccount.balance = BIGINT_0 } await this.evm.journal.putAccount(caller, fromAccount) if (this.DEBUG) { debug(`Update fromAccount (caller) balance (-> ${fromAccount.balance}))`) } if (enableProfiler) { // eslint-disable-next-line no-console console.timeEnd(balanceNonceLabel) // eslint-disable-next-line no-console console.log('[ For execution see detailed table output ]') } /* * Execute message */ const { value, data, to } = tx if (this.DEBUG) { debug( `Running tx=${ tx.isSigned() ? bytesToHex(tx.hash()) : 'unsigned' } with caller=${caller} gasLimit=${gasLimit} to=${ to?.toString() ?? 'none' } value=${value} data=${short(data)}` ) } const results = (await this.evm.runCall({ block, gasPrice, caller, gasLimit, to, value, data, blobVersionedHashes, })) as RunTxResult if (enableProfiler) { // eslint-disable-next-line no-console console.time(logsGasBalanceLabel) } if (this.DEBUG) { debug(`Update fromAccount (caller) nonce (-> ${fromAccount.nonce})`) } if (this.DEBUG) { const { executionGasUsed, exceptionError, returnValue } = results.execResult debug('-'.repeat(100)) debug( `Received tx execResult: [ executionGasUsed=${executionGasUsed} exceptionError=${ exceptionError !== undefined ? `'${exceptionError.error}'` : 'none' } returnValue=${short(returnValue)} gasRefund=${results.gasRefund ?? 0} ]` ) } /* * Parse results */ // Generate the bloom for the tx results.bloom = txLogsBloom(results.execResult.logs, this.common) if (this.DEBUG) { debug(`Generated tx bloom with logs=${results.execResult.logs?.length}`) } // Calculate the total gas used results.totalGasSpent = results.execResult.executionGasUsed + txBaseFee if (this.DEBUG) { debugGas(`tx add baseFee ${txBaseFee} to totalGasSpent (-> ${results.totalGasSpent})`) } // Add blob gas used to result if (isBlobEIP4844Tx(tx)) { results.blobGasUsed = totalblobGas } // Process any gas refund let gasRefund = results.execResult.gasRefund ?? BIGINT_0 results.gasRefund = gasRefund const maxRefundQuotient = this.common.param('gasConfig', 'maxRefundQuotient') if (gasRefund !== BIGINT_0) { const maxRefund = results.totalGasSpent / maxRefundQuotient gasRefund = gasRefund < maxRefund ? gasRefund : maxRefund results.totalGasSpent -= gasRefund if (this.DEBUG) { debug(`Subtract tx gasRefund (${gasRefund}) from totalGasSpent (-> ${results.totalGasSpent})`) } } else { if (this.DEBUG) { debug(`No tx gasRefund`) } } results.amountSpent = results.totalGasSpent * gasPrice // Update sender's balance fromAccount = await state.getAccount(caller) if (fromAccount === undefined) { fromAccount = new Account() } const actualTxCost = results.totalGasSpent * gasPrice const txCostDiff = txCost - actualTxCost fromAccount.balance += txCostDiff await this.evm.journal.putAccount(caller, fromAccount) if (this.DEBUG) { debug( `Refunded txCostDiff (${txCostDiff}) to fromAccount (caller) balance (-> ${fromAccount.balance})` ) } // Update miner's balance let miner if (this.common.consensusType() === ConsensusType.ProofOfAuthority) { miner = block.header.cliqueSigner() } else { miner = block.header.coinbase } let minerAccount = await state.getAccount(miner) if (minerAccount === undefined) { minerAccount = new Account() } // add the amount spent on gas to the miner's account results.minerValue = this.common.isActivatedEIP(1559) === true ? results.totalGasSpent * inclusionFeePerGas! : results.amountSpent minerAccount.balance += results.minerValue // Put the miner account into the state. If the balance of the miner account remains zero, note that // the state.putAccount function puts this into the "touched" accounts. This will thus be removed when // we clean the touched accounts below in case we are in a fork >= SpuriousDragon await this.evm.journal.putAccount(miner, minerAccount) if (this.DEBUG) { debug(`tx update miner account (${miner}) balance (-> ${minerAccount.balance})`) } if (enableProfiler) { // eslint-disable-next-line no-console console.timeEnd(logsGasBalanceLabel) // eslint-disable-next-line no-console console.time(cleanupAndReceiptsLabel) } /* * Cleanup accounts */ if (results.execResult.selfdestruct !== undefined) { for (const addressToSelfdestructHex of results.execResult.selfdestruct) { const address = new Address(hexToBytes(addressToSelfdestructHex)) if (this.common.isActivatedEIP(6780)) { // skip cleanup of addresses not in createdAddresses if (!results.execResult.createdAddresses!.has(address.toString())) { continue } } await this.evm.journal.deleteAccount(address) if (this.DEBUG) { debug(`tx selfdestruct on address=${address}`) } } } if (opts.reportAccessList === true && this.common.isActivatedEIP(2930)) { // Convert the Map to the desired type const accessList: AccessList = [] for (const [address, set] of this.evm.journal.accessList!) { const addressPrefixed = '0x' + address const item: AccessListItem = { address: addressPrefixed, storageKeys: [], } for (const slot of set) { const slotPrefixed = '0x' + slot item.storageKeys.push(slotPrefixed) } accessList.push(item) } results.accessList = accessList } await this.evm.journal.cleanup() state.originalStorageCache.clear() // Generate the tx receipt const gasUsed = opts.blockGasUsed !== undefined ? opts.blockGasUsed : block.header.gasUsed const cumulativeGasUsed = gasUsed + results.totalGasSpent results.receipt = await generateTxReceipt.bind(this)( tx, results, cumulativeGasUsed, totalblobGas, blobGasPrice ) if (enableProfiler) { // eslint-disable-next-line no-console console.timeEnd(cleanupAndReceiptsLabel) } /** * The `afterTx` event * * @event Event: afterTx * @type {Object} * @property {Object} result result of the transaction */ const event: AfterTxEvent = { transaction: tx, ...results } await this._emit('afterTx', event) if (this.DEBUG) { debug( `tx run finished hash=${ opts.tx.isSigned() ? bytesToHex(opts.tx.hash()) : 'unsigned' } sender=${caller}` ) } return results } /** * @method txLogsBloom * @private */ function txLogsBloom(logs?: any[], common?: Common): Bloom { const bloom = new Bloom(undefined, common) if (logs) { for (let i = 0; i < logs.length; i++) { const log = logs[i] // add the address bloom.add(log[0]) // add the topics const topics = log[1] for (let q = 0; q < topics.length; q++) { bloom.add(topics[q]) } } } return bloom } /** * Returns the tx receipt. * @param this The vm instance * @param tx The transaction * @param txResult The tx result * @param cumulativeGasUsed The gas used in the block including this tx * @param blobGasUsed The blob gas used in the tx * @param blobGasPrice The blob gas price for the block including this tx */ export async function generateTxReceipt( this: VM, tx: TypedTransaction, txResult: RunTxResult, cumulativeGasUsed: bigint, blobGasUsed?: bigint, blobGasPrice?: bigint ): Promise<TxReceipt> { const baseReceipt: BaseTxReceipt = { cumulativeBlockGasUsed: cumulativeGasUsed, bitvector: txResult.bloom.bitvector, logs: txResult.execResult.logs ?? [], } let receipt if (this.DEBUG) { debug( `Generate tx receipt transactionType=${ tx.type } cumulativeBlockGasUsed=${cumulativeGasUsed} bitvector=${short(baseReceipt.bitvector)} (${ baseReceipt.bitvector.length } bytes) logs=${baseReceipt.logs.length}` ) } if (!tx.supports(Capability.EIP2718TypedTransaction)) { // Legacy transaction if (this.common.gteHardfork(Hardfork.Byzantium) === true) { // Post-Byzantium receipt = { status: txResult.execResult.exceptionError !== undefined ? 0 : 1, // Receipts have a 0 as status on error ...baseReceipt, } as PostByzantiumTxReceipt } else { // Pre-Byzantium const stateRoot = await this.stateManager.getStateRoot() receipt = { stateRoot, ...baseReceipt, } as PreByzantiumTxReceipt } } else { // Typed EIP-2718 Transaction if (isBlobEIP4844Tx(tx)) { receipt = { blobGasUsed, blobGasPrice, status: txResult.execResult.exceptionError ? 0 : 1, ...baseReceipt, } as EIP4844BlobTxReceipt } else { receipt = { status: txResult.execResult.exceptionError ? 0 : 1, ...baseReceipt, } as PostByzantiumTxReceipt } } return receipt } /** * Internal helper function to create an annotated error message * * @param msg Base error message * @hidden */ function _errorMsg(msg: string, vm: VM, block: Block, tx: TypedTransaction) { const blockErrorStr = 'errorStr' in block ? block.errorStr() : 'block' const txErrorStr = 'errorStr' in tx ? tx.errorStr() : 'tx' const errorMsg = `${msg} (${vm.errorStr()} -> ${blockErrorStr} -> ${txErrorStr})` return errorMsg }