---
eip: 8279
title: Block Access List Byte Floor
description: Meter EIP-7928 Block Access List bytes at runtime and fold them into the EIP-7623 transaction floor.
author: Toni Wahrstätter (@nerolation)
discussions-to: https://ethereum-magicians.org/t/eip-8279-block-access-list-byte-floor/28662
status: Draft
type: Standards Track
category: Core
created: 2026-05-23
requires: 7623, 7702, 7928, 7976, 7981, 8131
---

## Abstract

Meter the bytes each opcode adds to the [EIP-7928](./eip-7928.md) 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 MB into a 60M-gas block: 75% of gas on cold `SLOAD`s (32 BAL bytes per 2,100 gas) + 25% on calldata at 16 gas/byte. On top of [EIP-8131](./eip-8131.md)'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](./eip-7623.md) caps worst-case block size by charging at least 64 gas per non-zero calldata byte. [EIP-7981](./eip-7981.md) extended that to access-list entries, and [EIP-8131](./eip-8131.md) generalises the floor to a uniform per-byte rule over all tx-content fields, including [EIP-7702](./eip-7702.md) authorization tuples and [EIP-4844](./eip-4844.md) blob versioned hashes. [EIP-7928](./eip-7928.md) 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](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174).

### 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
```

### Data meter and floor accumulator

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

- `bal_data_bytes` — an explicit count of the bytes each opcode contributes to the BAL, and
- `floor_gas_used` — the floor accumulator, derived from the static seed plus the metered bytes.

Neither 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) with `bal_data_bytes = 0`, and both are extended at runtime. No gas is reserved or deducted from the execution budget; the derived floor is checked against `tx.gas` only to ensure the user can pay it if it ends up binding.

```
def meter_bal_data(tx_env, num_bytes):
    tx_env.bal_data_bytes += num_bytes
    new_floor = tx_env.static_floor + tx_env.bal_data_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
```

`meter_bal_data` MUST be called BEFORE the matching BAL insertion or state mutation. An `OutOfGasError` aborts the operation before any unpaid BAL byte exists. The metered count is a deliberate upper bound on the transaction's actual BAL contribution (see [Reverts](#reverts) and [Repeated writes to the same slot](#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](./eip-8131.md) 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 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 data metering

Each trigger calls `meter_bal_data` with the byte count below, before the corresponding BAL entry is added:

| Trigger | Bytes added |
|---|---|
| Cold account access (`BALANCE`, `EXT*`, `CALL*`, `SELFDESTRUCT` beneficiary, `CREATE`/`CREATE2` deployed address) | 20 |
| Cold storage access (`SLOAD`, `SSTORE`) | 32 |
| `SSTORE` first moving a slot off its pre-transaction value | +32 |
| `SSTORE` returning a slot to its pre-transaction value | −32 (refund) |
| `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.

The 32 storage-value bytes are metered at most once per slot, tracking whether the slot's current value differs from its pre-transaction value: charged on the first `SSTORE` that makes it differ, refunded by a later `SSTORE` that restores it. The refund subtracts from `bal_data_bytes` and recomputes `floor_gas_used = static_floor + bal_data_bytes * FLOOR_GAS_PER_BYTE`. This mirrors [EIP-7928](./eip-7928.md), which records a single post-value per changed slot and demotes a slot whose post-value equals its pre-value to a no-op write in `storage_reads` — the slot key remains (covered by the cold-access row above), only the value entry disappears. The condition is the BAL's no-op-write rule, **not** the [EIP-3529](./eip-3529.md) `SSTORE` gas refund: clearing a non-zero slot to zero earns a gas refund but is still a recorded BAL write, so its value bytes are never refunded.

### 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-2935.md), [EIP-4788](./eip-4788.md), [EIP-7002](./eip-7002.md), [EIP-7251](./eip-7251.md)) 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 `meter_bal_data` 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 `meter_bal_data`) 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](./eip-8131.md) (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`.

### Meter before; refund only when bytes leave the BAL

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](#reverts)), because rewinding them would entangle the meter with the existing OOG and refund machinery for no benefit — the count feeds nothing but a floor that rarely binds, so over-counting is free.

The one refund is the storage-value byte, and it is safe precisely because it is keyed to the BAL rather than to gas. A slot restored to its pre-transaction value becomes a no-op write under EIP-7928, so its 32 value bytes genuinely leave the BAL; refunding only when bytes provably leave the BAL keeps the meter an upper bound and never under-counts, so the [block-size bound](#block-size-bound) holds. This is deliberately narrower than the [EIP-3529](./eip-3529.md) `SSTORE` gas refund, which also fires when clearing a non-zero slot to zero — a case EIP-7928 still records as a write, and which therefore earns no byte refund. Keying the byte refund to the gas refund instead would drop bytes that remain in the BAL and break the bound.

### 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 `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. `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 `≤ 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, meter the runtime BAL bytes 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 `SLOAD`s 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 `SLOAD`s: `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.

- Metered: `32 (key) + 32 (value) = 64` BAL bytes → `4,096` gas folded into `floor_gas_used`.
- Intrinsic `SSTORE` cost: 22,100. `max(intrinsic, floor) = intrinsic`. The floor accumulator is dominated by execution gas.

### 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 are refunded.
- Net metered: `32` (key only). The BAL records the slot in `storage_reads`, with no `storage_changes` value entry — the meter matches.

### 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.

### Reverted `CALL` with value

`CALL` with `value > 0` to a recipient that reverts.

- 32 balance bytes are metered (folded into `floor_gas_used`) before `generic_call`.
- The recipient's balance change is discarded on revert (per EIP-7928); neither `bal_data_bytes` nor the floor is 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. Neither `bal_data_bytes` nor the derived floor is rewound for discarded entries. The storage-value refund fires only on an explicit `SSTORE` back to the pre-transaction value on the committed path — value bytes charged inside a frame that later reverts are left in place, not refunded. Safe: the meter over-counts, never under-counts.

### Repeated writes to the same slot

The 32 storage-value bytes are metered once per slot — on the first `SSTORE` that moves it off its pre-transaction value — and refunded if it returns there, matching the single post-value the BAL records per changed slot. Repeated distinct writes (`A → B → C`) therefore meter 32 value bytes total, not 32 per write, and a net-zero round-trip (`O → X → O`) meters none. `bal_data_bytes` can still over-count where the BAL deduplicates further (e.g. value bytes charged in a frame that later reverts, never rewound); the bound 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

Copyright and related rights waived via [CC0](../LICENSE.md).
