EIP.tools

EIP.tools

⚠️ DraftStandards Track: Core

EIP-8279: Block Access List Byte Floor

Meter EIP-7928 Block Access List bytes at runtime and fold them into the EIP-7623 transaction floor, capping worst-case block size.

Authors
Created2026-05-23
Discussion Linkhttps://ethereum-magicians.org/t/eip-8279-block-access-list-byte-floor/28662
Requires

Markdown

https://raw.githubusercontent.com/ethereum/EIPs/ma...

EIP-GPT summary

Contents
AbstractMotivationSpecificationConstantsData meter and floor accumulatorStatic floor seedRuntime data meteringFinal chargeSystem transactions and withdrawalsRationaleFloor side, not intrinsic sideRuntime extensions can bindOne per-byte rateMeter before; never refundPer-auth coverage is staticCounter, not a reservationCeiling is the execution-gas budget, not tx.gasmax(execution, floor) preservedBackwards CompatibilityTest CasesBare ETH transferCold SLOAD bypass attemptPre-warmed storage-key touchCold SSTORE (zero β†’ non-zero)Net-zero SSTORE round-tripRepeated distinct writesRepeated departures from the original valueReverted CALL with valueSecurity ConsiderationsBlock-size boundRevertsRepeated writes to the same slotGas estimationCopyright

Abstract

Meter the bytes execution adds to the EIP-7928 Block Access List in an explicit per-transaction counter, and fold that count, at 64 gas/byte, into the transaction's floor accumulator β€” checked at runtime before the BAL grows. Today an attacker can pack ~1.55 MiB into a 60M-gas block: 75% of gas on cold SLOADs (32 BAL bytes per 2,100 gas) + 25% on calldata at 16 gas/byte. On top of EIP-8131's tx-content floor (same rate), block content is capped at block_gas_limit / 64 β‰ˆ 0.89 MiB, a ~42% cut in worst-case block size at an unchanged gas limit. Neither EIP alone closes the bypass: 8131 does not price BAL bytes; 8279 reuses 8131's floor. In the sampled transactions, the runtime BAL component did not bind on its own; it matters mainly when the static floor is already high.

Motivation

Every node must be able to handle the largest block an attacker can build, not the average one. Lowering that worst case buys headroom, which can either be spent on throughput by raising the gas limit or kept as a buffer. Left unspent, it is a direct gain in robustness. EIP-7623 caps worst-case block size by charging at least 64 gas per non-zero calldata byte. EIP-7981 extended that to access-list entries, and EIP-8131 generalises the floor to a uniform per-byte rule over all tx-content fields, including EIP-7702 authorization tuples and EIP-4844 blob versioned hashes. EIP-7928 introduces a new source of block bytes, the BAL, populated by runtime opcodes. None of those static floors cover it.

The cheapest BAL contributor is a cold SLOAD: 32 bytes for 2,100 gas. Combined with all-non-zero calldata, an attacker pushes intrinsic gas above the calldata floor, pays intrinsic, and gets calldata at 16 gas/byte while loading the rest of the block via SLOAD keys.

At a 60M gas limit:

AttackWorst-case block bytes
Pure calldata at floor0.894 MiB (target)
Auth-tuple bypass under EIP-8131 alone1.067 MiB
Cold-SLOAD bypass under EIP-79281.548 MiB

SSTORE, address access, CREATE, and successful EIP-7702 delegations give further vectors. This EIP closes them with one uniform mechanism.

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.

Constants

FLOOR_GAS_PER_BYTE          = 64   # per-byte floor rate (EIP-7976)
TX_MAX_GAS_LIMIT            = 16_777_216   # per-transaction execution-gas cap (EIP-7825)
BAL_BYTES_PER_ADDRESS       = 20
BAL_BYTES_PER_STORAGE_KEY   = 32
BAL_BYTES_PER_STORAGE_VALUE = 32
BAL_BYTES_PER_BALANCE       = 32
BAL_BYTES_PER_NONCE         = 8
DELEGATION_CODE_BYTES       = 23   # EIP-7702 delegation marker length

Data meter and floor accumulator

Clients keep two internal, per-transaction counters and three sets on the execution environment for the duration of the transaction:

  • bal_data_bytes β€” an explicit count of the bytes execution contributes to the BAL,
  • floor_gas_used β€” the floor accumulator, derived from the static seed plus the metered bytes, and
  • metered_addresses β€” the addresses whose 20-byte address has already been metered,
  • metered_storage_keys β€” the (storage_address, key) pairs whose 32-byte storage key has already been metered, and
  • metered_storage_values β€” the (storage_address, key) pairs whose 32-byte storage value has already been metered.

