# Besu: EIP-2780 / EIP-7976 / EIP-7981 intrinsic gas, calldata floor and access-list cost for Amsterdam

Item `besu`, batch `el-intrinsic`. Review method: I read code only, from shallow clones of the repositories pinned below, and executed nothing against any network or node. Amsterdam/Glamsterdam is not live on Ethereum mainnet, so the live-code withholding rule does not apply to anything below.

## Result in one line

**No candidate.** In the scope of this item, Besu's Amsterdam intrinsic gas, calldata floor, access-list charge, the order of validity checks, and the refund-then-floor settlement all compute the same values as the execution-specs Amsterdam fork at the pinned commit, for every transaction shape I checked. Geth agrees with both. One known, open Besu PR covers a floor-related difference in an RPC-only value (`eth_estimateGas` / `eth_simulateV1`). It is not consensus, and execution-specs does not define that value (see Known-check). At the end there is one pointer outside this item's scope, for the EIP-8037 reviewer. It is not verified.

## Pins

| Repo | Ref | Commit (pinned via api.github.com/repos/…/commits/<ref>) | Commit date |
|---|---|---|---|
| besu-eth/besu | main | `aa06c4f8dfc461e483d46c6a1ac72b5bfef8d945` | 2026-10-27T13:41:33Z |
| ethereum/execution-specs | forks/bogota (default branch) | `7f2f4c9bf6d73b09ecb8b24eb14d08bf0cf0247b` | 2026-10-27T17:40:55Z |
| ethereum/go-ethereum | master | `8817f99f92852f8ac53e596de6202dd9e3494065` | 2026-10-27T03:32:47Z |

The shallow clones' `git rev-parse HEAD` matched these hashes. EIP texts are from eips.ethereum.org/EIPS/eip-N (raw markdown in ethereum/EIPS master). EIP-2780, EIP-7976 and EIP-7981 are all `status: Last Call`.

URL shorthands used below:
- `B/` = https://github.com/besu-eth/besu/blob/aa06c4f8dfc461e483d46c6a1ac72b5bfef8d945/
- `S/` = https://github.com/ethereum/execution-specs/blob/7f2f4c9bf6d73b09ecb8b24eb14d08bf0cf0247b/src/ethereum/forks/amsterdam/
- `G/` = https://github.com/ethereum/go-ethereum/blob/8817f99f92852f8ac53e596de6202dd9e3494065/

Besu paths abbreviated below:
- `AGC` = `evm/src/main/java/org/hyperledger/besu/evm/gascalculator/AmsterdamGasCalculator.java`
- `MTV` = `ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java`
- `MTP` = `ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionProcessor.java`
- `TGA` = `ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/TransactionGasAccounting.java`

## Reference formula (execution-specs, pinned)

`S/transactions.py#L689-L798` (`calculate_intrinsic_cost`) defines both values:
- **Execution side:** `TX_BASE + recipient_execution_gas + init_code_gas + data_cost + access_list_cost + access_list_data_cost + auth_cost`.
- **Floor side:** `TX_BASE + recipient_execution_gas + len(data)*4*16 + access_list_data_cost`.

Each term is defined as follows:
- `recipient_execution_gas`:
  - `CREATE_ACCESS` (12000) for a creation;
  - `0` for a self-transfer (`tx.to == sender`);
  - otherwise `COLD_ACCOUNT_ACCESS` (3000), plus `TX_VALUE_COST` (6000) if `tx.value > 0`.
- `access_list_data_cost = (80·addresses + 128·keys) · 16`. It is added to both sides: "Charge the access list data surcharge on both sides of the gas-used maximum" (`S/transactions.py#L762-L766`).
- Constants (`S/vm/gas.py`):
  - `TX_BASE` 12000 and `TX_VALUE_COST` 6000 (L143-L144);
  - `TX_DATA_TOKEN_STANDARD` 4 and `TX_DATA_TOKEN_FLOOR` 16 (L145-L146);
  - `TX_ACCESS_LIST_ADDRESS = COLD_ACCOUNT_ACCESS − WARM_ACCESS` = 2900 and `TX_ACCESS_LIST_STORAGE_KEY = COLD_STORAGE_ACCESS − WARM_ACCESS` = 2000 (L147-L152);
  - `EXECUTION_PER_AUTH_BASE_COST = 101·16 + 3000 + 3000 + 2·100` = 7816 (L157-L163);
  - `init_code_cost = 2·ceil32(len)/32` (L864-L882).
- Floor-token constants: `ACCESS_LIST_ADDRESS_FLOOR_TOKENS = 80` and `ACCESS_LIST_STORAGE_KEY_FLOOR_TOKENS = 128` (`S/transactions.py#L69-L77`).

