Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
01243fd
EIP-7976: Increase Calldata Floor Cost
jangko Apr 21, 2026
7d3638f
EIP-7981: Increase Access List Cost
jangko Apr 21, 2026
bc15b8d
Implement dynamic EIP-8037 costs
jangko Apr 21, 2026
c0c67bc
Upgrade to bal@v5.7.0 test fixtures
jangko Apr 22, 2026
eb63851
zero execution state gas on top-level failure
jangko Apr 22, 2026
06087f4
CREATE failure refunds state gas to reservoir
jangko Apr 22, 2026
729d46a
immutable intrinsic state gas for EIP-7702
jangko Apr 22, 2026
1268021
per-dimension block gas limit check at tx inclusion
jangko Apr 22, 2026
711af3d
EIP-8037 - 0 to x to 0 SSTORE refunds to state gas
jangko Apr 22, 2026
82314f2
EIP-8037 nested child frame refunds
jangko Apr 22, 2026
b9d17c2
Fixes
jangko Apr 22, 2026
6b21415
Fix EIP-7981
jangko Apr 22, 2026
820580f
Fixes
jangko Apr 22, 2026
1c2c116
EIP-8037 - SELFDESTRUCT same-tx refunds state gas at end of tx
jangko Apr 22, 2026
66b4a34
Fix sstore credit state gas
jangko Apr 22, 2026
0d826af
Fix selfdestruct same-tx refund mechanism
jangko Apr 22, 2026
4d6f401
Merge branch 'master' into bal-devnet-4
jangko Apr 22, 2026
eeea08d
Merge branch 'master' into bal-devnet-4
jangko Apr 23, 2026
bb64997
Fix EIP-4788 and EIP-2935 syscall: fail silently
jangko Apr 23, 2026
cddae9f
Remove space
jangko Apr 23, 2026
e4380ae
Skip withdrawal_requests.json in engine_test
jangko Apr 23, 2026
82b464a
Update nvp access list test
jangko Apr 23, 2026
9017764
Disable zkevm test due to incompatibility
jangko Apr 24, 2026
49a3961
Merge branch 'master' into bal-devnet-4
jangko Apr 24, 2026
cf39234
Merge branch 'master' into bal-devnet-4
jangko Apr 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions execution_chain/core/eip8037.nim
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,42 @@
# 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
STATE_BYTES_PER_AUTH_BASE* = 23
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)
33 changes: 26 additions & 7 deletions execution_chain/core/executor/process_transaction.nim
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import
../../db/ledger,
../../transaction/call_evm,
../../transaction/call_common,
../../transaction/call_types,
../../transaction,
../../evm/state,
../../evm/types,
Expand Down Expand Up @@ -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
Expand All @@ -127,14 +145,15 @@ 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
# before leaving is crucial for some unit tests that us a direct/deep call
# 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.
Expand All @@ -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(
Expand Down
6 changes: 5 additions & 1 deletion execution_chain/core/tx_pool/tx_desc.nim
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import
../../db/ledger,
../../constants,
../../transaction,
../../transaction/call_types,
../../core/eip8037,
../chain/forked_chain,
../pow/header,
Expand Down Expand Up @@ -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",
Expand Down
6 changes: 1 addition & 5 deletions execution_chain/core/validate.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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] =

Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down
58 changes: 41 additions & 17 deletions execution_chain/db/ledger.nim
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ type
selfDestruct: HashSet[Address]
accessList: ac_access_list.AccessList

SelfDestructRefund* = object
createdSlots*: int
codeLen*: int

const
resetFlags = {
Dirty,
Expand Down Expand Up @@ -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
# ------------------------------------------------------------------------------
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
7 changes: 5 additions & 2 deletions execution_chain/evm/interpreter/gas_costs.nim
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ type
nonZeroVal*: bool
gasCost1*: GasInt
isNewAccount*: proc(): bool {.gcsafe, raises: [].}
gasLeft*: GasInt
gasLeft*: GasInt
gasCallDelegate*: GasProc
contractGas*: UInt256

Expand Down Expand Up @@ -101,6 +101,7 @@ type
gasCost*: GasInt
gasRefund*: int64
stateGas*: GasInt
creditStateGas*: GasInt

CallGasResult = tuple[gasCost, childGasLimit: GasInt]

Expand Down Expand Up @@ -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)
Expand Down
20 changes: 20 additions & 0 deletions execution_chain/evm/interpreter/gas_meter.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 4 additions & 1 deletion execution_chain/evm/interpreter/op_handlers/oph_call.nim
Original file line number Diff line number Diff line change
Expand Up @@ -197,14 +197,17 @@ 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:
# On failure (revert or exceptional halt) state changes are rolled back,
# 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:
Expand Down
Loading
Loading