None is part of the signed transaction, RLP-encoded, gossiped, or persisted; no new transaction field or type is introduced. floor_gas_used is seeded with the static floor (below), bal_data_bytes = 0, and all three sets empty. The counters and sets are extended at runtime. No gas is reserved or deducted from the execution budget; the derived floor is checked against the transaction's execution-gas budget, execution_budget = min(tx.gas, TX_MAX_GAS_LIMIT), only to ensure the user can pay it if it ends up binding.

def meter_bal_data(tx_env, num_bytes):
    new_bal_data_bytes = tx_env.bal_data_bytes + num_bytes
    new_floor = tx_env.static_floor + new_bal_data_bytes * FLOOR_GAS_PER_BYTE
    if new_floor > tx_env.execution_budget:    # min(tx.gas, TX_MAX_GAS_LIMIT)
        raise OutOfGasError
    tx_env.bal_data_bytes = new_bal_data_bytes
    tx_env.floor_gas_used = new_floor

def meter_bal_address(tx_env, address):
    if address in tx_env.metered_addresses:
        return
    meter_bal_data(tx_env, BAL_BYTES_PER_ADDRESS)
    tx_env.metered_addresses.add(address)

def meter_bal_storage_key(tx_env, storage_address, key):
    slot = (storage_address, key)
    if slot in tx_env.metered_storage_keys:
        return
    meter_bal_data(tx_env, BAL_BYTES_PER_STORAGE_KEY)
    tx_env.metered_storage_keys.add(slot)

def meter_bal_storage_value(tx_env, storage_address, key):
    slot = (storage_address, key)
    if slot in tx_env.metered_storage_values:
        return
    meter_bal_data(tx_env, BAL_BYTES_PER_STORAGE_VALUE)
    tx_env.metered_storage_values.add(slot)

meter_bal_data MUST be called BEFORE the matching BAL insertion or state mutation, and after the pre-state execution-gas validation that EIP-7928 requires for that access has passed. An access that fails its pre-state validation meters nothing, and an OutOfGasError at the metering step aborts before any unpaid BAL byte exists. The metered count is a deliberate upper bound on the transaction's actual BAL contribution (see Reverts and Repeated writes to the same slot); over-counting only raises the floor, never lowers it, so the bound below is never violated.

Static floor seed

The tx-content portion of the floor (calldata, access-list entries, authorization tuples, blob versioned hashes) is defined by EIP-8131 as tx_floor. This EIP adds the per-auth BAL contribution that arises when an authorization is processed:

auth_bal_bytes = BAL_BYTES_PER_ADDRESS     #  20: authority address
               + DELEGATION_CODE_BYTES     #  23: delegation marker
               + BAL_BYTES_PER_NONCE       #   8: authority nonce

static_floor = tx_floor + FLOOR_GAS_PER_BYTE * auth_bal_bytes * num_authorizations

tx_env.static_floor holds this value and tx_env.floor_gas_used is initialised to it (with bal_data_bytes = 0) at the start of execution. The per-auth term covers each authorization's worst-case BAL contribution statically, so set_delegation (which runs before the EVM's OOG handler) never meters at runtime.

Per-transaction accounting always adds further BAL entries that no opcode places there: sender, coinbase, and top-level target addresses; sender and coinbase balances; sender nonce; recipient balance for a top-level value transfer; top-level creation target nonce; and an optional top-level EIP-7702 delegated target. These total at most 184 bytes, fully covered by the TX_BASE / FLOOR_GAS_PER_BYTE = 328 bytes of headroom already in TX_BASE.

Runtime data metering

Each trigger meters the byte count below before the corresponding BAL entry is added. Address and storage-key touches use meter_bal_address and meter_bal_storage_key; storage values use meter_bal_storage_value; all other rows call meter_bal_data directly. These first-touch sets are independent of EVM warmness: an address or storage key pre-warmed by an EIP-2930 access list is still metered when execution first touches it, because EIP-7928 then adds it to the BAL.

