# Erigon: EIP-2780 / EIP-7976 / EIP-7981 intrinsic gas, calldata floor and access-list cost (Amsterdam) vs spec

Batch `el-intrinsic`, item `erigon`. This is a review of public code only. Nothing was run against any network or node. Amsterdam is not live on mainnet, so none of this is live code.

## 1. Pins

| Repo | Branch | Commit (from `api.github.com/repos/<repo>/commits/<branch>`) | Commit date |
|---|---|---|---|
| erigontech/erigon | main | `f32a39d1e5f87a9ec3a9ce2858171c74acc3c242` | 2026-10-27T16:04:32Z |
| ethereum/execution-specs | forks/bogota (default branch) | `7f2f4c9bf6d73b09ecb8b24eb14d08bf0cf0247b` | 2026-10-27T17:40:55Z |
| paradigmxyz/reth | main | `e0378eb691aa2770bb96fd4ecec2f4445a2a3e7c` | 2026-10-27T12:11:23Z |
| alloy-rs/evm2 (Reth's EVM, see note) | rev pinned by Reth | `4a53ec080dfbb904e98e3ca8f0abcc11ab8abae0` | n/a |

Each repo was shallow-cloned and `git rev-parse HEAD` matched the API hash.

Reth note: Reth no longer computes intrinsic gas itself. `Cargo.toml#L440-L443` pins `evm2 = { git = "https://github.com/alloy-rs/evm2", rev = "4a53ec080dfbb904e98e3ca8f0abcc11ab8abae0" }`, and `Cargo.toml#L676-L677` patches it to `DaniPopes/evm2` at the same rev. The formula was read from `raw.githubusercontent.com/alloy-rs/evm2/4a53ec08…/crates/evm2/src/ethereum/mod.rs`. The GitHub API redirects DaniPopes/evm2 to the same repository id. Reth cites below are at the Reth pin and evm2 cites are at the evm2 rev.

Abbreviations:
- **E** = `https://github.com/erigontech/erigon/blob/f32a39d1e5f87a9ec3a9ce2858171c74acc3c242/`
- **S** = `https://github.com/ethereum/execution-specs/blob/7f2f4c9bf6d73b09ecb8b24eb14d08bf0cf0247b/src/ethereum/forks/amsterdam/`
- **R** = `https://github.com/paradigmxyz/reth/blob/e0378eb691aa2770bb96fd4ecec2f4445a2a3e7c/`
- **V** = `https://github.com/alloy-rs/evm2/blob/4a53ec080dfbb904e98e3ca8f0abcc11ab8abae0/`

## 2. Summary

- **Block validity, gas, fees and settlement:** Erigon's consensus path agrees with the spec on every rule in the brief (table in §4). That path is `execution/protocol/mdgas/intrinsic_gas.go` → `execution/protocol/txn_executor.go`.
- **Constants:** these all match: TX_BASE 12000, TX_VALUE_COST 6000, COLD_ACCOUNT_ACCESS 3000, CREATE_ACCESS 12000, access-list costs 2900/2000, the 7702 auth base 7816, floor tokens 4/byte at 16 gas/token, and access-list floor tokens 80/128 on both sides.
- **Supporting evidence:** this agrees with public hive results. Every Erigon run of the `glamsterdam` group in `hive.ethpandaops.io/glamsterdam/listing.jsonl` on 2026-10-26 had 0 failures. Examples: `eels/consume-enginex` 83258/83258 (`3.9.0-dev-391edfe5`), `eels/consume-engine` 44225/44225, `eels/consume-rlp` 44161/44161, `eels/execute, hive mode` 173/173.
- **One candidate (C1), not consensus:** when the txpool reloads persisted transactions at startup, it validates them without the sender. So a self-transfer loses its EIP-2780 carve-out there, and a valid low-gas self-transfer is dropped from the pool. This affects the local txpool only.

## 3. Candidates

### C1. The txpool reload from DB charges self-transfers as calls to another account (EIP-2780), so valid self-transfers are dropped

**(1) Spec.**
- S`transactions.py#L736-L747`:
  ```
  is_create = tx.to == Bytes0(b"")
  is_self_transfer = tx.to == sender
  ...
  elif not is_self_transfer:
      recipient_execution_gas = GasCosts.COLD_ACCOUNT_ACCESS
      if tx.value > U256(0):
          recipient_execution_gas += GasCosts.TX_VALUE_COST
  ```
- S`transactions.py#L719-L720`: "Self-transfers (``sender == tx.to``) skip the recipient and value charges."
- EIP-2780 (https://eips.ethereum.org/EIPS/eip-2780): "A self-transfer (`tx.sender == tx.to`) charges neither the recipient touch nor the value cost".

**(2) Erigon at the pin.**
- E`txnprovider/txpool/pool.go#L1047-L1056` (`validateTx`):
  ```go
  txnSender, senderOk := txn.Txn.GetSender()
  to := txn.Txn.GetTo()
  intrinsicGasResult, overflow := mdgas.CalcIntrinsicGas(mdgas.IntrinsicGasCalcArgs{
  ...
      IsSelfTransfer:     senderOk && to != nil && txnSender.Value() == *to,
  ```
- E`txnprovider/txpool/pool.go#L1081-L1085`:
  ```go
  if gas > txn.GetGas() {
  ...
      return txpoolcfg.IntrinsicGas, nil
  ```
- The reload path, E`txnprovider/txpool/pool.go#L2794-L2796` and `#L2808-L2823`:
  ```go
  parseCtx := NewTxnParseContext(p.chainID)
  parseCtx.WithSender(false)
  ...
  addr, txnRlp := *(*[20]byte)(v[:20]), bytes.Clone(v[20:])
  ...
  _, err = parseCtx.ParseTransaction(txnRlp, 0, txn, nil, false /* hasEnvelope */, true /*wrappedWithBlobs*/, nil)
  ...
  txn.SenderID, txn.Traced = p.senders.getOrCreateID(addr, p.logger)
  ...
  reason, err := p.validateTx(txn, isLocalTx, cacheView)
  ```
- The sender is recovered and cached on the transaction only when `withSender` is set. E`txnprovider/txpool/pool_txn_parser.go#L396-L415` begins `if ctx.withSender && len(sender) == length.Addr { ... addr, err := txn.Sender(*signer)`, and `Sender` stores `tx.from`: E`execution/types/legacy_tx.go#L422-L434` and `dynamic_fee_tx.go#L378-L389`.
- Otherwise `GetSender` returns false: E`execution/types/legacy_tx.go#L87-L92`:
  ```go
  func (ct *CommonTx) GetSender() (accounts.Address, bool) {
  	if sc := ct.from; !sc.IsNil() {
  		return sc, true
  	}
  	return accounts.NilAddress, false
  ```
- So on reload `senderOk == false`, `IsSelfTransfer` is false, and `CalcIntrinsicGas` adds COLD_ACCOUNT_ACCESS, plus TX_VALUE_COST when there is value: E`execution/protocol/mdgas/intrinsic_gas.go#L76-L85`. The floor at `#L91-L92` is anchored on the same overstated base.

**(3) When results differ.**
- **Who is affected:** the node runs with Amsterdam active. Its persisted pool (`kv.PoolTransaction`) holds a self-transfer, meaning sender equals recipient, of any type except creation and AA.
- **Gas limits that trigger it:** the self-transfer's gas limit is at or above the spec intrinsic (12000 plus calldata and other terms), but below the overstated figure:
  - zero value: below spec intrinsic + 3000;
  - non-zero value: below spec intrinsic + 9000.
- **What happens:** when the node restarts, `fromDB` rejects the transaction with discard reason `IntrinsicGas`, even though the spec and block validation accept it. A minimal 12000-gas self-transfer is an example.
- **Paths that are fine:** live admission is unaffected, for both P2P and RPC, because the parse context default is `withSender: true` (`pool_txn_parser.go#L72`). Block building in `best` takes the sender from storage (`pool.go#L818`, `#L855`). Block execution uses `msg.From()` (`execution/protocol/txn_executor.go#L995`).
- **Result:** after a restart, the transaction silently disappears from the node's pool. It is not deleted from the pool DB, so it is re-validated on every restart.

**(4) Reth at its pin.** Reth agrees with the spec. Its pool validation always passes the recovered sender:
- R`crates/transaction-pool/src/validate/eth.rs#L1577-L1585`:
  ```rust
  let gas = gas_rules.calculate(
      transaction.sender(),
      transaction.kind(),
      transaction.value(),
  ```
- evm2 then derives the self-transfer flag from that sender, V`crates/evm2/src/ethereum/mod.rs#L852-L855`:
  ```rust
  let eip2780_base = version.feature(EvmFeatures::EIP2780).then(|| {
      let is_self_transfer = matches!(to, TxKind::Call(to) if to == caller);
      eip2780_base_to_value_gas(version, is_create, is_self_transfer, value)
  ```
- Base computation: V`mod.rs#L892-L910`.

**(5) Confidence: medium.** The code path is high confidence. How often it is hit in practice is medium.

Disproofs tried:
- (a) I looked for any `SetSender` between parse and `validateTx` in `fromDBLocked` (`pool.go#L2803-L2830`). There is none. `recoverAuthorizations` runs only after `validateTx`.
- (b) I checked whether `ParseTransaction` with `withSender=false` fills `from` some other way. It does not: step 8 is gated on `ctx.withSender`.
- (c) I checked that `accounts.Address` equality is value equality, because it is a `unique.Handle` (E`execution/types/accounts/key_types.go#L29`, `#L55-L63`). So the live paths are correct and the bug is only the missing sender.
- (d) Open issue #25273 notes that on a fresh process the reload can reject transactions anyway, because `blockGasLimit` is 0. But the intrinsic check at `pool.go#L1081` runs before the block-gas-limit check at `#L1090`. So C1 hits independently, and still hits after #25273 is fixed.
- (e) Not exercised: I did not run Erigon. No pool unit test covers `fromDB` with a self-transfer. I only saw `TestBestYieldsTxnBelowLegacyMinGasPostAmsterdam`, which is the `best` path.

**(6) Severity: cosmetic, local txpool only.** There is no consensus split, no invalid block accepted, no valid block rejected, and no wrong gas or fee charged. It does wrongly evict valid persisted transactions. It is the txpool counterpart of the brief's "txpool and block validation using one formula": the formula is the same, but one input differs on this path.

No other candidate: every other rule in scope matches the spec (§4).

## 4. Rules checked and found correct

All Erigon cites are E@`f32a39d1`. The spec column is S@`7f2f4c9b`.

| # | Rule (spec) | Erigon file#line | Result |
|---|---|---|---|
| 1 | TX_BASE = 12000 (S`vm/gas.py#L143`) | `execution/protocol/params/protocol.go#L253`; added at `execution/protocol/mdgas/intrinsic_gas.go#L76-L77` | match |
| 2 | Recipient COLD_ACCOUNT_ACCESS 3000 for a non-self call (S`transactions.py#L744-L745`, `vm/gas.py#L77`) | `protocol.go#L255`; `intrinsic_gas.go#L80-L81` | match |
| 3 | TX_VALUE_COST 6000 only for a non-self call with value > 0 (S`transactions.py#L746-L747`, `vm/gas.py#L144`) | `protocol.go#L254`; `intrinsic_gas.go#L82-L84` | match |
| 4 | Creation: CREATE_ACCESS = ACCOUNT_WRITE 9000 + COLD 3000 = 12000. No TX_VALUE_COST even with value (S`transactions.py#L741-L743`, `vm/gas.py#L85,#L90`; EIP-2780 says the value write is "already covered by `CREATE_ACCESS`") | `protocol.go#L238,#L241,#L256`; `intrinsic_gas.go#L78-L79` (the `if` branch skips value) | match |
| 5 | Self-transfer: base only (S`transactions.py#L737,#L744`) | `txn_executor.go#L995` `IsSelfTransfer: !contractCreation && st.msg.To() == st.msg.From()` (handle equality is value equality, `accounts/key_types.go#L29,#L55-L63`); `intrinsic_gas.go#L80` | match (block path); txpool reload: C1 |
| 6 | Calldata execution cost: zero byte 4, non-zero 16 (1/4 tokens × TX_DATA_TOKEN_STANDARD 4) (S`transactions.py#L732-L734,#L801-L809`) | `protocol.go#L43,#L105`; `intrinsic_gas.go#L98-L124` | match |
| 7 | Init-code word cost 2 × ceil(len/32), creation only, not in the floor (S`transactions.py#L743`, `vm/gas.py#L864-L883`) | `protocol.go#L51`; `intrinsic_gas.go#L126-L136`, `#L310-L316` | match |
| 8 | Access-list execution cost 2900 per address and 2000 per key (COLD − WARM; S`vm/gas.py#L147-L152`) | `protocol.go#L236-L237,#L243-L244`; `intrinsic_gas.go#L138-L164` | match |
| 9 | EIP-7976 floor tokens = len(data) × 4, at 16 gas/token (S`transactions.py#L775,#L782-L786`, `vm/gas.py#L145-L146`) | `protocol.go#L109,#L112`; `intrinsic_gas.go#L193-L204`, `#L248-L261` | match |
| 10 | EIP-7981 access-list floor tokens: 80/address and 128/key (S`transactions.py#L69-L83,#L757-L760`). Erigon computes 20 B × 4 and 32 B × 4 | `protocol.go#L110-L112`; `intrinsic_gas.go#L216-L232` | match |
| 11 | Access-list data surcharge (tokens × 16, i.e. 1280/2048) added to both the execution intrinsic and the floor (S`transactions.py#L762-L766,#L785,#L794`; EIP-7981 calls it a "flat surcharge", which gives the same number) | execution side `intrinsic_gas.go#L234-L246`; floor side `#L248-L261` | match |
| 12 | Floor anchor = TX_BASE + recipient (+ value) execution gas, excluding init code and auth (S`transactions.py#L779-L786`; EIP-2780 "Per-authorization and initcode charges are excluded") | `intrinsic_gas.go#L91-L92` (`FloorGasCost = ExecutionGas` is set before data, init code, access list and auth are added) | match |
| 13 | EIP-7702 per-auth intrinsic = EXECUTION_PER_AUTH_BASE_COST = 101×16 + 3000 + 3000 + 2×100 = 7816. Not in the floor (S`vm/gas.py#L157-L163`, `transactions.py#L768-L772`) | `protocol.go#L155,#L247`; `intrinsic_gas.go#L263-L281` | match |
| 14 | Checks: gas > TX_MAX_TOTAL_GAS_LIMIT rejected before the intrinsic checks. Intrinsic and floor each ≤ tx.gas and each ≤ TX_MAX_GAS_LIMIT 16,777,216 (S`transactions.py#L640-L641,#L671-L684`, `vm/gas.py#L153-L154`) | `txn_executor.go#L342-L356` (cap on `max(exec,floor)`, equivalent to the two spec checks) and `#L411-L413` (`gas < max(exec,floor)`). `SkipExecutionGasCap` and `CheckGas(false)` are set only by RPC (`rpc/...`, e.g. `rpc/transactions/call.go#L91`) | match. Order differs: Erigon checks the balance (`#L380-L409`) and the init-code size (`#L415-L419`) after the cap but around the intrinsic check, while the spec checks the init-code size first and the balance later. Both reject, so block validity is the same. Only the error name can differ (not a candidate) |
| 15 | Floor applied after the refund: refund = min(used/5, counter), then gas_used = max(after_refund, floor). Block execution gas = max(pre-refund execution, floor) (S`vm/gas.py#L1211-L1222`) | `txn_executor.go#L687-L692`, `#L700-L730` | match |
| 16 | Boundary floor == execution cost: no adjustment (spec `max`) | `txn_executor.go#L716` (strict `<`, so equal means no change) | match |
| 17 | Fork gating at the first Amsterdam block: all three EIPs switch on with the block's rules | `txn_executor.go#L1000-L1003` (`IsEIP7976/7981/2780: rules.IsAmsterdam`, rules from the block header) | match |
| 18 | Txpool uses the same formula as block validation | `txnprovider/txpool/pool.go#L1049-L1070` (`validateTx`, Amsterdam flags `#L1062-L1064`, `max(exec,floor)` `#L1067-L1070`, cap `#L1099-L1110`); `best` `#L848-L870`; shutter `txnprovider/shutter/pool.go#L352-L382`; all call `mdgas.CalcIntrinsicGas`/`IntrinsicGas` | match. Exception: the self-transfer input on reload (C1). Note: the pool's `isAmsterdam` uses wall-clock time (`pool.go#L1303-L1305`), so packing estimates around the fork boundary can use the pre-Amsterdam schedule. The assembler re-executes every transaction, so this only affects packing, not results |
| 19 | Protocol minimum-gas filters: Amsterdam minimum = TX_BASE 12000. Each tx's block execution gas is ≥ floor ≥ TX_BASE | `mdgas/intrinsic_gas.go#L286-L308` (`MinTxGas`, `MinTxBlockGas`); engine payload count check `execution/engineapi/engine_server.go#L433-L441` (`len(txs) > gasLimit/12000` means invalid); block assembler `execution/exec/block_assembler.go#L323` | match (the filter can never reject a spec-valid block) |
| 20 | Overflow: the spec uses unbounded `Uint` | `intrinsic_gas.go` uses `SafeMul`/`SafeAdd` throughout, and overflow means the tx is rejected (`txn_executor.go#L573-L576`) | equivalent in practice. Overflow needs more than about 2^60 data, which is impossible under the size limits |

Reth / evm2 cross-check of the same rules:
- **Base, recipient, value, self-transfer and creation:** V`crates/evm2/src/ethereum/mod.rs#L835-L910`, matching the spec.
- **Amsterdam constants:** V`crates/evm2/src/version/gas_params.rs#L604-L628` asserts:
  - `TxBaseStipend` 12_000;
  - `TxAccessListAddressCost` `2900 + 20 * 64`;
  - `TxAccessListStorageKeyCost` `2000 + 32 * 64`;
  - `TxAccessListFloorByteMultiplier` 4;
  - `TxFloorZeroByteMultiplier` 4.
- **Floor:** V`gas_params.rs#L497-L510` computes the base plus tokens × per-token cost, where tokens include access-list bytes × 4.
- **Pool check:** R`crates/transaction-pool/src/validate/eth.rs#L1591-L1596` uses `gas_limit < gas.intrinsic_gas || gas_limit < gas.floor_gas` and the cap on `max`.

Reth agrees with the spec, and with Erigon, on every row.

## 5. Known-check

Searches were run through `api.github.com/search/issues` (issues and PRs, open and closed) on 2026-10-27.

| Query | Relevant hits |
|---|---|
| `repo:erigontech/erigon 2780` (31) | #22053 implement EIP-2780 (closed PR); #22537 update EIP-2780 runtime gas (closed); #22916 merge transfer log into value cost (closed); #23052 EIP-2780 minTxGas floor in block assembler (closed); #25176 / #25372 reject payload with more txs than the gas limit allows (closed); issues #22027, #22379, #22803 (closed). None about the pool reload or the sender. |
| `repo:erigontech/erigon 7976` (19) | #20543 implement EIP-7976 (closed issue); #20613 implement EIP-7976 (closed PR) |
| `repo:erigontech/erigon 7981` (13) | #20544 (closed issue); #20631, #20671 implement EIP-7981 (closed PRs) |
| `repo:erigontech/erigon self-transfer` (71) | none about intrinsic gas in the pool |
| `repo:erigontech/erigon intrinsic gas txpool` (29) | #21942 "Harmonize/de-duplicate consensus admission checks: TxnExecutor.preCheck vs TxPool.validateTx" (open, generic); #25273 "txpool: the startup reload can block payload builds and does not restore the persisted pool" (open; covers lock contention and blockGasLimit=0 on reload, not the missing sender) |
| `repo:erigontech/erigon calldata floor` (27) | #24519 / #24520 RPC EIP-8037 caps (closed); nothing about intrinsic or floor mismatches |
| `repo:erigontech/erigon fromDB sender` (8) | #23511 load pool from DB under p.lock (closed); #24849 (closed); none about self-transfer |
| `repo:erigontech/erigon validateTx self-transfer sender` (2) | #22053, #23052 only |
| `repo:ethereum/execution-specs "Test Release Tracker" in:title` (32) | #3217 `tests@v21.0.0` (Glamsterdam Testnet, open), whose remaining item is "Final consume-engine/x & rlp sweep … all clients passing"; #3518 `glamsterdam-devnet@v8.1.4` (closed). No Erigon intrinsic or floor failure is listed. |
| `hive.ethpandaops.io/glamsterdam/listing.jsonl` | 999 runs. Erigon's latest runs (2026-10-26) all have `fails: 0` (see §2). |

**C1: not found.** The queries are listed above. Closest related: #25273 (open), which is a different cause on the same reload path. Hive and EEST exercise block or engine validation, not the pool's persisted reload, so 0 failures there is consistent with C1.

## 6. URLs read

- https://api.github.com/repos/erigontech/erigon/commits/main · https://api.github.com/repos/ethereum/execution-specs/commits/forks/bogota · https://api.github.com/repos/paradigmxyz/reth/commits/main
- Shallow clones: https://github.com/erigontech/erigon (main), https://github.com/ethereum/execution-specs (forks/bogota), https://github.com/paradigmxyz/reth (main)
- Spec: S`transactions.py`, S`vm/gas.py`, S`fork.py` (L525-L555)
- Erigon: E`execution/protocol/mdgas/intrinsic_gas.go`, E`execution/protocol/params/protocol.go`, E`execution/protocol/txn_executor.go`, E`execution/types/accounts/key_types.go`, E`execution/types/legacy_tx.go`, E`execution/types/dynamic_fee_tx.go`, E`txnprovider/txpool/pool.go`, E`txnprovider/txpool/pool_txn_parser.go`, E`txnprovider/shutter/pool.go`, E`execution/engineapi/engine_server.go`, E`execution/exec/block_assembler.go`
- Reth: R`Cargo.toml`, R`Cargo.lock`, R`crates/evm/evm/src/lib.rs`, R`crates/transaction-pool/src/validate/eth.rs`
- evm2: https://api.github.com/repos/alloy-rs/evm2/git/trees/4a53ec080dfbb904e98e3ca8f0abcc11ab8abae0?recursive=1 · https://raw.githubusercontent.com/alloy-rs/evm2/4a53ec080dfbb904e98e3ca8f0abcc11ab8abae0/crates/evm2/src/ethereum/mod.rs · https://raw.githubusercontent.com/alloy-rs/evm2/4a53ec080dfbb904e98e3ca8f0abcc11ab8abae0/crates/evm2/src/version/gas_params.rs
- EIPs: https://eips.ethereum.org/EIPS/eip-2780 (Last Call, deadline 2026-11-01) · https://eips.ethereum.org/EIPS/eip-7976 (Last Call) · https://eips.ethereum.org/EIPS/eip-7981 (Last Call; prose says "flat surcharge", the reference implementation adds it to both intrinsic and floor)
- Known-check: https://api.github.com/search/issues (queries in §5) · https://api.github.com/repos/erigontech/erigon/issues/25273 · https://api.github.com/repos/ethereum/execution-specs/issues/3217 · https://api.github.com/repos/ethereum/execution-specs/issues/3889 · https://hive.ethpandaops.io/glamsterdam/listing.jsonl