Validity (`S/transactions.py#L588-L687`, `validate_transaction`): the transaction is invalid if any of these holds:
- `tx.gas > TX_MAX_TOTAL_GAS_LIMIT` (2^32−1);
- `intrinsic.execution > tx.gas`;
- `calldata_floor > tx.gas`;
- `intrinsic.execution > TX_MAX_GAS_LIMIT` (2^24);
- `calldata_floor > TX_MAX_GAS_LIMIT`.

All of these are strict `>`, so equality is valid.

Settlement (`S/vm/gas.py#L1163-L1220`):
```
gas_refund = min(gas_used_before_refund // 5, refund_counter)
gas_used = max(gas_used_after_refund, calldata_floor)
execution_gas_used = max(gas_used_before_refund − max(0, state_gas_used), calldata_floor)
```
`process_transaction` calls this function (`S/fork.py#L1056-L1063`).

How the EIP texts relate to the spec:
- **EIP-2780.** The intrinsic decomposition and the floor anchor ("that base term is replaced by the transaction's decomposed base — TX_BASE_COST plus the applicable recipient and value primitives … Per-authorization charges and initcode word charges are excluded from this base") agree with the spec.
- **EIP-7976.** "floor_tokens_in_calldata = (zero_bytes_in_calldata + nonzero_bytes_in_calldata) * 4" and "the floor cost is 64 gas per calldata byte".
- **EIP-7981.** The prose says "The access list data cost is added as a flat surcharge to the gas used calculation", with `access_list_data_cost = 64 * access_list_bytes` (1280 per address, 2048 per key). Adding a constant outside `max()` is algebraically the same as adding it to both arguments, which is what the spec does. So the "flat surcharge" prose and the "both sides" spec are numerically identical.
- **EIP-7981 parameter table.** It still lists `ACCESS_LIST_ADDRESS_COST` 2400 / `ACCESS_LIST_STORAGE_KEY_COST` 1900 (EIP-2930 values). The spec and every client reviewed here use the EIP-8038 values 2900/2000.

## Candidates

None. Each topic in the brief was compared and found to match; see the table below. I ran the disproof the other way round: for each rule I looked for an input (a transaction shape, a boundary value, or the first Amsterdam block) on which Besu's arithmetic diverges from the spec. I found none.

## Rules checked and found correct (Besu file#line at `aa06c4f8…`)

| # | Rule (spec) | Besu | Geth | Verdict |
|---|---|---|---|---|
| 1 | `TX_BASE` = 12000 replaces 21000 (`S/vm/gas.py#L143`) | `B/AGC#L113`, used at `B/AGC#L214-L218` and `B/AGC#L239` | `TxBaseCost2780 = 12000` `G/params/protocol_params.go#L111`, `G/core/state_transition.go#L172` | match |
| 2 | Recipient: creation → `CREATE_ACCESS` 12000; self-transfer → 0; otherwise → `COLD_ACCOUNT_ACCESS` 3000 (`S/transactions.py#L736-L747`) | `B/AGC#L247-L263`; `CREATE_ACCESS = ACCOUNT_WRITE + COLD_ACCOUNT_ACCESS` at `B/AGC#L102`; self-transfer test `to == getSender()` at `B/AGC#L266-L271` | `G/core/state_transition.go#L163-L184` | match |
| 3 | `TX_VALUE_COST` 6000 only for a non-self, non-create call with value > 0. Creation with value pays nothing extra (`S/transactions.py#L744-L747`; EIP-2780 "if non-zero and contract creation, there are no charges") | `B/AGC#L125`, `B/AGC#L248-L262` (creation returns `CREATE_ACCESS` regardless of value; self-transfer returns 0) | `G/core/state_transition.go#L186-L192` | match |
| 4 | Calldata execution cost: tokens = zero + 4·nonzero, times 4 (`S/transactions.py#L732-L734`, `#L801-L810`) | `B/AGC#L226-L231` (zero count from `Transaction.getPayloadZeroBytes`, `B/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/Transaction.java#L424-L426`) | `G/core/state_transition.go#L93-L112` (4/16 per byte) | match |
| 5 | Init-code word cost `2·ceil(len/32)` for creations, execution side only, not in the floor anchor (`S/transactions.py#L741-L743`, `#L779-L786`; `S/vm/gas.py#L864-L882`) | `B/AGC#L137`, `B/AGC#L233-L237`, `B/AGC#L273-L276` | `G/core/state_transition.go#L114-L120` | match |
| 6 | Access-list per-entry cost 2900/2000 (EIP-8038; `S/vm/gas.py#L147-L152`) | `B/AGC#L80-L84`, `B/AGC#L199-L212` | `G/params/protocol_params.go#L124-L125`, `G/core/state_transition.go#L127-L141` | match |
| 7 | EIP-7981 access-list data charge, 80/128 tokens × 16 = 1280/2048, on the execution side (`S/transactions.py#L749-L766`, `#L794`) | Added per entry inside `accessListGasCost`: `B/AGC#L55-L63`, `B/AGC#L206-L211` | `G/core/state_transition.go#L143-L157` | match |
| 8 | Same EIP-7981 charge on the floor side (`S/transactions.py#L782-L786`) | `B/AGC#L187-L196` and `B/AGC#L278-L285`: (20·addr + 32·keys) bytes × 64 = (80·addr + 128·keys) × 16 | `G/core/state_transition.go#L212-L225` (20·4 and 32·4 tokens × 16) | match |
| 9 | EIP-7976 floor: every calldata byte = 4 tokens × 16 = 64 gas (`S/transactions.py#L774-L775`, `#L782-L786`) | `B/AGC#L51-L52`, `B/AGC#L187`, `B/AGC#L194-L196` | `G/core/state_transition.go#L202-L211`, `#L251-L255` | match |
| 10 | Floor anchored on `TX_BASE + recipient_execution_gas` (includes `TX_VALUE_COST`; excludes init-code and auth) (`S/transactions.py#L777-L786`) | `B/AGC#L190-L193` (same helper `baseRecipientExecutionGas` as the intrinsic) | `G/core/state_transition.go#L243-L249` | match |
| 11 | EIP-7702 intrinsic: `EXECUTION_PER_AUTH_BASE_COST` 7816 × authorizations, with no intrinsic `PER_EMPTY_ACCOUNT_COST` and no refund (`S/transactions.py#L768-L772`) | `B/AGC#L133-L134`, `B/AGC#L399-L405`; summed via `B/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/GasCalculator.java#L603-L607` and `B/MTV#L255-L262`; Amsterdam path sets no delegation refund (`B/MTP#L310`, `#L326-L330`; `codeDelegationRefund` is assigned only in the non-state-gas branch, `#L343-L345`) | `ExecutionPerAuthBaseCost = 7816` `G/params/protocol_params.go#L108`, `G/core/state_transition.go#L85-L88` | match |
| 12 | Validity checks: `intrinsic > gas`, `floor > gas`, `intrinsic > 2^24`, `floor > 2^24`, `gas > 2^32−1` (strict `>`) (`S/transactions.py#L640-L641`, `#L671-L685`) | `max(intrinsic, floor)` against the 2^24 cap at `B/MTV#L255-L273` and against `tx.gas` at `#L275-L281`, both via `compareUnsigned(...) > 0`; `tx.gas` against 4,294,967,295 at `B/MTV#L112-L121` with `B/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AmsterdamTargetingGasLimitCalculator.java#L33` and `#L54-L57` (2^24 = `EIP_7825_TRANSACTION_GAS_LIMIT_CAP`, `OsakaTargetingGasLimitCalculator.java#L37`), wired for Amsterdam at `B/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java#L1169-L1183` | `G/core/state_transition.go#L733-L753` | match. The checks run in a different order, but every failing check makes the transaction invalid, so the outcome is the same. Equality is valid in all three implementations. |
| 13 | Intrinsic checked before balance/nonce (`S/fork.py#L539` before `check_nonce` and the balance check) | `validate` (intrinsic/floor) at `B/MTP#L214-L226` comes before `validateForSender` at `#L232-L238`; the intrinsic is re-checked at `#L288-L303` | `G/core/state_transition.go#L733-L747` | match (block validity is the same either way) |
| 14 | Gas split after intrinsic: `execution = min(2^24 − intrinsic, gas − intrinsic)`, remainder to the reservoir (`S/vm/gas.py#L1105-L1137`) | `B/MTP#L354-L371` | n/a | match |
| 15 | Refund cap 1/5 of pre-refund usage (execution + state); floor applied **after** the refund: `max(after_refund, floor)` (`S/vm/gas.py#L1210-L1214`) | `B/AGC#L412-L435` (`totalConsumed/5`, `Math.max(gasUsed, floorCost)`); quotient 5 at `B/evm/src/main/java/org/hyperledger/besu/evm/gascalculator/LondonGasCalculator.java#L30`, `#L96-L98`; self-destruct refund 0 at `#L48-L50`; receipt/sender gas at `B/TGA#L105` | `G/core/state_transition.go#L1066-L1085` | match |
| 16 | Boundary where floor = execution cost: `max()` returns either; no strict/non-strict divergence (`S/vm/gas.py#L1214`, `#L1217-L1222`) | `B/AGC#L434`, `B/TGA#L103` | `G/core/state_transition.go#L1066`, `#L1078` (`gasUsed < floor`, so equality leaves it unchanged) | match |
| 17 | Block execution-gas dimension carries the floor: `max(pre_refund − state, floor)` (`S/vm/gas.py#L1216-L1222`) | `B/TGA#L91-L106`, accumulated at `B/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessor.java#L304-L307` | `G/core/state_transition.go#L1066` | match (see the pointer at the end on the `max(0, state)` clamp) |
| 18 | One formula for the txpool and block validation | The txpool calls the same `MainnetTransactionValidator.validate` (`B/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java#L519-L528`) with the same `GasCalculator`. The spec is selected from the **chain head** (`#L476-L481`), so on the last pre-Amsterdam head the pool still applies Osaka rules. Block import/building re-validates with the block's own spec (`B/MTP#L214-L226`) | Geth's pool also uses head rules: `G/core/txpool/validation.go#L74`, same `IntrinsicGas`/`FloorDataGas` at `#L135-L156` | not a spec rule; Besu and Geth behave the same. At most a transient pool-admission difference at the fork boundary, with no block-validity effect. |
| 19 | First Amsterdam block | Amsterdam milestone wiring selects `AmsterdamGasCalculator` and `AmsterdamTargetingGasLimitCalculator` per block header (`B/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java#L1169-L1183`); every intrinsic and floor term is fork-local, with no parent-dependent input | `rules.IsAmsterdam` throughout `G/core/state_transition.go#L72-L256` | match |
| 20 | Protocol minimum-gas filters | Block builder "block full" threshold is `getMinimumTransactionCost()` = 12000 in Amsterdam (`B/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/selectors/BlockSizeTransactionSelector.java#L148-L153`); `eth_estimateGas` lower bound uses the same value (`B/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/AbstractEstimateGas.java#L115`, `#L153`). No hard-coded 21000 found in the core/eth/api/blockcreation main sources (grep for `21_?000`) | n/a | heuristics only; consistent with 12000 |
| 21 | Overflow | Products use `clampedAdd`/`clampedMultiply`, or are bounded by the maximum init-code size and RLP size (`B/AGC#L187-L196`, `#L206-L211`, `#L230-L239`) | explicit overflow errors at `G/core/state_transition.go#L104-L156`, `#L207-L253` | no reachable difference |