TriggerBytes added
First account touch in the transaction (BALANCE, EXT*, CALL*, SELFDESTRUCT beneficiary, CREATE/CREATE2 deployed address)20
First storage-key touch in the transaction (SLOAD, SSTORE)32
SSTORE with current == original and new != original, if the (storage_address, key) pair has not been metered+32
CALL with non-zero value to a different account (caller and recipient balances)64
SELFDESTRUCT with non-zero balance to a different beneficiary (originator and beneficiary balances)64
CREATE / CREATE2 before incrementing the creator's nonce8
CREATE / CREATE2 after the collision check (new contract's nonce)8
CREATE / CREATE2 with non-zero endowment (creator and new contract balances)+64
Successful top-level contract creation, CREATE, or CREATE2 deploy, just before set_codelen(deployed_code)

EIP-7702 delegations are covered by the static auth_bytes term. CALLCODE, DELEGATECALL, and STATICCALL transfer no value out of the executing account and therefore add no balance bytes.

The 32 storage-value bytes are metered at most once per slot per transaction. Before mutating storage, every SSTORE with current == original and new != original MUST call meter_bal_storage_value(tx_env, storage_address, key); the helper returns without charging if the pair was already metered. Here, storage_address is the account whose storage the current execution context modifies; for CALLCODE and DELEGATECALL, it is not the account whose code is executing. The metered_storage_values set is never rewound, and the charge is not refunded if a later SSTORE restores the original value. This can over-count a net-zero write, but keeps bal_data_bytes an upper bound without making it part of call-frame rollback: if the first changing write reverts, its retained charge covers any later committed change to the same slot; if a restoring write reverts, the charge still covers the earlier change that reappears in the BAL.

Final charge

tx.gasUsed = max(execution_gas_used, tx_env.floor_gas_used)

Transaction validation continues to require execution_budget >= max(intrinsic, static_floor).

System transactions and withdrawals

System-contract calls (EIP-2935, EIP-4788, EIP-7002, EIP-7251) and withdrawals do NOT consume from a per-transaction floor. Their BAL bytes are absorbed by the existing EIP-7928 block-level buffer bal_items <= block_gas_limit / ITEM_COST.

Rationale

Floor side, not intrinsic side

Raising execution charges would penalise every user, including ones who never bypass. Charging on the floor only bites when execution is too cheap to cover the floor, which is exactly the bypass case.

A pure intrinsic surcharge also fails to cap the block at gas_limit / 64: for any added per-SLOAD cost X, the optimum bypass still yields B = gas_limit / 64 + 24 Β· gas_limit / (2100 + X) bytes, with the residual only vanishing as X grows large. Floor-side charging gives a closed bound directly.

Runtime extensions can bind

Runtime contributions are checked against the execution-gas budget because they can make the floor bind even when execution gas would not. Representative cases are:

TriggerFloor extensionMin execution charge
First account touch, cold1,2802,600
First account touch, pre-warmed by EIP-29301,280100
First storage-key touch, cold SLOAD2,0482,100
First storage-key touch, pre-warmed SLOAD2,048100
SSTORE (cold, zero β†’ non-zero)4,09622,100
CALL with non-zero value4,0969,000
SELFDESTRUCT with non-zero balance, beneficiary already metered4,0965,000
SELFDESTRUCT with non-zero balance, warm beneficiary first touched5,3765,000
Successful CREATE / CREATE2, no endowment/code (address and two nonces)2,30432,000
Deployed code (per byte)64200

A pre-warmed EIP-2930 entry is charged once for its bytes in the transaction and again when execution first touches it and EIP-7928 copies it into the BAL. The warm execution charge can be lower than this second floor contribution. A positive-balance SELFDESTRUCT to a warm, not-yet-metered beneficiary likewise adds the 20-byte beneficiary address plus two 32-byte balances: 84 Γ— 64 = 5,376 floor gas against a 5,000 execution-gas charge. The pre-mutation runtime check handles these cases directly. Under EIP-8037 the code-deposit execution charge drops to the hashing cost of 6 gas per word, under 0.2 gas per byte, while the floor extension stays at 64 per byte, so deployed code becomes the runtime trigger whose floor extension exceeds its execution charge by the widest margin.

The main high-volume case remains a static seed defined by EIP-8131 (calldata, access-list entries, auths, blob hashes) that is already close to the execution-gas budget; that is the calldata + SLOAD bypass this EIP targets.

One per-byte rate

Pricing every BAL byte at the same FLOOR_GAS_PER_BYTE as non-zero calldata makes the worst case invariant to the attacker's choice: calldata, auths, storage keys, addresses, code, and balances all yield 1/64 bytes per gas at the floor. The optimum collapses to B = gas_limit / 64.

Meter before; never refund

EIP-7928 keeps accessed addresses and storage keys in the BAL even when their call frame reverts. Metering each contribution before the insertion guarantees every BAL byte is accounted for, and an out-of-gas at the metering step aborts before the matching insertion. The meter is otherwise a deliberate upper bound, not an exact mirror of the BAL: changes from reverted frames are never rewound (see Reverts), and storage-value bytes are never refunded. A refund when a slot returns to its pre-transaction value would have to be journaled with call-frame state: if the restoring write later reverted, the earlier value change would reappear in the BAL while its bytes remained refunded. The monotonic metered_storage_values set instead charges each slot at most once without entering rollback machinery, preserving the block-size bound while avoiding repeated charges for the same possible post-value. Without the set, every additional O β†’ X β†’ O cycle would add another 2,048 floor gas even though EIP-7928 records at most one post-value for the slot.

Per-auth coverage is static

set_delegation runs outside the EVM's exception handler. A runtime meter_bal_data raising OutOfGasError there would propagate uncaught and crash block processing. Folding the worst-case 51 BAL bytes per auth (on top of the 108-byte tuple priced by EIP-8131, for 159 B total) into the static floor lets set_delegation mutate state freely: any transaction whose execution-gas budget cannot cover it has already been rejected at validation.

Counter, not a reservation

The runtime mechanism does not deduct gas from the execution budget or pre-commit any gas. meter_bal_data only increments the explicit byte counter (bal_data_bytes) and the derived accumulator (floor_gas_used), and checks that the floor remains ≀ execution_budget. Execution gas accounting is untouched. At the end, tx.gasUsed = max(execution_gas_used, floor_gas_used); the floor accumulator decides only how much of the upfront-debited tx.gas Γ— gas_price gets kept versus refunded.

Ceiling is the execution-gas budget, not tx.gas

The floor is an execution-dimension quantity: block accounting compares it against the transaction's execution gas, and EIP-7825 caps a transaction's execution gas at TX_MAX_GAS_LIMIT. Under EIP-8037, tx.gas may exceed that cap, with the excess held in a state-gas reservoir that only state charges can draw on. Admitting the floor up to tx.gas would then let a deploy transaction with a large reservoir push the floor past TX_MAX_GAS_LIMIT through code-deposit bytes, exceeding the per-transaction execution cap that validation enforced statically and the execution capacity the block reserved for it. Checking against min(tx.gas, TX_MAX_GAS_LIMIT) keeps the floor inside the execution budget the transaction actually has. Without EIP-8037, tx.gas never exceeds TX_MAX_GAS_LIMIT, so the budget equals tx.gas and nothing changes.

max(execution, floor) preserved

The EIP-7623 / EIP-8131 charging shape is unchanged; only the floor side has a runtime tail. Transactions whose execution exceeds the floor pay exactly as today.

Backwards Compatibility

Hard fork.

The static floor takes tx_floor from EIP-8131 and adds 51 BAL bytes per authorization, plus runtime extensions of the floor accumulator for BAL bytes contributed during execution. The combined floor per auth is (108 + 51) Γ— 64 = 10,176 gas, still below the PER_EMPTY_ACCOUNT_COST = 25,000 intrinsic per auth. Static transaction validation changes only when this combined static floor dominates. During execution, a runtime BAL extension can also raise gasUsed or cause OutOfGasError when the new floor exceeds the execution-gas budget, including for pre-warmed first touches. A bare ETH transfer stays at 21,000.

Mainnet sample (1,500 random blocks, 441,271 transactions, 20,000 of those traced for BAL bytes, May 2026): EIP-8131 alone makes 3.24% of transactions pay more (+3.29% gas). EIP-8279 adds another 0.54 pp and +0.77 pp, for a combined 3.79% / +4.06%. In the sampled transactions, the BAL piece did not bind on its own: per-tx BAL bytes average 120-200 B after TX_BASE absorption, under 13 k floor gas, well below typical execution gas. EIP-8279's main job is to close the calldata + SLOAD bypass when paired with EIP-8131, not to add a broadly binding standalone floor.

Wallets and eth_estimateGas MUST compute the new static floor (including the per-auth term) and, when tracing execution, meter the runtime BAL bytes for first touches, value-bearing calls, and deployments alongside intrinsic gas.

Test Cases

Bare ETH transfer

tx.to = recipient, tx.value = 1 ether, tx.data = b"".

  • static_floor = TX_BASE = 21,000 gas.
  • Minimum tx.gas = 21,000, unchanged.

Cold SLOAD bypass attempt

Before this EIP, an idealized worst-case mix at gas_limit = 60M uses 937,171 non-zero calldata bytes plus 21,421 cold SLOADs in one transaction, for 1,622,643 logical content bytes.

  • Static seed: 21,000 + 64 Γ— 937,171 = 59,999,944.
  • Per cold SLOAD floor extension: 32 Γ— 64 = 2,048.
  • With this EIP, the first cold SLOAD raises the floor above gas_limit; all 21,421 would raise it to 59,999,944 + 21,421 Γ— 2,048 = 103,870,152.
  • The largest feasible metered content is floor((gas_limit - TX_BASE) / 64) = 937,171 bytes. Including the implicit per-transaction bytes covered by TX_BASE, the bound remains gas_limit / 64 = 937,500 bytes.

Pre-warmed storage-key touch

The transaction's EIP-2930 access list contains (contract, slot), and execution then performs SLOAD(slot).

  • EIP-8131 meters the 20-byte address and 32-byte key in the transaction's static floor.
  • The key is EVM-warm, but its first actual SLOAD calls meter_bal_storage_key, adding another 32 bytes for the BAL copy. The account address is covered separately when first touched or by the implicit per-transaction accounting above.
  • Repeated touches in the transaction add nothing.

Cold SSTORE (zero β†’ non-zero)

Single cold SSTORE writing a non-zero value to an empty slot.

  • Metered: 32 (key) + 32 (value) = 64 BAL bytes β†’ 4,096 gas folded into floor_gas_used.
  • The SSTORE execution charge is 22,100, larger than the 4,096 floor extension, so this trigger remains execution-dominated.

Net-zero SSTORE round-trip

Slot starts at original value O; the transaction writes X β‰  O then writes O again.

  • First SSTORE (O β†’ X): 32 (cold key) + 32 (value) metered.
  • Second SSTORE (X β†’ O): slot is back at its pre-transaction value, a no-op write under EIP-7928; the 32 value bytes stay metered.
  • Net metered: 64 (key and the upper-bound value charge). The BAL records the slot in storage_reads, with no storage_changes value entry, so the meter over-counts by 32 bytes.

Repeated distinct writes

Slot starts at O; the transaction writes A, then B, then C (all distinct, C β‰  O).

  • The 32 value bytes are metered once, on the O β†’ A transition; A β†’ B and B β†’ C add nothing.
  • Net metered: 32 (key) + 32 (value) = 64, matching the single post-value C the BAL records.

Repeated departures from the original value

Slot starts at O; the transaction writes A, restores O, then writes B (A != O, B != O).

  • O β†’ A meters the 32 value bytes and adds the slot to metered_storage_values.
  • A β†’ O refunds nothing; O β†’ B sees the slot already in metered_storage_values and adds nothing.
  • Net metered: 32 (key) + 32 (value) = 64, matching the single post-value B the BAL records.

Reverted CALL with value

CALL with value > 0 to a recipient that reverts.

  • 64 balance bytes are metered (caller and recipient, folded into floor_gas_used) before generic_call.
  • Both balance changes are discarded on revert (per EIP-7928); neither bal_data_bytes nor the floor is rewound.
  • Over-accounting by 64 bytes; the bound still holds.

Security Considerations

Block-size bound

Per transaction, every byte the tx contributes to the block (tx blob + BAL) is accounted for at the floor rate via either the static seed or a runtime extension, with implicit per-tx bytes covered by TX_BASE:

block_bytes_per_tx Γ— FLOOR_GAS_PER_BYTE  ≀  tx_env.floor_gas_used  ≀  tx.gasUsed

Summing across user transactions:

sum(block_bytes) ≀ sum(tx.gasUsed) / 64 ≀ block_gas_limit / 64

System-contract and withdrawal contributions are bounded separately by the EIP-7928 block-level ITEM_COST buffer (~1,428 items at a 60M gas limit), sized to absorb existing system contract use.

Reverts

Accessed addresses and storage keys persist in the BAL across reverts (per EIP-7928); balance, nonce, code, and storage-value changes from reverted frames are discarded. Neither bal_data_bytes, floor_gas_used, nor any of the three metering sets is rewound for discarded entries, and no storage-value charge is refunded. Safe: the meter over-counts, never under-counts.

Repeated writes to the same slot

The 32 storage-value bytes are metered at most once per slot per transaction. Repeated distinct writes (O β†’ A β†’ B β†’ C) and repeated departures from the original value (O β†’ X β†’ O β†’ Y) therefore meter 32 value bytes total, not 32 per write or departure. A net-zero round-trip (O β†’ X β†’ O) keeps that charge and over-counts the BAL by 32 bytes. Since metered_storage_values is not rewound, a charge from a reverted frame also covers any later committed change to the same slot. The meter can over-count, but never under-counts; the bound holds.

Gas estimation

Wallets and eth_estimateGas MUST simulate floor_gas_used alongside intrinsic and execution gas. Without it, estimates for floor-bound transactions are too low; a transaction may pass static validation and then halt with OutOfGasError when a runtime extension exceeds the execution-gas budget min(tx.gas, TX_MAX_GAS_LIMIT).

Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth