UNPKG

@nomicfoundation/ethereumjs-vm

Version:
578 lines 24.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateTxReceipt = exports.runTx = void 0; const ethereumjs_block_1 = require("@nomicfoundation/ethereumjs-block"); const ethereumjs_common_1 = require("@nomicfoundation/ethereumjs-common"); const ethereumjs_tx_1 = require("@nomicfoundation/ethereumjs-tx"); const ethereumjs_util_1 = require("@nomicfoundation/ethereumjs-util"); const debug_1 = require("debug"); const index_js_1 = require("./bloom/index.js"); const { debug: createDebugLogger } = debug_1.default; 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, preMergeHf) { return hardfork !== ethereumjs_common_1.Hardfork.Paris ? hardfork : preMergeHf; } /** * @ignore */ async function runTx(opts) { if (this._opts.profilerOpts?.reportAfterTx === true) { enableProfiler = true; } if (enableProfiler) { const title = `Profiler run - Tx ${(0, ethereumjs_util_1.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 ?? ethereumjs_block_1.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(ethereumjs_tx_1.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(ethereumjs_tx_1.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 = 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) { 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 = 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'); this.evm.clearPerformanceLogs(); } } } exports.runTx = runTx; async function _runTx(opts) { 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() ? (0, ethereumjs_util_1.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((0, ethereumjs_util_1.bytesToUnprefixedHex)(tx.to.bytes)); } if (this.common.isActivatedEIP(3651) === true) { this.evm.journal.addAlwaysWarmAddress((0, ethereumjs_util_1.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 ethereumjs_util_1.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 && !(0, ethereumjs_util_1.equalsBytes)(fromAccount.codeHash, ethereumjs_util_1.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(ethereumjs_tx_1.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 = ethereumjs_util_1.BIGINT_0; let totalblobGas = ethereumjs_util_1.BIGINT_0; if (tx.supports(ethereumjs_tx_1.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.maxFeePerGas; } if (tx instanceof ethereumjs_tx_1.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; 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; let inclusionFeePerGas; // EIP-1559 tx if (tx.supports(ethereumjs_tx_1.Capability.EIP1559FeeMarket)) { const baseFee = block.header.baseFeePerGas; inclusionFeePerGas = tx.maxPriorityFeePerGas < tx.maxFeePerGas - baseFee ? tx.maxPriorityFeePerGas : tx.maxFeePerGas - baseFee; gasPrice = inclusionFeePerGas + baseFee; } else { // Have to cast as legacy tx since EIP1559 tx does not have gas price gasPrice = tx.gasPrice; if (this.common.isActivatedEIP(1559) === true) { const baseFee = block.header.baseFeePerGas; inclusionFeePerGas = tx.gasPrice - baseFee; } } // EIP-4844 tx let blobVersionedHashes; if (tx instanceof ethereumjs_tx_1.BlobEIP4844Transaction) { blobVersionedHashes = tx.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 < ethereumjs_util_1.BIGINT_0) { fromAccount.balance = ethereumjs_util_1.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() ? (0, ethereumjs_util_1.bytesToHex)(tx.hash()) : 'unsigned'} with caller=${caller} gasLimit=${gasLimit} to=${to?.toString() ?? 'none'} value=${value} data=${(0, ethereumjs_util_1.short)(data)}`); } const results = (await this.evm.runCall({ block, gasPrice, caller, gasLimit, to, value, data, blobVersionedHashes, })); 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=${(0, ethereumjs_util_1.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 ((0, ethereumjs_tx_1.isBlobEIP4844Tx)(tx)) { results.blobGasUsed = totalblobGas; } // Process any gas refund let gasRefund = results.execResult.gasRefund ?? ethereumjs_util_1.BIGINT_0; results.gasRefund = gasRefund; const maxRefundQuotient = this.common.param('gasConfig', 'maxRefundQuotient'); if (gasRefund !== ethereumjs_util_1.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 ethereumjs_util_1.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() === ethereumjs_common_1.ConsensusType.ProofOfAuthority) { miner = block.header.cliqueSigner(); } else { miner = block.header.coinbase; } let minerAccount = await state.getAccount(miner); if (minerAccount === undefined) { minerAccount = new ethereumjs_util_1.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 ethereumjs_util_1.Address((0, ethereumjs_util_1.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 = []; for (const [address, set] of this.evm.journal.accessList) { const addressPrefixed = '0x' + address; const item = { 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 = { transaction: tx, ...results }; await this._emit('afterTx', event); if (this.DEBUG) { debug(`tx run finished hash=${opts.tx.isSigned() ? (0, ethereumjs_util_1.bytesToHex)(opts.tx.hash()) : 'unsigned'} sender=${caller}`); } return results; } /** * @method txLogsBloom * @private */ function txLogsBloom(logs, common) { const bloom = new index_js_1.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 */ async function generateTxReceipt(tx, txResult, cumulativeGasUsed, blobGasUsed, blobGasPrice) { const baseReceipt = { 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=${(0, ethereumjs_util_1.short)(baseReceipt.bitvector)} (${baseReceipt.bitvector.length} bytes) logs=${baseReceipt.logs.length}`); } if (!tx.supports(ethereumjs_tx_1.Capability.EIP2718TypedTransaction)) { // Legacy transaction if (this.common.gteHardfork(ethereumjs_common_1.Hardfork.Byzantium) === true) { // Post-Byzantium receipt = { status: txResult.execResult.exceptionError !== undefined ? 0 : 1, ...baseReceipt, }; } else { // Pre-Byzantium const stateRoot = await this.stateManager.getStateRoot(); receipt = { stateRoot, ...baseReceipt, }; } } else { // Typed EIP-2718 Transaction if ((0, ethereumjs_tx_1.isBlobEIP4844Tx)(tx)) { receipt = { blobGasUsed, blobGasPrice, status: txResult.execResult.exceptionError ? 0 : 1, ...baseReceipt, }; } else { receipt = { status: txResult.execResult.exceptionError ? 0 : 1, ...baseReceipt, }; } } return receipt; } exports.generateTxReceipt = generateTxReceipt; /** * Internal helper function to create an annotated error message * * @param msg Base error message * @hidden */ function _errorMsg(msg, vm, block, tx) { 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; } //# sourceMappingURL=runTx.js.map