Worked values that are the same in Besu, the spec and Geth (from the formulas above):
- Self-transfer: 12000.
- Zero-value call: 15000.
- Value transfer: 21000.
- Creation with or without value: 24000 + 2·words + calldata.
- Floor of a value transfer with 100 calldata bytes: 21000 + 6400.
- An access list of 1 address and 1 key adds 2900 + 2000 + 1280 + 2048 to the intrinsic, and 1280 + 2048 to the floor.

## Known-check

- **besu-eth/besu issues/PRs** (GitHub search API, open and closed). Queries: `repo:besu-eth/besu 2780`, `7976`, `7981`, `intrinsic gas amsterdam`, `calldata floor`, `access list floor`. Relevant hits:
  - Implementation PRs, all closed:
    - https://github.com/besu-eth/besu/pull/10246, /10375 ("Add EIP-7976 to Amsterdam")
    - /10247, /10388 ("Add EIP-7981 to Amsterdam")
    - /10908 (EIP-2780/7702/8038 intrinsic gas)
    - /10925 (EIP-2780 fold transfer log into TX_VALUE_COST)
    - /10930 (EIP-3120/8037/8038 calldata floor)
    - /11140 (devnet-8 EIP-2780/8038 revisions)
    - /10594 (reject tx whose calldata floor exceeds the cap)
    - /11411 and /11414 (tx.gas cap at TX_MAX_TOTAL_GAS_LIMIT)
  - **Open PR https://github.com/besu-eth/besu/pull/11580** "Apply the calldata floor to the total gas used before refunds" (open, created 2026-10-13). It fixes the RPC-only `maxUsedGas`/estimate value, currently `max(execution, floor) + state` at `B/TGA#L103-L104`, which should be `max(execution + state, floor)`, as Geth's `peakUsed` computes (`G/core/state_transition.go#L1077-L1085`). The PR states that block accounting and receipts are unchanged. execution-specs does not define this RPC value. Status: **known (open PR)**; not consensus; not counted as a candidate.
  - Open issue https://github.com/besu-eth/besu/issues/11492: the BFT `transactiongaslimit` override drops the Amsterdam intrinsic cap of 2^24. It affects BFT/private chains only, not mainnet Amsterdam. Status: **known (open)**; outside this item's scope.
