UNPKG

@ethereumjs/evm

Version:
397 lines (355 loc) 13 kB
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 }