@polygon-hermez/vm
Version:
An Ethereum VM implementation
649 lines (589 loc) • 18.7 kB
text/typescript
/* eslint-disable no-undef */
import { debug as createDebugLogger } from 'debug'
import { Address, BN, KECCAK256_NULL, toBuffer } from 'ethereumjs-util'
import { Block } from '@ethereumjs/block'
import { ConsensusType } from '@ethereumjs/common'
import {
AccessList,
AccessListItem,
AccessListEIP2930Transaction,
FeeMarketEIP1559Transaction,
Transaction,
TypedTransaction,
Capability,
} from '@ethereumjs/tx'
import VM from './index'
import Bloom from './bloom'
import { default as EVM, EVMResult } from './evm/evm'
import { short } from './evm/opcodes/util'
import Message from './evm/message'
import TxContext from './evm/txContext'
import { getActivePrecompiles } from './evm/precompiles'
import { EIP2929StateManager } from './state/interface'
import type {
TxReceipt,
BaseTxReceipt,
PreByzantiumTxReceipt,
PostByzantiumTxReceipt,
} from './types'
const debug = createDebugLogger('vm:tx')
const debugGas = createDebugLogger('vm:tx:gas')
/**
* Options for the `runTx` method.
*/
export interface RunTxOpts {
/**
* The `@ethereumjs/block` the `tx` belongs to.
* If omitted, a default blank block will be used.
*/
block?: Block
/**
* An `@ethereumjs/tx` to run
*/
tx: TypedTransaction
/**
* If true, skips the nonce check
*/
skipNonce?: boolean
/**
* If true, skips the balance check
*/
skipBalance?: boolean
/**
* If true, skips the validation of the tx's gas limit
* agains the block's gas limit.
*/
skipBlockGasLimitValidation?: boolean
/**
* If true, adds a generated EIP-2930 access list
* to the `RunTxResult` returned.
*
* Option works with all tx types. EIP-2929 needs to
* be activated (included in `berlin` HF).
*
* Note: if this option is used with a custom {@link StateManager} implementation
* {@link StateManager.generateAccessList} must be implemented.
*/
reportAccessList?: boolean
/**
* To obtain an accurate tx receipt input the block gas used up until this tx.
*/
blockGasUsed?: BN
/**
* GasPrice proposed by the sequencer expresed in percentage of the tx gas price.
*/
effectivePercentage?: Number
/**
* Virtual Counters Manager instance
*/
vcm?: any
}
/**
* Execution result of a transaction
*/
export interface RunTxResult extends EVMResult {
/**
* Bloom filter resulted from transaction
*/
bloom: Bloom
/**
* The amount of ether used by this transaction
*/
amountSpent: BN
/**
* The tx receipt
*/
receipt: TxReceipt
/**
* The amount of gas as that was refunded during the transaction (i.e. `gasUsed = totalGasConsumed - gasRefund`)
*/
gasRefund?: BN
/**
* EIP-2930 access list generated for the tx (see `reportAccessList` option)
*/
accessList?: AccessList
}
export interface AfterTxEvent extends RunTxResult {
/**
* The transaction which just got finished
*/
transaction: TypedTransaction
}
/**
* @ignore
*/
export default async function runTx(this: VM, opts: RunTxOpts): Promise<RunTxResult> {
// tx is required
if (!opts.tx) {
throw new Error('invalid input, tx is required')
}
// create a reasonable default if no block is given
opts.block = opts.block ?? Block.fromBlockData({}, { common: opts.tx.common })
if (
opts.skipBlockGasLimitValidation !== true &&
opts.block.header.gasLimit.lt(opts.tx.gasLimit)
) {
const msg = _errorMsg('tx has a higher gas limit than the block', this, opts.block, opts.tx)
throw new Error(msg)
}
// Have to cast as `EIP2929StateManager` to access clearWarmedAccounts
const state = this.stateManager as EIP2929StateManager
if (opts.reportAccessList && !('generateAccessList' in state)) {
const msg = _errorMsg(
'reportAccessList needs a StateManager implementing the generateAccessList() method',
this,
opts.block,
opts.tx
)
throw new Error(msg)
}
// Ensure we start with a clear warmed accounts Map
if (this._common.isActivatedEIP(2929)) {
state.clearWarmedAccounts()
}
await state.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)) {
// Is it an Access List transaction?
if (!this._common.isActivatedEIP(2930)) {
await state.revert()
const msg = _errorMsg(
'Cannot run transaction: EIP 2930 is not activated.',
this,
opts.block,
opts.tx
)
throw new Error(msg)
}
if (opts.reportAccessList && !('generateAccessList' in state)) {
await state.revert()
const msg = _errorMsg(
'StateManager needs to implement generateAccessList() when running with reportAccessList option',
this,
opts.block,
opts.tx
)
throw new Error(msg)
}
if (opts.tx.supports(Capability.EIP1559FeeMarket) && !this._common.isActivatedEIP(1559)) {
await state.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
castedTx.AccessListJSON.forEach((accessListItem: AccessListItem) => {
const address = toBuffer(accessListItem.address)
state.addWarmedAddress(address)
accessListItem.storageKeys.forEach((storageKey: string) => {
state.addWarmedStorage(address, toBuffer(storageKey))
})
})
}
try {
const result = await _runTx.bind(this)(opts)
await state.commit()
if (this.DEBUG) {
debug(`tx checkpoint committed`)
}
if (this._common.isActivatedEIP(2929) && opts.reportAccessList) {
const { tx } = opts
// Do not include sender address in access list
const removed = [tx.getSenderAddress()]
// Only include to address on present storage slot accesses
const onlyStorage = tx.to ? [tx.to] : []
result.accessList = state.generateAccessList!(removed, onlyStorage)
}
return result
} catch (e: any) {
await state.revert()
if (this.DEBUG) {
debug(`tx checkpoint reverted`)
}
throw e
} finally {
if (this._common.isActivatedEIP(2929)) {
state.clearWarmedAccounts()
}
}
}
async function _runTx(this: VM, opts: RunTxOpts): Promise<RunTxResult> {
// Have to cast as `EIP2929StateManager` to access the EIP2929 methods
const state = this.stateManager as EIP2929StateManager
this.vcm = opts.vcm
let tx = opts.tx
const { 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() ? opts.tx.hash().toString('hex') : 'unsigned'
} sender=${caller}`
)
}
if (this._common.isActivatedEIP(2929)) {
// Add origin and precompiles to warm addresses
getActivePrecompiles(this._common).forEach((address: Address) =>
state.addWarmedAddress(address.buf)
)
state.addWarmedAddress(caller.buf)
if (tx.to) {
// Note: in case we create a contract, we do this in EVMs `_executeCreate` (this is also correct in inner calls, per the EIP)
state.addWarmedAddress(tx.to.buf)
}
}
// Validate gas limit against tx base fee (DataFee + TxFee + Creation Fee)
const txBaseFee = tx.getBaseFee()
const gasLimit = tx.gasLimit.clone()
if (gasLimit.lt(txBaseFee)) {
const msg = _errorMsg('base fee exceeds gas limit', this, block, tx)
throw new Error(msg)
}
gasLimit.isub(txBaseFee)
if (this.DEBUG) {
debugGas(`Subtracting base fee (${txBaseFee}) from gasLimit (-> ${gasLimit})`)
}
if (this._common.isActivatedEIP(1559)) {
// 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.lt(baseFeePerGas)) {
const msg = _errorMsg(
`Transaction's maxFeePerGas (${maxFeePerGas}) is less than the block's baseFeePerGas (${baseFeePerGas})`,
this,
block,
tx
)
throw new Error(msg)
}
}
// Check from account's balance and nonce
let fromAccount = await state.getAccount(caller)
const { nonce, balance } = fromAccount
// EIP-3607: Reject transactions from senders with deployed code
if (this._common.isActivatedEIP(3607) && !fromAccount.codeHash.equals(KECCAK256_NULL)) {
const msg = _errorMsg('invalid sender address, address is not EOA (EIP-3607)', this, block, tx)
throw new Error(msg)
}
if (!opts.skipBalance) {
const cost = tx.getUpfrontCost(block.header.baseFeePerGas)
if (balance.lt(cost)) {
const msg = _errorMsg(
`sender doesn't have enough funds to send tx. The upfront cost is: ${cost} and the sender's account (${caller}) only has: ${balance}`,
this,
block,
tx
)
throw new Error(msg)
}
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`
const cost = tx.gasLimit.mul((tx as FeeMarketEIP1559Transaction).maxFeePerGas).add(tx.value)
if (balance.lt(cost)) {
const msg = _errorMsg(
`sender doesn't have enough funds to send tx. The max cost is: ${cost} and the sender's account (${caller}) only has: ${balance}`,
this,
block,
tx
)
throw new Error(msg)
}
}
}
if (!opts.skipNonce) {
if (!nonce.eq(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(Capability.EIP1559FeeMarket)) {
const baseFee = block.header.baseFeePerGas!
inclusionFeePerGas = BN.min(
(tx as FeeMarketEIP1559Transaction).maxPriorityFeePerGas,
(tx as FeeMarketEIP1559Transaction).maxFeePerGas.sub(baseFee)
)
gasPrice = inclusionFeePerGas.add(baseFee)
} else {
// Have to cast as legacy tx since EIP1559 tx does not have gas price
// Compute effective gas price and update tx
if (typeof opts.effectivePercentage === 'undefined') {
opts.effectivePercentage = 255
}
const effectivegasPrice = (<Transaction>tx).gasPrice
.mul(new BN(Number(opts.effectivePercentage) + 1))
.div(new BN(256))
gasPrice = effectivegasPrice
// Create new Transaction object with updated gas price
tx = new Transaction({
...tx,
gasPrice,
})
if (this._common.isActivatedEIP(1559)) {
const baseFee = block.header.baseFeePerGas!
inclusionFeePerGas = (<Transaction>tx).gasPrice.sub(baseFee)
}
}
// Update from account's nonce and balance
fromAccount.nonce.iaddn(1)
const txCost = tx.gasLimit.mul(gasPrice)
fromAccount.balance.isub(txCost)
await state.putAccount(caller, fromAccount)
if (this.DEBUG) {
debug(
`Update fromAccount (caller) nonce (-> ${fromAccount.nonce}) and balance(-> ${fromAccount.balance})`
)
}
/*
* Execute message
*/
const txContext = new TxContext(gasPrice, caller)
const { value, data, to } = tx
const message = new Message({
caller,
gasLimit,
to,
value,
data,
})
const evm = new EVM(this, txContext, block)
if (this.DEBUG) {
debug(
`Running tx=0x${
tx.isSigned() ? tx.hash().toString('hex') : 'unsigned'
} with caller=${caller} gasLimit=${gasLimit} to=${
to ? to.toString() : ''
} value=${value} data=0x${short(data)}`
)
}
const results = (await evm.executeMessage(message)) as RunTxResult
if (this.DEBUG) {
debug('-'.repeat(100))
debug(
`Received tx results gasUsed=${results.gasUsed} execResult: [ gasUsed=${
results.gasUsed
} exceptionError=${
results.execResult.exceptionError ? results.execResult.exceptionError.error : ''
} returnValue=${short(results.execResult.returnValue)} gasRefund=${
results.execResult.gasRefund
} ]`
)
}
/*
* Parse results
*/
// Generate the bloom for the tx
results.bloom = txLogsBloom(results.execResult.logs)
if (this.DEBUG) {
debug(`Generated tx bloom with logs=${results.execResult.logs?.length}`)
}
// Caculate the total gas used
results.gasUsed.iadd(txBaseFee)
if (this.DEBUG) {
debugGas(`tx add baseFee ${txBaseFee} to gasUsed (-> ${results.gasUsed})`)
}
// Process any gas refund
let gasRefund = results.execResult.gasRefund ?? new BN(0)
const maxRefundQuotient = this._common.param('gasConfig', 'maxRefundQuotient')
if (!gasRefund.isZero()) {
const maxRefund = results.gasUsed.divn(maxRefundQuotient)
gasRefund = BN.min(gasRefund, maxRefund)
results.gasUsed.isub(gasRefund)
if (this.DEBUG) {
debug(`Subtract tx gasRefund (${gasRefund}) from gasUsed (-> ${results.gasUsed})`)
}
} else {
if (this.DEBUG) {
debug(`No tx gasRefund`)
}
}
results.amountSpent = results.gasUsed.mul(gasPrice)
// Update sender's balance
fromAccount = await state.getAccount(caller)
const actualTxCost = results.gasUsed.mul(gasPrice)
const txCostDiff = txCost.sub(actualTxCost)
fromAccount.balance.iadd(txCostDiff)
await state.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) {
// Backwards-compatibilty check
// TODO: can be removed along VM v6 release
if ('cliqueSigner' in block.header) {
miner = block.header.cliqueSigner()
} else {
miner = Address.zero()
}
} else {
miner = block.header.coinbase
}
const minerAccount = await state.getAccount(miner)
// add the amount spent on gas to the miner's account
if (this._common.isActivatedEIP(1559)) {
minerAccount.balance.iadd(results.gasUsed.mul(<BN>inclusionFeePerGas))
} else {
minerAccount.balance.iadd(results.amountSpent)
}
// 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 state.putAccount(miner, minerAccount)
if (this.DEBUG) {
debug(`tx update miner account (${miner}) balance (-> ${minerAccount.balance})`)
}
/*
* Cleanup accounts
*/
if (results.execResult.selfdestruct) {
const keys = Object.keys(results.execResult.selfdestruct)
for (const k of keys) {
const address = new Address(Buffer.from(k, 'hex'))
await state.deleteAccount(address)
if (this.DEBUG) {
debug(`tx selfdestruct on address=${address}`)
}
}
}
await state.cleanupTouchedAccounts()
state.clearOriginalStorageCache()
// Generate the tx receipt
const cumulativeGasUsed = (opts.blockGasUsed ?? block.header.gasUsed).add(results.gasUsed)
results.receipt = await generateTxReceipt.bind(this)(tx, results, cumulativeGasUsed)
/**
* 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() ? opts.tx.hash().toString('hex') : 'unsigned'
} sender=${caller}`
)
}
return results
}
/**
* @method txLogsBloom
* @private
*/
function txLogsBloom(logs?: any[]): Bloom {
const bloom = new Bloom()
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
*/
export async function generateTxReceipt(
this: VM,
tx: TypedTransaction,
txResult: RunTxResult,
cumulativeGasUsed: BN
): Promise<TxReceipt> {
const baseReceipt: BaseTxReceipt = {
gasUsed: cumulativeGasUsed.toArrayLike(Buffer),
bitvector: txResult.bloom.bitvector,
logs: txResult.execResult.logs ?? [],
}
let receipt
if (this.DEBUG) {
debug(
`Generate tx receipt transactionType=${
tx.type
} gasUsed=${cumulativeGasUsed} bitvector=${short(baseReceipt.bitvector)} (${
baseReceipt.bitvector.length
} bytes) logs=${baseReceipt.logs.length}`
)
}
if (!tx.supports(Capability.EIP2718TypedTransaction)) {
// Legacy transaction
if (this._common.gteHardfork('byzantium')) {
// Post-Byzantium
receipt = {
status: txResult.execResult.exceptionError ? 0 : 1, // Receipts have a 0 as status on error
...baseReceipt,
} as PostByzantiumTxReceipt
} else {
// Pre-Byzantium
const stateRoot = await this.stateManager.getStateRoot(true)
receipt = {
stateRoot: stateRoot,
...baseReceipt,
} as PreByzantiumTxReceipt
}
} else {
// Typed EIP-2718 Transaction
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
}