@nomicfoundation/ethereumjs-vm
Version:
An Ethereum VM implementation
578 lines • 24.6 kB
JavaScript
;
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