- **execution-specs "Test Release Tracker" issues.** Searched `repo:ethereum/execution-specs "Test Release Tracker" glamsterdam`:
  - https://github.com/ethereum/execution-specs/issues/3217 (`tests@v21.0.0`, Glamsterdam Testnet, open)
  - /3518, /3479, /3452, /3311 (`glamsterdam-devnet@v8.1.x`, closed)
  - /3034 (`glamsterdam-devnet@v7.0.0`, closed)

  Issues #3217 and #3518 (bodies and comments) mention no Besu entry. I did not read the other trackers' comments.
- **hive.ethpandaops.io/glamsterdam/listing.jsonl** (fetched 2026-10-27).
  - Latest Besu runs: `eels/consume-engine` (besu v26.10-develop-bdb9e5c, 2026-10-25): 44225 tests, 9 fails. `eels/consume-enginex` (fc46028, 2026-10-26): 83258 tests, 69 fails.
  - The failing tests are `test_unrecoverable_signature`, `test_reject_incorrect_two_dimensional_header` (EIP-8037 header rules), BPO-transition blob tests, and EIP-7918 reserve-price transitions. **None is in the eip2780/eip7976/eip7981/eip7623/eip8038 test directories.**
  - In the consume-engine run, all 292 eip2780, 553 eip7976, 194 eip7981, 966 eip7623 and 274 eip8038 test cases pass.
  - Caveat: those hive builds are at Besu develop `bdb9e5c`/`fc46028`, not at the pin `aa06c4f8`.
- Already indexed by the requester: none.
- Overall: no intrinsic/floor/access-list candidate, so no "known" mark is needed. The only floor-related open item (#11580) is RPC-only.

## Pointer outside this item's scope (EIP-8037, not verified)

The spec clamps state gas before the block execution dimension: "the state gas used (clamped to zero, since refunds can drive it negative)", `settled_state_gas_used = StateGas(Uint(max(0, state_gas_used)))` (`S/vm/gas.py#L1216-L1222`).
- **Besu** uses the unclamped `stateGasUsed()` in `B/TGA#L92-L106` (`executionGas = consumedGas − stateGas`) and adds it unclamped to `cumulativeStateGasUsed` (`B/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/AbstractBlockProcessor.java#L312`). `stateGasUsed` can be decremented by `refundRolledBackStateGas` (`B/MTP#L835`).
- **Geth** returns an error when the top-frame state gas is negative (`G/core/state_transition.go#L1047-L1049`).

If the top-frame state gas could end up negative, the three implementations would differ. I did not establish whether it can be negative in any of them; that belongs to the EIP-8037 reviewer. I report no candidate for it here.

## URLs read

- https://api.github.com/repos/besu-eth/besu/commits/main · https://api.github.com/repos/ethereum/execution-specs/commits/forks/bogota · https://api.github.com/repos/ethereum/go-ethereum/commits/master
- https://github.com/besu-eth/besu (shallow clone @aa06c4f8): the files cited above
- https://github.com/ethereum/execution-specs (shallow clone @7f2f4c9b): `src/ethereum/forks/amsterdam/transactions.py`, `vm/gas.py`, `fork.py`
- https://github.com/ethereum/go-ethereum (shallow clone @8817f99f): `core/state_transition.go`, `params/protocol_params.go`, `core/txpool/validation.go`
- https://eips.ethereum.org/EIPS/eip-2780 · https://eips.ethereum.org/EIPS/eip-7976 · https://eips.ethereum.org/EIPS/eip-7981 · https://eips.ethereum.org/EIPS/eip-8038 (raw via https://raw.githubusercontent.com/ethereum/EIPS/master/EIPS/eip-N.md)
- https://github.com/besu-eth/besu/pull/11580 · https://github.com/besu-eth/besu/issues/11492 (via api.github.com) and the search results listed above
- https://github.com/ethereum/execution-specs/issues/3217 · https://github.com/ethereum/execution-specs/issues/3518
- https://hive.ethpandaops.io/glamsterdam/listing.jsonl · https://hive.ethpandaops.io/glamsterdam/results/1793007262-bcaea78021015fd4b0776e606b920c4d.json · https://hive.ethpandaops.io/glamsterdam/results/1793018005-fa5bfa67da513c8de132d620958d92dc.json
