Alert Source Discuss
⚠️ Draft Standards Track: Core

EIP-8279: Block Access List Byte Floor

Charge EIP-7623 floor gas at runtime for items added to the EIP-7928 Block Access List.

Authors Toni Wahrstätter (@nerolation)
Created 2026-05-23
Discussion Link https://ethereum-magicians.org/t/eip-8279-block-access-list-byte-floor/28662
Requires EIP-7623, EIP-7702, EIP-7928, EIP-7976, EIP-7981, EIP-8131

Abstract

Extend the transaction’s floor accumulator by 64 gas for each byte an opcode adds to the EIP-7928 Block Access List, checked at runtime before the BAL grows. Today an attacker can pack ~1.55 MB 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 MB (~42% reduction). Neither EIP alone closes the bypass: 8131 does not price BAL bytes; 8279 reuses 8131’s floor. Typical transactions are unaffected: the runtime BAL floor never binds in isolation.

Motivation

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:

Attack Worst-case block bytes
Pure calldata at floor 0.894 MB (target)
Auth-tuple bypass under EIP-8131 alone 1.067 MB
Cold-SLOAD bypass under EIP-7928 1.548 MB

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)
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

Floor accumulator

Clients keep an internal, per-transaction floor_gas_used counter on the execution environment for the duration of the transaction. It is not part of the signed transaction, RLP-encoded, gossiped, or persisted; no new transaction field or type is introduced. The counter is seeded with the static floor (below) and extended at runtime. No gas is reserved or deducted from the execution budget; the counter is checked against tx.gas only to ensure the user can pay the floor if it ends up binding.

def extend_floor(tx_env, num_bytes):
    new_floor = tx_env.floor_gas_used + num_bytes * FLOOR_GAS_PER_BYTE
    if new_floor > tx_env.gas_limit:    # tx_env.gas_limit = tx.gas
        raise OutOfGasError
    tx_env.floor_gas_used = new_floor

extend_floor MUST be called BEFORE the matching BAL insertion or state mutation. An OutOfGasError aborts the operation before any unpaid BAL byte exists.

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.floor_gas_used is initialised to static_floor 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 extends the floor at runtime.

Per-transaction accounting always adds further BAL entries that no opcode places there: sender, coinbase, and target (each with address plus the relevant balance/nonce changes); recipient balance for value transfers; contract nonce on creation; 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 floor extensions

Trigger Bytes added
Cold account access (BALANCE, EXT*, CALL*, SELFDESTRUCT beneficiary, CREATE/CREATE2 deployed address) 20
Cold storage access (SLOAD, SSTORE) 32
SSTORE whose new value differs from the current value +32
CALL with non-zero value to a different account 32
SELFDESTRUCT with non-zero balance to a different beneficiary 32
CREATE / CREATE2 after the collision check (new contract’s nonce) 8
CREATE / CREATE2 with non-zero endowment (new contract’s balance) +32
Successful CREATE / CREATE2 deploy, just before set_code len(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.

Final charge

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

Transaction validation continues to require tx.gas >= 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 opcode intrinsic costs 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 cannot make the floor bind on their own

Every runtime trigger pairs its extend_floor call with an execution charge larger than the floor extension itself:

Trigger Floor extension Min execution charge
Cold account access 1,280 2,600
Cold SLOAD 2,048 2,100
SSTORE (cold, zero → non-zero) 4,096 22,100
CALL with non-zero value 2,048 9,000
SELFDESTRUCT with non-zero balance 2,048 5,000
CREATE / CREATE2 (nonce only) 512 32,000
Deployed code (per byte) 64 200

For every BAL-contributing opcode, floor_extension < execution_gas. Executing one more such opcode raises execution_gas_used by strictly more than it raises floor_gas_used. A transaction whose execution gas already exceeds its static floor therefore stays execution-dominated no matter how many BAL-contributing opcodes it runs: the gap between execution_gas_used and floor_gas_used can only widen. Floor-side OutOfGasError (new_floor > tx.gas inside extend_floor) requires the floor accumulator to climb past tx.gas; from an execution-dominated state, execution-side OOG fires first.

The floor only binds when the static seed defined by EIP-8131 (calldata, access-list entries, auths, blob hashes) already pushes static_floor close to tx.gas before any opcode runs. That is precisely the calldata + cold-SLOAD bypass this EIP targets. Transactions whose static seed is small, the common case, cannot reach a binding floor through opcode execution alone.

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.

Extend before, not refund after

EIP-7928 keeps accessed addresses and storage keys in the BAL even when their call frame reverts. Extending the floor accumulator before the insertion guarantees every BAL byte is accounted for, and an out-of-gas at the extension aborts before the matching insertion. A refund-on-revert design would have to interact with EIP-3529 storage refunds and the existing OOG semantics; up-front accounting avoids both.

Per-auth coverage is static

set_delegation runs outside the EVM’s exception handler. A runtime extend_floor 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 tx.gas 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. extend_floor only increments a parallel accumulator (floor_gas_used) and checks that it remains ≤ tx.gas. 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.

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 that opcodes contribute during execution. The combined floor per auth is (108 + 51) × 64 = 10,176 gas, still below the AUTH_PER_EMPTY_ACCOUNT = 25,000 intrinsic per auth. The minimum tx.gas for a transaction only changes when the floor side already dominated, i.e. for calldata-heavy or BAL-heavy transactions. 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%. The BAL piece never bites 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 job is to close the calldata + cold-SLOAD bypass when paired with EIP-8131, not to add a standalone floor.

Wallets and eth_estimateGas MUST compute the new static floor (including the per-auth term) and, when tracing execution, accumulate the runtime floor extensions for cold accesses, 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

Pre-EIP optimum at gas_limit = 60M: 937,500 non-zero calldata bytes plus 21,429 cold SLOADs in one transaction.

  • Static seed: 21,000 + 64 × 937,500 = 60,021,000.
  • Per cold SLOAD floor extension: 32 × 64 = 2,048.
  • Cumulative floor after 21,429 SLOADs: 60,021,000 + 21,429 × 2,048 ≈ 103.9M, exceeding gas_limit. The transaction fails validation or hits OutOfGasError mid-execution.
  • The largest feasible mix yields B = gas_limit / 64 = 937,500 block bytes regardless of the calldata / SLOAD split.

Cold SSTORE (zero → non-zero)

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

  • Floor extension: 32 (key) + 32 (value) = 4,096 gas added to floor_gas_used.
  • Intrinsic SSTORE cost: 22,100. max(intrinsic, floor) = intrinsic. The floor accumulator is dominated by execution gas.

Reverted CALL with value

CALL with value > 0 to a recipient that reverts.

  • 32-byte balance extension is added to floor_gas_used before generic_call.
  • The recipient’s balance change is discarded on revert (per EIP-7928); the accumulator is not rewound.
  • Over-accounting by 32 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. The floor accumulator is not rewound for discarded entries. Safe: the accounting over-charges, never under-charges.

Repeated writes to the same slot

SSTORE adds another 32 bytes to the floor accumulator each time it changes the current value, even though the BAL records only one final post-value per slot per transaction. The accounting over-charges; the bound still holds.

Gas estimation

Wallets and eth_estimateGas MUST simulate floor_gas_used alongside intrinsic gas. Without it, estimates for floor-bound transactions are too low and submissions will be rejected at validation.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Toni Wahrstätter (@nerolation), "EIP-8279: Block Access List Byte Floor [DRAFT]," Ethereum Improvement Proposals, no. 8279, May 2026. Available: https://eips.ethereum.org/EIPS/eip-8279.