@ethereumjs/evm
Version:
JavaScript Ethereum Virtual Machine (EVM) implementation
397 lines (355 loc) • 13 kB
text/typescript
import { Hardfork } from '@ethereumjs/common'
import { BIGINT_0, BIGINT_31, BIGINT_32 } from '@ethereumjs/util'
import { activeCostPerStateByte } from '../eip8037.ts'
import { EVMError } from '../errors.ts'
import {
accessAddressEIP2929,
addAddressToBAL,
getAddressAccessCost,
warmAddress,
} from './EIP2929.ts'
import { maxCallGas, subMemUsage, trap, writeCallOutput } from './util.ts'
import type { Common } from '@ethereumjs/common'
import type { Address } from '@ethereumjs/util'
import type { RunState } from '../interpreter.ts'
/** Set by the gas handler; consumed in {@link Interpreter.runStep} after gas is charged. */
export type Eip7928PostTargetCallOog = {
outOffset: bigint
outLength: bigint
}
/** True when `regularGas + stateGas` fits in `gas_left + state_gas_reservoir` (EIP-8037). */
export function canAfford(runState: RunState, regularGas: bigint, stateGas = BIGINT_0): boolean {
const gasLeft = runState.interpreter.getGasLeft()
const reservoir = runState.interpreter._evm.stateGasReservoir
return regularGas + stateGas <= gasLeft + reservoir
}
export function newAccountStateGasCost(common: Common, runState: RunState): bigint {
if (!common.isActivatedEIP(8037)) {
return BIGINT_0
}
const stateBytesPerNewAccount = common.param('stateBytesPerNewAccount')
const blockGasLimit = runState.env.block.header.gasLimit
const costPerStateByte = activeCostPerStateByte(common, blockGasLimit)
return stateBytesPerNewAccount * costPerStateByte
}
/** Pre-target OOG: trap before any target access / BAL entry. */
export function eip7928TrapPreTarget(runState: RunState, common: Common, gas: bigint): void {
if (common.isActivatedEIP(7928) && gas > runState.interpreter.getGasLeft()) {
trap(EVMError.errorMessages.OUT_OF_GAS)
}
}
/** Post-target CALL OOG: burn remaining frame gas and exceptional-halt in runStep. */
export function eip7928SchedulePostTargetCallOog(
runState: RunState,
outOffset: bigint,
outLength: bigint,
): bigint {
runState.eip7928PostTargetCallOog = { outOffset, outLength }
runState.interpreter._evm.eip7928CallPostTargetOog = true
return runState.interpreter.getGasLeft()
}
/** Post-target CREATE OOG: charge accrued gas only; opcode pushes 0. */
export function eip7928PostTargetCreateOog(
runState: RunState,
common: Common,
gas: bigint,
): bigint {
if (!common.isActivatedEIP(7928)) {
trap(EVMError.errorMessages.OUT_OF_GAS)
}
runState.messageGasLimit = BIGINT_0
runState.eip7928PostTargetCreateOog = true
return gas
}
/** Post-target CALL OOG when 7928 is off (legacy: trap in gas handler). */
function postTargetCallOogOrTrap(
runState: RunState,
common: Common,
outOffset: bigint,
outLength: bigint,
): bigint {
if (!common.isActivatedEIP(7928)) {
trap(EVMError.errorMessages.OUT_OF_GAS)
}
return eip7928SchedulePostTargetCallOog(runState, outOffset, outLength)
}
export function consumeEip7928PostTargetCallOog(runState: RunState): void {
const pending = runState.eip7928PostTargetCallOog
if (pending === undefined) {
return
}
runState.eip7928PostTargetCallOog = undefined
writeCallOutput(runState, pending.outOffset, pending.outLength)
trap(EVMError.errorMessages.OUT_OF_GAS)
}
export async function getCallNewAccountPostCosts(
runState: RunState,
common: Common,
toAddress: Address,
): Promise<{ regular: bigint; state: bigint; createsNewAccount: boolean }> {
let createsNewAccount = false
if (common.gteHardfork(Hardfork.SpuriousDragon)) {
const account = await runState.stateManager.getAccount(toAddress)
if (account === undefined || account.isEmpty()) {
createsNewAccount = true
}
} else if ((await runState.stateManager.getAccount(toAddress)) === undefined) {
createsNewAccount = true
}
if (!createsNewAccount) {
return { regular: BIGINT_0, state: BIGINT_0, createsNewAccount: false }
}
return {
regular: common.param('callNewAccountGas'),
state: newAccountStateGasCost(common, runState),
createsNewAccount: true,
}
}
function commitTargetAccess(runState: RunState, toAddress: Address, common: Common): void {
if (common.isActivatedEIP(2929)) {
warmAddress(runState, toAddress.bytes)
}
addAddressToBAL(runState, toAddress.bytes, common)
}
function finalizeCallMessageGas(
runState: RunState,
common: Common,
gas: bigint,
currentGasLimit: bigint,
newAccountStateGas: bigint,
outOffset: bigint,
outLength: bigint,
): bigint {
const gasLimit = maxCallGas(
currentGasLimit,
runState.interpreter.getGasLeft() - gas,
runState,
common,
)
if (common.isActivatedEIP(7928)) {
if (!canAfford(runState, gas, newAccountStateGas)) {
return eip7928SchedulePostTargetCallOog(runState, outOffset, outLength)
}
}
if (gasLimit > runState.interpreter.getGasLeft() - gas) {
return postTargetCallOogOrTrap(runState, common, outOffset, outLength)
}
if (gas > runState.interpreter.getGasLeft()) {
return postTargetCallOogOrTrap(runState, common, outOffset, outLength)
}
// EIP-8037: pre-charge the new-account state-gas BEFORE the inner call
// runs. This matches the EELS amsterdam (tests-bal) reference, where
// `charge_state_gas(STATE_BYTES_PER_NEW_ACCOUNT * COST_PER_STATE_BYTE)`
// happens in the CALL opcode body before the sub-frame is processed and
// before the insufficient-balance check. The charge is unconditional
// (per spec it sticks even when the sub-call later fails for insufficient
// balance or reverts), so we charge here rather than after frame exit.
// The post-frame charge in evm.ts is skipped under EIP-8037 to avoid
// double-counting (see `_executeCall` post-execution block).
if (common.isActivatedEIP(8037) && newAccountStateGas > BIGINT_0) {
runState.interpreter.chargeStateGas(newAccountStateGas, 'CALL pre-charge new_account')
}
runState.messageGasLimit = gasLimit
return gas
}
export type CallFamilyGasOpts = {
currentGasLimit: bigint
toAddress: Address
value: bigint
inOffset: bigint
inLength: bigint
outOffset: bigint
outLength: bigint
includeValueTransfer: boolean
includeNewAccountPostCheck: boolean
eip7702GetAccessCost: (
runState: RunState,
common: Common,
address: Address,
charge2929Gas: boolean,
) => Promise<{ gas: bigint; delegationAddress: Uint8Array | null }>
eip7702WarmAddress: (runState: RunState, delegationAddress: Uint8Array) => void
}
/**
* Shared EIP-2929 / EIP-7928 / EIP-8037 gas path for CALL, CALLCODE, DELEGATECALL, STATICCALL.
*/
export async function callFamilyGas(
runState: RunState,
gas: bigint,
common: Common,
opts: CallFamilyGasOpts,
): Promise<bigint> {
const {
currentGasLimit,
toAddress,
value,
inOffset,
inLength,
outOffset,
outLength,
includeValueTransfer,
includeNewAccountPostCheck,
eip7702GetAccessCost,
eip7702WarmAddress,
} = opts
gas += subMemUsage(runState, inOffset, inLength, common)
gas += subMemUsage(runState, outOffset, outLength, common)
eip7928TrapPreTarget(runState, common, gas)
let charge2929Gas = true
if (
(common.isActivatedEIP(6800) || common.isActivatedEIP(7864)) &&
runState.interpreter._evm.getPrecompile(toAddress) === undefined
) {
const coldAccessGas = runState.env.accessWitness!.readAccountBasicData(toAddress)
if (includeValueTransfer && value !== BIGINT_0) {
const contractAddress = runState.interpreter.getAddress()
gas += runState.env.accessWitness!.writeAccountBasicData(contractAddress)
gas += runState.env.accessWitness!.writeAccountBasicData(toAddress)
}
gas += coldAccessGas
charge2929Gas = coldAccessGas === BIGINT_0
}
if (common.isActivatedEIP(2929)) {
gas += getAddressAccessCost(runState, toAddress.bytes, common, charge2929Gas)
}
let valueTransferGas = BIGINT_0
if (
includeValueTransfer &&
value !== BIGINT_0 &&
!common.isActivatedEIP(6800) &&
!common.isActivatedEIP(7864)
) {
valueTransferGas = common.param('callValueTransferGas')
}
eip7928TrapPreTarget(runState, common, gas + valueTransferGas)
commitTargetAccess(runState, toAddress, common)
gas += valueTransferGas
let newAccountStateGas = BIGINT_0
// SpuriousDragon+ charges the new-account fee only on value transfers, but
// pre-SpuriousDragon charges it for any call to a non-existent account.
if (
includeNewAccountPostCheck &&
(value !== BIGINT_0 || !common.gteHardfork(Hardfork.SpuriousDragon))
) {
const { regular, state } = await getCallNewAccountPostCosts(runState, common, toAddress)
gas += regular
newAccountStateGas = state
}
if (common.isActivatedEIP(7702)) {
const { gas: delegationGas, delegationAddress } = await eip7702GetAccessCost(
runState,
common,
toAddress,
charge2929Gas,
)
gas += delegationGas
if (delegationAddress !== null) {
if (common.isActivatedEIP(7928)) {
if (gas > runState.interpreter.getGasLeft()) {
return postTargetCallOogOrTrap(runState, common, outOffset, outLength)
}
eip7702WarmAddress(runState, delegationAddress)
addAddressToBAL(runState, delegationAddress, common)
} else {
eip7702WarmAddress(runState, delegationAddress)
}
}
}
return finalizeCallMessageGas(
runState,
common,
gas,
currentGasLimit,
newAccountStateGas,
outOffset,
outLength,
)
}
export type Create7928GasOpts = {
offset: bigint
length: bigint
salt?: Uint8Array
/** Extra pre-initcode gas (CREATE2 keccak). */
extraPreTargetGas?: bigint
preChargeLabel: string
}
/**
* Shared EIP-7928 / EIP-8037 gas path for CREATE and CREATE2.
*/
export async function create7928Gas(
runState: RunState,
gas: bigint,
common: Common,
opts: Create7928GasOpts,
getCreateTargetAddressBytes: (
runState: RunState,
initCode: Uint8Array,
salt?: Uint8Array,
) => Promise<Uint8Array>,
): Promise<bigint> {
const { offset, length, salt, extraPreTargetGas = BIGINT_0, preChargeLabel } = opts
// EIP-3860: reject oversized initcode BEFORE any state-gas pre-charge so
// that EIP-8037's reservoir is preserved when the size check fails. This
// matches the EELS amsterdam (tests-bal) reference, which raises
// OutOfGasError from `generic_create` ahead of `charge_state_gas`. Without
// this, the new-account state-gas pre-charge runs first and is hard to
// unwind cleanly at the tx-root frame (depth=0): the frame-revert handler
// refunds the spilled slice to a parent reservoir that doesn't exist, and
// the user is overcharged by `STATE_BYTES_PER_NEW_ACCOUNT * costPerStateByte`.
if (
common.isActivatedEIP(3860) &&
length > Number(common.param('maxInitCodeSize')) &&
!runState.interpreter._evm.allowUnlimitedInitCodeSize
) {
trap(EVMError.errorMessages.INITCODE_SIZE_VIOLATION)
}
gas += subMemUsage(runState, offset, length, common)
if (common.isActivatedEIP(3860)) {
gas += ((length + BIGINT_31) / BIGINT_32) * common.param('initCodeWordGas')
}
gas += extraPreTargetGas
eip7928TrapPreTarget(runState, common, gas)
let initCode = new Uint8Array(0)
if (length !== BIGINT_0) {
initCode = runState.memory.read(Number(offset), Number(length), true)
}
const targetAddress = await getCreateTargetAddressBytes(runState, initCode, salt)
const newAccountStateGas = newAccountStateGasCost(common, runState)
if (common.isActivatedEIP(8037)) {
if (!canAfford(runState, gas, newAccountStateGas)) {
trap(EVMError.errorMessages.OUT_OF_GAS)
}
}
if (common.isActivatedEIP(2929)) {
warmAddress(runState, targetAddress)
}
if (common.isActivatedEIP(2929)) {
gas += accessAddressEIP2929(runState, runState.interpreter.getAddress().bytes, common, false)
}
if (common.isActivatedEIP(8037)) {
if (!canAfford(runState, gas, newAccountStateGas)) {
addAddressToBAL(runState, targetAddress, common)
return eip7928PostTargetCreateOog(runState, common, gas)
}
if (gas > runState.interpreter.getGasLeft()) {
addAddressToBAL(runState, targetAddress, common)
return eip7928PostTargetCreateOog(runState, common, gas)
}
if (gas > BIGINT_0) {
runState.interpreter.useGas(gas, preChargeLabel)
gas = BIGINT_0
}
runState.interpreter.chargeStateGas(newAccountStateGas, `${preChargeLabel} new_account`)
} else if (gas > runState.interpreter.getGasLeft()) {
addAddressToBAL(runState, targetAddress, common)
return eip7928PostTargetCreateOog(runState, common, gas)
}
let gasLimit = BigInt(runState.interpreter.getGasLeft()) - gas
gasLimit = maxCallGas(gasLimit, gasLimit, runState, common)
if (gas > runState.interpreter.getGasLeft()) {
addAddressToBAL(runState, targetAddress, common)
return eip7928PostTargetCreateOog(runState, common, gas)
}
runState.messageGasLimit = gasLimit
return gas
}