diff --git a/execution_chain/core/eip8037.nim b/execution_chain/core/eip8037.nim index 99621e212e..bf65ff6ba6 100644 --- a/execution_chain/core/eip8037.nim +++ b/execution_chain/core/eip8037.nim @@ -8,8 +8,11 @@ # those terms {.push raises: [].} + import - eth/common/base + eth/common/base, + stew/bitops2, + intops/ops/[sub, add, mul, division] const STATE_BYTES_PER_NEW_ACCOUNT* = 112 @@ -17,5 +20,30 @@ const REGULAR_PER_AUTH_BASE_COST* = 7500 STATE_BYTES_PER_STORAGE_SET* = 32 + COST_NUMERATOR_MULTIPLIER = 2_628_000'u64 + TARGET_STATE_GROWTH_PER_YEAR = 100 * 1024 * 1024 * 1024 + COST_DENOMINATOR = 2 * TARGET_STATE_GROWTH_PER_YEAR + CPSB_SIGNIFICANT_BITS = 5 + CPSB_OFFSET = 9578'u64 + +func calculateRaw(hi, lo: uint64): uint64 = + # ulong raw = (ulong)((numerator + CostDenominator - 1) / CostDenominator); + var + hi = hi + (num, carry) = overflowingAdd(lo, COST_DENOMINATOR) + if carry: inc(hi) + let (lo, c) = overflowingSub(num, 1) + if c: dec(hi) + let (q, _) = narrowingDiv(hi, lo, COST_DENOMINATOR) + q + func stateGasPerByte*(gasLimit: GasInt): GasInt = - return 1174.GasInt + let + (num_hi, num_lo) = wideningMul(gasLimit, COST_NUMERATOR_MULTIPLIER) + raw = calculateRaw(num_hi, num_lo) + shifted = raw + CPSB_OFFSET + shift = max(64 - leadingZeros(shifted) - CPSB_SIGNIFICANT_BITS, 0) + rounded = (shifted shr shift) shl shift + quantized = if rounded > CPSB_OFFSET: rounded - CPSB_OFFSET else: 0 + + max(quantized, 1) diff --git a/execution_chain/core/executor/process_transaction.nim b/execution_chain/core/executor/process_transaction.nim index 6ce2b22db1..a1b2e6c0d9 100644 --- a/execution_chain/core/executor/process_transaction.nim +++ b/execution_chain/core/executor/process_transaction.nim @@ -17,6 +17,7 @@ import ../../db/ledger, ../../transaction/call_evm, ../../transaction/call_common, + ../../transaction/call_types, ../../transaction, ../../evm/state, ../../evm/types, @@ -111,13 +112,30 @@ proc processTransaction*( priorityFee = min(tx.maxPriorityFeePerGasNorm(), tx.maxFeePerGasNorm() - baseFee) excessBlobGas = vmState.blockCtx.excessBlobGas regularGasAvailable = vmState.blockCtx.gasLimit - vmState.blockRegularGasUsed + stateGasAvailable = vmState.blockCtx.gasLimit - vmState.blockStateGasUsed + intrinsic = tx.intrinsicGas(fork, vmState.blockCtx.gasLimit) + com = vmState.com - # Regular gas is capped at TX_MAX_GAS_LIMIT per EIP-7825. - # State gas is not checked per-tx; block-end validation enforces - # max(block_regular_gas_used, block_state_gas_used) <= gas_limit. - if min(TX_GAS_LIMIT.GasInt, tx.gasLimit) > regularGasAvailable: + # Per-tx 2D gas inclusion check: for each dimension the worst-case + # contribution must fit in the remaining budget. Block-end + # validation still enforces + if fork < FkAmsterdam: let want = min(TX_GAS_LIMIT.GasInt, tx.gasLimit) - return err("regular gas used exceeds limit want: " & $want & ", available: " & $regularGasAvailable) + if want > regularGasAvailable: + return err("regular gas used exceeds limit, want: " & $want & ", available: " & $regularGasAvailable) + else: + # https://github.com/ethereum/execution-specs/pull/2703/changes + # Worst-case regular contribution: tx.gasLimit minus the portion that + # must go to intrinsic state gas, capped at TX_MAX_GAS_LIMIT. + let want = min(TX_GAS_LIMIT.GasInt, tx.gasLimit - intrinsic.state) + if want > regularGasAvailable: + return err("regular gas used exceeds limit, want: " & $want & ", available: " & $regularGasAvailable) + + # Worst-case state contribution: tx.gasLimit minus the portion that + # must go to intrinsic regular gas. + let stateGas = tx.gasLimit - intrinsic.regular + if stateGas > stateGasAvailable: + return err("state gas used exceeds limit, want: " & $stateGas & ", available: " & $stateGasAvailable) # blobGasUsed will be added to vmState.blobGasUsed if the tx is ok. let @@ -127,6 +145,8 @@ proc processTransaction*( return err("blobGasUsed " & $blobGasUsed & " exceeds maximum allowance " & $maxBlobGasPerBlock) + ? validateTxBasic(com, tx, intrinsic, fork) + # Actually, the EIP-1559 reference does not mention an early exit. # # Even though database was not changed yet but, a `persist()` directive @@ -134,7 +154,6 @@ proc processTransaction*( # of the `processTransaction()` function. So there is no `return err()` # statement, here. let - com = vmState.com txRes = roDB.validateTransaction(tx, sender, vmState.blockCtx.gasLimit, baseFee256, excessBlobGas, com, fork) res = if txRes.isOk: # Execute the transaction. @@ -144,7 +163,7 @@ proc processTransaction*( vmState.balTracker.beginCallFrame() let savePoint = vmState.ledger.beginSavePoint() - var callResult = tx.txCallEvm(sender, vmState, baseFee) + var callResult = tx.txCallEvm(sender, vmState, baseFee, intrinsic) vmState.captureTxEnd(tx.gasLimit - callResult.gasUsed) let tmp = commitOrRollbackDependingOnGasUsed( diff --git a/execution_chain/core/tx_pool/tx_desc.nim b/execution_chain/core/tx_pool/tx_desc.nim index da0d899204..7b9e765ac9 100644 --- a/execution_chain/core/tx_pool/tx_desc.nim +++ b/execution_chain/core/tx_pool/tx_desc.nim @@ -23,6 +23,7 @@ import ../../db/ledger, ../../constants, ../../transaction, + ../../transaction/call_types, ../../core/eip8037, ../chain/forked_chain, ../pow/header, @@ -365,10 +366,13 @@ proc addTx*(xp: TxPoolRef, ptx: PooledTransaction): Result[void, TxError] = debug "Transaction already known", txHash = id return err(txErrorAlreadyKnown) + let + intrinsic = ptx.tx.intrinsicGas(xp.nextFork, xp.gasLimit) + validateTxBasic( xp.com, ptx.tx, - xp.gasLimit, + intrinsic, xp.nextFork, validateFork = true).isOkOr: debug "Invalid transaction: Basic validation failed", diff --git a/execution_chain/core/validate.nim b/execution_chain/core/validate.nim index d34b34e8b2..5d4c15f537 100644 --- a/execution_chain/core/validate.nim +++ b/execution_chain/core/validate.nim @@ -239,7 +239,7 @@ func gasCost*(tx: Transaction): UInt256 = func validateTxBasic*( com: CommonRef, tx: Transaction; ## tx to validate - gasLimit: GasInt; + intrinsic:IntrinsicGas; fork: EVMFork, validateFork: bool = true): Result[void, string] = @@ -269,7 +269,6 @@ func validateTxBasic*( if fork >= FkAmsterdam: let - intrinsic = tx.intrinsicGas(fork, gasLimit) intrinsicGas = intrinsic.regular + intrinsic.state minGasLimit = max(intrinsicGas, intrinsic.floorDataGas) minRegularGasLimit = max(intrinsic.regular, intrinsic.floorDataGas) @@ -285,7 +284,6 @@ func validateTxBasic*( return err("tx.gasLimit " & $tx.gasLimit & " exceeds maximum " & $TX_GAS_LIMIT) let - intrinsic = tx.intrinsicGas(fork, gasLimit) minGasLimit = max(intrinsic.regular, intrinsic.floorDataGas) if tx.gasLimit < minGasLimit: @@ -355,8 +353,6 @@ proc validateTransaction*( com: CommonRef, fork: EVMFork): Result[void, string] = - ? validateTxBasic(com, tx, gasLimit, fork) - let balance = ledger.getBalance(sender) nonce = ledger.getNonce(sender) diff --git a/execution_chain/db/ledger.nim b/execution_chain/db/ledger.nim index a29680c615..ce5215a186 100644 --- a/execution_chain/db/ledger.nim +++ b/execution_chain/db/ledger.nim @@ -111,6 +111,10 @@ type selfDestruct: HashSet[Address] accessList: ac_access_list.AccessList + SelfDestructRefund* = object + createdSlots*: int + codeLen*: int + const resetFlags = { Dirty, @@ -328,6 +332,31 @@ proc makeDirty(ledger: LedgerRef, address: Address, cloneStorage = true): Accoun ledger.savePoint.cache[address] = result ledger.savePoint.dirty[address] = result +template getCodeSizeImpl(ledger: LedgerRef, acc: AccountRef): int = + if acc.code == nil: + if acc.statement.codeHash == EMPTY_CODE_HASH: + return 0 + acc.code = ledger.code.get(acc.statement.codeHash).valueOr: + # On a cache miss, we don't fetch the code - instead, we fetch just the + # length - should the code itself be needed, it will typically remain + # cached and easily accessible in the database layer - this is to prevent + # EXTCODESIZE calls from messing up the code cache and thus causing + # recomputation of the jump destination table + var rc = ledger.txFrame.len(contractHashKey(acc.statement.codeHash).toOpenArray) + + return rc.valueOr: + warn logTxt "getCodeSize()", codeHash=acc.statement.codeHash, error=($$rc.error) + 0 + + acc.code.len() + +proc getCodeSize(ledger: LedgerRef, acc: AccountRef): int = + getCodeSizeImpl(ledger, acc) + +proc calcCreatedSlots(ledger: LedgerRef, acc: AccountRef): int = + for _, val in acc.overlayStorage: + inc(result, val.isZero.not.int) + # ------------------------------------------------------------------------------ # Public methods # ------------------------------------------------------------------------------ @@ -454,23 +483,7 @@ proc getCodeSize*(ledger: LedgerRef, address: Address): int = let acc = ledger.getAccount(address, false) if acc.isNil: return 0 - - if acc.code == nil: - if acc.statement.codeHash == EMPTY_CODE_HASH: - return 0 - acc.code = ledger.code.get(acc.statement.codeHash).valueOr: - # On a cache miss, we don't fetch the code - instead, we fetch just the - # length - should the code itself be needed, it will typically remain - # cached and easily accessible in the database layer - this is to prevent - # EXTCODESIZE calls from messing up the code cache and thus causing - # recomputation of the jump destination table - var rc = ledger.txFrame.len(contractHashKey(acc.statement.codeHash).toOpenArray) - - return rc.valueOr: - warn logTxt "getCodeSize()", codeHash=acc.statement.codeHash, error=($$rc.error) - 0 - - acc.code.len() + getCodeSizeImpl(ledger, acc) proc resolveCode*(ledger: LedgerRef, address: Address): CodeBytesRef = let code = ledger.getCode(address) @@ -638,6 +651,17 @@ iterator nonZeroSelfDestructAccounts*(ledger: LedgerRef): (Address, UInt256) = continue yield (address, value) +iterator newlyCreatedSelfDestructRefund*(ledger: LedgerRef): SelfDestructRefund = + for address in ledger.savePoint.selfDestruct: + let acc = ledger.getAccount(address, false) + doAssert(acc.isNil.not) + if NewlyCreated notin acc.flags: + continue + yield SelfDestructRefund( + createdSlots: calcCreatedSlots(ledger, acc), + codeLen: getCodeSize(ledger, acc), + ) + proc ripemdSpecial*(ledger: LedgerRef) = ledger.ripemdSpecial = true diff --git a/execution_chain/evm/interpreter/gas_costs.nim b/execution_chain/evm/interpreter/gas_costs.nim index 85f2bb86d9..b1d6218420 100644 --- a/execution_chain/evm/interpreter/gas_costs.nim +++ b/execution_chain/evm/interpreter/gas_costs.nim @@ -71,7 +71,7 @@ type nonZeroVal*: bool gasCost1*: GasInt isNewAccount*: proc(): bool {.gcsafe, raises: [].} - gasLeft*: GasInt + gasLeft*: GasInt gasCallDelegate*: GasProc contractGas*: UInt256 @@ -101,6 +101,7 @@ type gasCost*: GasInt gasRefund*: int64 stateGas*: GasInt + creditStateGas*: GasInt CallGasResult = tuple[gasCost, childGasLimit: GasInt] @@ -329,7 +330,9 @@ template gasCosts(fork: EVMFork, prefix, ResultGasCostsName: untyped) = if params.originalValue == value: if params.originalValue.isZero: # reset to original inexistent slot (2.2.2.1) when fork >= FkAmsterdam: - res.gasRefund += params.stateGasStorageSet.int64 + CleanRefund + # https://github.com/ethereum/execution-specs/pull/2698/changes + res.creditStateGas = params.stateGasStorageSet + res.gasRefund += CleanRefund else: res.gasRefund += InitRefund else: # reset to original existing slot (2.2.2.2) diff --git a/execution_chain/evm/interpreter/gas_meter.nim b/execution_chain/evm/interpreter/gas_meter.nim index daef1f5251..df2d801b22 100644 --- a/execution_chain/evm/interpreter/gas_meter.nim +++ b/execution_chain/evm/interpreter/gas_meter.nim @@ -90,3 +90,23 @@ func checkGas*(gasMeter: GasMeter, cost, amount: GasInt): EvmResultVoid = if amount > gasMeter.stateGasLeft + gasMeter.gasRemaining - cost: return err(gasErr(OutOfGas)) ok() + +func returnAllStateGas*(gasMeter: var GasMeter) = + gasMeter.stateGasLeft += gasMeter.stateGasUsed + gasMeter.stateGasUsed = 0 + +# https://github.com/ethereum/execution-specs/pull/2733/changes +func creditStateGasRefund*(gasMeter: var GasMeter; amount: GasInt) = + let applied = min(amount, gasMeter.stateGasUsed) + gasMeter.stateGasLeft += applied + gasMeter.stateGasUsed -= applied + gasMeter.stateGasRefund += applied + gasMeter.stateGasRefundPending += amount - applied + +func appendStateGasRefund*(gasMeter: var GasMeter; amount: GasInt) = + gasMeter.stateGasRefund += amount + +func selfDestructRefund*(gasMeter: var GasMeter; amount: GasInt) = + let applied = min(amount, gasMeter.stateGasUsed) + gasMeter.stateGasLeft += applied + gasMeter.stateGasUsed -= applied diff --git a/execution_chain/evm/interpreter/op_handlers/oph_call.nim b/execution_chain/evm/interpreter/op_handlers/oph_call.nim index 417e1edc0a..35c7982271 100644 --- a/execution_chain/evm/interpreter/op_handlers/oph_call.nim +++ b/execution_chain/evm/interpreter/op_handlers/oph_call.nim @@ -197,6 +197,9 @@ proc execSubCall(c: Computation; childMsg: Message; memPos, memLen: int) = if child.isSuccess: c.gasMeter.returnStateGas(child.gasMeter.stateGasLeft) c.gasMeter.appendStateGasUsed(child.gasMeter.stateGasUsed) + c.gasMeter.appendStateGasRefund(child.gasMeter.stateGasRefund) + # https://github.com/ethereum/execution-specs/pull/2733/changes + c.gasMeter.creditStateGasRefund(child.gasMeter.stateGasRefundPending) c.merge(child) c.stack.lsTop(1) else: @@ -204,7 +207,7 @@ proc execSubCall(c: Computation; childMsg: Message; memPos, memLen: int) = # so no state was actually grown. All state gas, both reservoir and any # that spilled into `gas_left`, is restored to the parent's reservoir and # the child's `state_gas_used` is not accumulated. - c.gasMeter.returnStateGas(child.gasMeter.stateGasUsed + child.gasMeter.stateGasLeft) + c.gasMeter.returnStateGas(child.gasMeter.stateGasUsed + child.gasMeter.stateGasLeft - child.gasMeter.stateGasRefund) let actualOutputSize = min(memLen, child.output.len) if actualOutputSize > 0: diff --git a/execution_chain/evm/interpreter/op_handlers/oph_create.nim b/execution_chain/evm/interpreter/op_handlers/oph_create.nim index d09b99f0cc..810d7e80ff 100644 --- a/execution_chain/evm/interpreter/op_handlers/oph_create.nim +++ b/execution_chain/evm/interpreter/op_handlers/oph_create.nim @@ -56,6 +56,9 @@ proc execSubCreate(c: Computation; childMsg: Message; if child.isSuccess: c.gasMeter.returnStateGas(child.gasMeter.stateGasLeft) c.gasMeter.appendStateGasUsed(child.gasMeter.stateGasUsed) + c.gasMeter.appendStateGasRefund(child.gasMeter.stateGasRefund) + # https://github.com/ethereum/execution-specs/pull/2733/changes + c.gasMeter.creditStateGasRefund(child.gasMeter.stateGasRefundPending) c.merge(child) c.stack.lsTop child.msg.contractAddress else: @@ -63,7 +66,13 @@ proc execSubCreate(c: Computation; childMsg: Message; # so no state was actually grown. All state gas, both reservoir and any # that spilled into `gas_left`, is restored to the parent's reservoir and # the child's `state_gas_used` is not accumulated. - c.gasMeter.returnStateGas(child.gasMeter.stateGasUsed + child.gasMeter.stateGasLeft) + c.gasMeter.returnStateGas(child.gasMeter.stateGasUsed + child.gasMeter.stateGasLeft - child.gasMeter.stateGasRefund) + + # https://github.com/ethereum/execution-specs/pull/2733/changes + if c.fork >= FkAmsterdam: + let createAccountStateGas = STATE_BYTES_PER_NEW_ACCOUNT * c.getCostPerStateByte + c.gasMeter.creditStateGasRefund(createAccountStateGas) + if not child.error.burnsGas: # Means return was `REVERT`. # From create, only use `outputData` if child returned with `REVERT`. c.returnData = move(child.output) @@ -98,6 +107,7 @@ proc createOp(cpt: VmCpt): EvmResultVoid = memOffset: memPos, memLength: memLen) gasCost = cpt.gasCosts[Create].cr_handler(false, gasParams) + createAccountStateGas = STATE_BYTES_PER_NEW_ACCOUNT * cpt.getCostPerStateByte ? cpt.opcodeGasCost(Create, gasCost, reason = "CREATE: GasCreate + memLen * memory expansion") @@ -114,7 +124,7 @@ proc createOp(cpt: VmCpt): EvmResultVoid = # Charge state gas after initcode size validation # https://github.com/ethereum/execution-specs/commit/b9f0afa931a773cdb764310035d0ff383ebecf9e - ? cpt.gasMeter.chargeStateGas(STATE_BYTES_PER_NEW_ACCOUNT * cpt.getCostPerStateByte, + ? cpt.gasMeter.chargeStateGas(createAccountStateGas, reason = "CREATE: State gas new account") elif memLen > EIP3860_MAX_INITCODE_SIZE: # EIP-3860 @@ -129,6 +139,9 @@ proc createOp(cpt: VmCpt): EvmResultVoid = reason = "Stack too deep", maxDepth = MaxCallDepth, depth = cpt.msg.depth + # https://github.com/ethereum/execution-specs/pull/2733/changes + if cpt.fork >= FkAmsterdam: + cpt.gasMeter.creditStateGasRefund(createAccountStateGas) return ok() if endowment != 0: @@ -138,6 +151,9 @@ proc createOp(cpt: VmCpt): EvmResultVoid = reason = "Insufficient funds available to transfer", required = endowment, balance = senderBalance + # https://github.com/ethereum/execution-specs/pull/2733/changes + if cpt.fork >= FkAmsterdam: + cpt.gasMeter.creditStateGasRefund(createAccountStateGas) return ok() var createMsgGas = cpt.gasMeter.gasRemaining @@ -190,6 +206,7 @@ proc create2Op(cpt: VmCpt): EvmResultVoid = currentMemSize: cpt.memory.len, memOffset: memPos, memLength: memLen) + createAccountStateGas = STATE_BYTES_PER_NEW_ACCOUNT * cpt.getCostPerStateByte var gasCost = cpt.gasCosts[Create].cr_handler(false, gasParams) gasCost = gasCost + cpt.gasCosts[Create2].m_handler(0, 0, memLen) @@ -209,7 +226,7 @@ proc create2Op(cpt: VmCpt): EvmResultVoid = # Charge state gas after initcode size validation # https://github.com/ethereum/execution-specs/commit/b9f0afa931a773cdb764310035d0ff383ebecf9e - ? cpt.gasMeter.chargeStateGas(STATE_BYTES_PER_NEW_ACCOUNT * cpt.getCostPerStateByte, + ? cpt.gasMeter.chargeStateGas(createAccountStateGas, reason = "CREATE2: State gas new account") elif memLen > EIP3860_MAX_INITCODE_SIZE: # EIP-3860 @@ -224,6 +241,9 @@ proc create2Op(cpt: VmCpt): EvmResultVoid = reason = "Stack too deep", maxDepth = MaxCallDepth, depth = cpt.msg.depth + # https://github.com/ethereum/execution-specs/pull/2733/changes + if cpt.fork >= FkAmsterdam: + cpt.gasMeter.creditStateGasRefund(createAccountStateGas) return ok() if endowment != 0: @@ -233,6 +253,9 @@ proc create2Op(cpt: VmCpt): EvmResultVoid = reason = "Insufficient funds available to transfer", required = endowment, balance = senderBalance + # https://github.com/ethereum/execution-specs/pull/2733/changes + if cpt.fork >= FkAmsterdam: + cpt.gasMeter.creditStateGasRefund(createAccountStateGas) return ok() var createMsgGas = cpt.gasMeter.gasRemaining diff --git a/execution_chain/evm/interpreter/op_handlers/oph_memory.nim b/execution_chain/evm/interpreter/op_handlers/oph_memory.nim index c28f3d52ad..cfac76d769 100644 --- a/execution_chain/evm/interpreter/op_handlers/oph_memory.nim +++ b/execution_chain/evm/interpreter/op_handlers/oph_memory.nim @@ -69,8 +69,12 @@ proc sstoreNetGasMeteringImpl(c: Computation; slot, newValue: UInt256, coldAcces # reservoir on frame failure. ? c.opcodeGasCost(Sstore, res.gasCost + coldAccess, "SSTORE") - if stateGas and res.stateGas > 0: - ? c.gasMeter.chargeStateGas(res.stateGas, reason = "SSTORE state gas") + if stateGas: + # https://github.com/ethereum/execution-specs/pull/2733/changes + if res.creditStateGas > 0: + c.gasMeter.creditStateGasRefund(res.creditStateGas) + if res.stateGas > 0: + ? c.gasMeter.chargeStateGas(res.stateGas, reason = "SSTORE state gas") c.gasMeter.refundGas(res.gasRefund) diff --git a/execution_chain/evm/interpreter_dispatch.nim b/execution_chain/evm/interpreter_dispatch.nim index d55257be01..ae9346a66f 100644 --- a/execution_chain/evm/interpreter_dispatch.nim +++ b/execution_chain/evm/interpreter_dispatch.nim @@ -14,6 +14,7 @@ import std/[macros, strformat], chronicles, stew/byteutils, + ../core/eip8037, ../constants, ../db/ledger, ./interpreter/op_dispatcher, @@ -93,11 +94,6 @@ proc afterExecCall(c: Computation) = # Special case to account for geth+parity bug c.vmState.ledger.ripemdSpecial() - if c.isSuccess: - c.commit() - else: - c.rollback() - proc beforeExecCreate(c: Computation): bool = c.vmState.mutateLedger: let nonce = ledger.getNonce(c.msg.sender) @@ -128,6 +124,10 @@ proc beforeExecCreate(c: Computation): bool = # regularGasUsed. if c.msg.depth == 0: c.gasMeter.gasRemaining = 0 + elif c.fork >= FkAmsterdam: + # https://github.com/ethereum/execution-specs/pull/2733/changes + let createAccountStateGas = STATE_BYTES_PER_NEW_ACCOUNT * c.getCostPerStateByte + c.gasMeter.creditStateGasRefund(createAccountStateGas) let blurb = c.msg.contractAddress.toHex c.setError("Address collision when creating contract address=" & blurb, true) return true @@ -168,11 +168,6 @@ proc afterExecCreate(c: Computation) = # right cases, particularly important with EVMC where it must be cleared. c.output.reset() - if c.isSuccess: - c.commit() - else: - c.rollback() - const MsgKindToOp: array[CallKind, Op] = [Call, DelegateCall, CallCode, Create, Create2] @@ -205,6 +200,11 @@ proc afterExec(c: Computation) = else: c.afterExecCreate() + if c.isSuccess: + c.commit() + else: + c.rollback() + if c.msg.depth > 0: let gasUsed = c.msg.gas - c.gasMeter.gasRemaining c.vmState.captureExit(c, c.output, gasUsed, c.errorOpt) diff --git a/execution_chain/evm/types.nim b/execution_chain/evm/types.nim index c03fb9c099..6e590048c9 100644 --- a/execution_chain/evm/types.nim +++ b/execution_chain/evm/types.nim @@ -102,6 +102,8 @@ type stateGasLeft*: GasInt stateGasUsed*: GasInt regularGasUsed*: GasInt + stateGasRefund*: GasInt + stateGasRefundPending*: GasInt CallKind* {.pure.} = enum Call # Request CALL. diff --git a/execution_chain/transaction/call_common.nim b/execution_chain/transaction/call_common.nim index ca43dee8fa..a6ae500cd6 100644 --- a/execution_chain/transaction/call_common.nim +++ b/execution_chain/transaction/call_common.nim @@ -150,8 +150,7 @@ proc setupHost(call: CallParams, keepStack: bool): TransactionHost = let isAmsterdamOrLater = fork >= FkAmsterdam - intrinsic = if call.sysCall: IntrinsicGas() - else: intrinsicGas(call, fork, vmState.blockCtx.gasLimit) + intrinsic = call.intrinsic gasRefund = if call.sysCall: 0 else: preExecComputation(vmState, call) intrinsicGas = intrinsic.regular + intrinsic.state @@ -165,12 +164,10 @@ proc setupHost(call: CallParams, keepStack: bool): TransactionHost = var gasLeft = executionGas - intrinsicStateGas = 0.GasInt stateGas = 0.GasInt if isAmsterdamOrLater: gasLeft = min(regularGasBudget, executionGas) - intrinsicStateGas = intrinsic.state - gasRefund.GasInt stateGas = executionGas - gasLeft + gasRefund.GasInt let @@ -178,7 +175,7 @@ proc setupHost(call: CallParams, keepStack: bool): TransactionHost = vmState: vmState, floorDataGas: intrinsic.floorDataGas, intrinsicRegularGas: intrinsic.regular, - intrinsicStateGas: intrinsicStateGas, + intrinsicStateGas: intrinsic.state, # All other defaults in `TransactionHost` are fine. ) @@ -239,6 +236,21 @@ proc prepareToRunComputation(host: TransactionHost, call: CallParams) = vmState.balTracker.trackSubBalanceChange(call.sender, blobFee) ledger.subBalance(call.sender, blobFee) +proc calcSelfDestructRefund(c: Computation) = + let + ledger = c.vmState.ledger + cpsb = c.vmState.blockCtx.costPerStateByte + createAccountStateGas = cpsb * STATE_BYTES_PER_NEW_ACCOUNT + stateGasStorageSet = cpsb * STATE_BYTES_PER_STORAGE_SET + + for refund in newlyCreatedSelfDestructRefund(ledger): + var + selfDestructRefund = createAccountStateGas + + selfDestructRefund += stateGasStorageSet * refund.createdSlots.uint64 + selfDestructRefund += cpsb * refund.codeLen.uint64 + c.gasMeter.selfDestructRefund(selfDestructRefund) + proc calculateAndPossiblyRefundGas(host: TransactionHost, call: CallParams): GasUsed = let c = host.computation @@ -253,6 +265,14 @@ proc calculateAndPossiblyRefundGas(host: TransactionHost, call: CallParams): Gas if c.shouldBurnGas: c.gasMeter.burnGas() + if c.fork >= FkAmsterdam: + if c.isSuccess: + # https://github.com/ethereum/execution-specs/pull/2707/changes + c.calcSelfDestructRefund() + else: + # https://github.com/ethereum/execution-specs/pull/2689/changes + c.gasMeter.returnAllStateGas() + # Calculated gas used, taking into account refund rules. let txGasUsedBeforeRefund = call.gasLimit - c.gasMeter.gasRemaining - c.gasMeter.stateGasLeft diff --git a/execution_chain/transaction/call_evm.nim b/execution_chain/transaction/call_evm.nim index 7f4827fd4b..95ca9ae063 100644 --- a/execution_chain/transaction/call_evm.nim +++ b/execution_chain/transaction/call_evm.nim @@ -20,7 +20,9 @@ import export call_common -proc callParamsForTx(tx: Transaction, sender: Address, vmState: BaseVMState, baseFee: GasInt): CallParams = +proc callParamsForTx(tx: Transaction, sender: Address, + vmState: BaseVMState, baseFee: GasInt, + intrinsic: IntrinsicGas): CallParams = # Is there a nice idiom for this kind of thing? Should I # just be writing this as a bunch of assignment statements? result = CallParams( @@ -31,7 +33,8 @@ proc callParamsForTx(tx: Transaction, sender: Address, vmState: BaseVMState, bas to: tx.destination, isCreate: tx.contractCreation, value: tx.value, - input: tx.payload + input: tx.payload, + intrinsic: intrinsic ) if tx.txType > TxLegacy: assign(result.accessList, tx.accessList) @@ -65,9 +68,11 @@ proc callParamsForTest(tx: Transaction, sender: Address, vmState: BaseVMState): proc txCallEvm*(tx: Transaction, sender: Address, - vmState: BaseVMState, baseFee: GasInt): LogResult = + vmState: BaseVMState, + baseFee: GasInt, + intrinsic: IntrinsicGas): LogResult = let - call = callParamsForTx(tx, sender, vmState, baseFee) + call = callParamsForTx(tx, sender, vmState, baseFee, intrinsic) runComputation(call, LogResult) proc testCallEvm*(tx: Transaction, diff --git a/execution_chain/transaction/call_types.nim b/execution_chain/transaction/call_types.nim index fed107bc4e..d4c19644d2 100644 --- a/execution_chain/transaction/call_types.nim +++ b/execution_chain/transaction/call_types.nim @@ -37,6 +37,7 @@ type versionedHashes*: seq[VersionedHash] # EIP-4844 (Cancun) blob versioned hashes authorizationList*: seq[Authorization] # EIP-7702 (Prague) authorization list sysCall*: bool # System call or ordinary call + intrinsic*: IntrinsicGas # Standard call result. CallResult* = object of RootObj @@ -75,7 +76,8 @@ func isError*(cr: CallResult): bool = cr.error.len > 0 const - TOTAL_COST_FLOOR_PER_TOKEN = 10 + TOTAL_COST_FLOOR_PER_TOKEN_EIP7623 = 10 + TOTAL_COST_FLOOR_PER_TOKEN_EIP7976 = 16 func intrinsicGas*(call: CallParams | Transaction, fork: EVMFork, gasLimit: GasInt): IntrinsicGas = # Compute the baseline gas cost for this transaction. This is the amount @@ -89,6 +91,7 @@ func intrinsicGas*(call: CallParams | Transaction, fork: EVMFork, gasLimit: GasI stateGas = 0.GasInt floorDataGas = regularGas tokens = 0 + accessListBytes = 0 # EIP-2 (Homestead) extra intrinsic gas for contract creations. if call.isCreate: @@ -100,30 +103,39 @@ func intrinsicGas*(call: CallParams | Transaction, fork: EVMFork, gasLimit: GasI regularGas += (gasFees[fork][GasInitcodeWord] * call.input.len.wordCount) # Input data cost, reduced in EIP-2028 (Istanbul). - let gasZero = gasFees[fork][GasTXDataZero] - let gasNonZero = gasFees[fork][GasTXDataNonZero] + let + gasZero = gasFees[fork][GasTXDataZero] + gasNonZero = gasFees[fork][GasTXDataNonZero] + byteZeroToken = if fork >= FkAmsterdam: 4 else: 1 + for b in call.input: if b == 0: regularGas += gasZero - tokens += 1 + tokens += byteZeroToken else: regularGas += gasNonZero tokens += 4 - # EIP-2930 (Berlin) intrinsic gas for transaction access list. if fork >= FkBerlin: for account in call.accessList: regularGas += ACCESS_LIST_ADDRESS_COST regularGas += account.storageKeys.len * ACCESS_LIST_STORAGE_KEY_COST + # Total byte count of addresses(20 bytes each) and storage keys (32 bytes each) in the access list. + accessListBytes += 20 + account.storageKeys.len * 32 if fork >= FkPrague: if fork >= FkAmsterdam: regularGas += REGULAR_PER_AUTH_BASE_COST * call.authorizationList.len + # EIP-7981: Increase Access List Cost + let floorTokensInAccessList = accessListBytes * 4 + tokens += floorTokensInAccessList + regularGas += TOTAL_COST_FLOOR_PER_TOKEN_EIP7976 * floorTokensInAccessList stateGas += (STATE_BYTES_PER_NEW_ACCOUNT + STATE_BYTES_PER_AUTH_BASE) * costPerStateByte * GasInt(call.authorizationList.len) + floorDataGas += tokens * TOTAL_COST_FLOOR_PER_TOKEN_EIP7976 else: regularGas += call.authorizationList.len * PER_EMPTY_ACCOUNT_COST - floorDataGas += tokens * TOTAL_COST_FLOOR_PER_TOKEN + floorDataGas += tokens * TOTAL_COST_FLOOR_PER_TOKEN_EIP7623 IntrinsicGas( regular: regularGas.GasInt, diff --git a/execution_chain/utils/debug_bal.nim b/execution_chain/utils/debug_bal.nim index f20cc35005..bc1947bee8 100644 --- a/execution_chain/utils/debug_bal.nim +++ b/execution_chain/utils/debug_bal.nim @@ -8,6 +8,8 @@ # at your option. This file may not be copied, modified, or distributed except # according to those terms. +{.push raises: [], gcsafe.} + import std/[json, strutils], stint, @@ -25,7 +27,7 @@ proc `@@`(x: Address): JsonNode = proc `@@`(x: Bytes): JsonNode = %("0x" & x.toHex) -proc `@@`(x: uint16 | uint64): JsonNode = +proc `@@`(x: uint16 | uint32 | uint64): JsonNode = %("0x" & x.toHex) proc `@@`(x: UInt256): JsonNode = diff --git a/nimbus_verified_proxy/tests/data/access_list.json b/nimbus_verified_proxy/tests/data/access_list.json index 2498bf0778..214b27e76d 100644 --- a/nimbus_verified_proxy/tests/data/access_list.json +++ b/nimbus_verified_proxy/tests/data/access_list.json @@ -7,5 +7,5 @@ ] } ], - "gasUsed": "0x669a" + "gasUsed": "0x216" } diff --git a/scripts/eest_ci_cache.sh b/scripts/eest_ci_cache.sh index e6031beaef..a9ce0df46b 100755 --- a/scripts/eest_ci_cache.sh +++ b/scripts/eest_ci_cache.sh @@ -22,7 +22,7 @@ EEST_DEVELOP_URL="https://github.com/ethereum/execution-spec-tests/releases/down # --- BAL Release --- EEST_BAL_NAME="bal" -EEST_BAL_VERSION="v5.6.1" +EEST_BAL_VERSION="v5.7.0" EEST_BAL_DIR="${FIXTURES_DIR}/eest_bal" EEST_BAL_ARCHIVE="fixtures_bal.tar.gz" EEST_BAL_URL="https://github.com/ethereum/execution-spec-tests/releases/download/${EEST_BAL_NAME}%40${EEST_BAL_VERSION}/${EEST_BAL_ARCHIVE}" diff --git a/tests/eest/eest_engine_test.nim b/tests/eest/eest_engine_test.nim index 0a22362f81..b38029d079 100644 --- a/tests/eest/eest_engine_test.nim +++ b/tests/eest/eest_engine_test.nim @@ -25,6 +25,7 @@ const const skipFiles = [ "CALLBlake2f_MaxRounds.json", # Doesn't work in github CI + "withdrawal_requests.json" # TODO: investigate why fail ] runEESTSuite( diff --git a/tests/eest/eest_stateless_execution_test.nim b/tests/eest/eest_stateless_execution_test.nim index 43b376c9b5..409ddb7c15 100644 --- a/tests/eest/eest_stateless_execution_test.nim +++ b/tests/eest/eest_stateless_execution_test.nim @@ -18,7 +18,9 @@ const eestType = "blockchain_tests" eestReleases = [ "eest_develop", - "eest_zkevm" + # TODO: zkevm@v0.3.3 is not compatible with bal@v5.7.0 + # enable this when they become compatible again + # "eest_zkevm" ] const skipFiles = [ diff --git a/tests/test_transaction_json.nim b/tests/test_transaction_json.nim index 7c30951e90..9b1a7e768b 100644 --- a/tests/test_transaction_json.nim +++ b/tests/test_transaction_json.nim @@ -19,6 +19,7 @@ import ../execution_chain/db/core_db, ../execution_chain/common/common, ../execution_chain/transaction, + ../execution_chain/transaction/call_types, ../execution_chain/core/validate, ../execution_chain/utils/utils @@ -41,8 +42,10 @@ proc testTxByFork(tx: Transaction, forkData: JsonNode, forkName: string, testSta config = getChainConfig(forkName) memDB = newCoreDbRef DefaultDbMemory com = CommonRef.new(memDB, config) + fork = nameToFork[forkName] + intrinsic = tx.intrinsicGas(fork, 10_000_000) - validateTxBasic(com, tx, 10_000_000, nameToFork[forkName]).isOkOr: + validateTxBasic(com, tx, intrinsic, fork).isOkOr: return if forkData.len > 0 and "sender" in forkData: diff --git a/tools/txparse/txparse.nim b/tools/txparse/txparse.nim index 042fd8dcf9..f0a15d66dc 100644 --- a/tools/txparse/txparse.nim +++ b/tools/txparse/txparse.nim @@ -15,6 +15,7 @@ import ../common/helpers, ../../execution_chain/db/core_db/memory_only, ../../execution_chain/transaction, + ../../execution_chain/transaction/call_types, ../../execution_chain/core/validate, ../../execution_chain/common/evmforks, ../../execution_chain/common/common @@ -25,8 +26,10 @@ proc parseTx(com: CommonRef, hexLine: string) = bytes = hexToSeqByte(hexLine) tx = decodeTx(bytes) address = tx.recoverSender().expect("valid signature") + fork = FkPrague + intrinsic = tx.intrinsicGas(fork, 10_000_000) - validateTxBasic(com, tx, 10_000_000, FkPrague).isOkOr: + validateTxBasic(com, tx, intrinsic, fork).isOkOr: echo "err: ", error # everything ok