| Client / reference | Branch | Commit | Spec commit |
|---|---|---|---|
| [Erigon](https://api.github.com/repos/erigontech/erigon/commits/main) | main | `f32a39d1e5f87a9ec3a9ce2858171c74acc3c242` | `7f2f4c9bf6d73b09ecb8b24eb14d08bf0cf0247b` |
| [Reth](https://api.github.com/repos/paradigmxyz/reth/commits/main) | main | `e0378eb691aa2770bb96fd4ecec2f4445a2a3e7c` | `7f2f4c9bf6d73b09ecb8b24eb14d08bf0cf0247b` |
| [evm2 (Reth dependency)](https://github.com/alloy-rs/evm2/tree/4a53ec080dfbb904e98e3ca8f0abcc11ab8abae0) | Reth-pinned revision; branch not supplied | `4a53ec080dfbb904e98e3ca8f0abcc11ab8abae0` | `7f2f4c9bf6d73b09ecb8b24eb14d08bf0cf0247b` |
| [Geth (Reth item's comparator)](https://github.com/ethereum/go-ethereum/tree/8817f99f92852f8ac53e596de6202dd9e3494065) | master | `8817f99f92852f8ac53e596de6202dd9e3494065` | `7f2f4c9bf6d73b09ecb8b24eb14d08bf0cf0247b` |
| [execution-specs](https://api.github.com/repos/ethereum/execution-specs/commits/forks/bogota) | forks/bogota | `7f2f4c9bf6d73b09ecb8b24eb14d08bf0cf0247b` | same |

# el-intrinsic — merge 2: Erigon and Reth

This report merges only the two supplied public-code reviews of EIP-2780, EIP-7976 and EIP-7981. It adds no findings or independent verification of upstream code. Both items describe Amsterdam as not live on mainnet at their review time. The broader goal's full fork EIP inventory and other client reviews are not supplied by these items.

Sources: `item-erigon` (original output `artifacts/goal/batches/el-intrinsic/items/erigon/result.md`, SHA-256 `addf893b59938eb8c010332ee0fedfb7b1709cba34f91c23a5b9ab4dc9d7824d`) and `item-reth` (original output `artifacts/goal/batches/el-intrinsic/items/reth/result.md`, SHA-256 `fd0e2352cf4a1b1add1684af67ce910874971caf357c4deb6bc382a1bf881603`). Pins and all review claims below are attributed to those inputs. Erigon's item dates its searches 2026-10-27. The source files were read from `.imd/reads/artifacts/`.

## Citation keys and numbering

Original file#line citations and quoted lines are preserved. **C1 means Erigon-1 throughout the copied material.** O1 remains a Reth observation, not a candidate. Original Erigon references to §4 mean the Erigon rule evidence below; §2 means its Hive results reproduced in the known-check summary.

- **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/`

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.

Reth item's unprefixed evm2 paths resolve under V`crates/evm2/src/`; its Reth paths resolve under R, its spec paths under S, and its Geth paths under [Geth at the comparator pin](https://github.com/ethereum/go-ethereum/blob/8817f99f92852f8ac53e596de6202dd9e3494065/). In Erigon's rule table, shortened `protocol.go`, `intrinsic_gas.go` and `txn_executor.go` refer to `execution/protocol/params/protocol.go`, `execution/protocol/mdgas/intrinsic_gas.go` and `execution/protocol/txn_executor.go`. Fragment-only references inherit their surrounding file. Some original references (for example Geth `#L141-…` and the legacy-handler row) are incomplete; no missing line ranges have been invented.

## Candidates

### From item-erigon

### Erigon-1 (source 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).

### From item-reth — no candidates; observation retained

**None.** I found no case where Reth (through evm2) gives a different intrinsic gas, floor gas, gas used or validity result from the spec at these pins. The disproof attempts are in the table below.

The only difference I found is in the txpool, not in consensus, and the spec does not define txpool behaviour, so I record it as an observation rather than a candidate:

- **Observation O1 (txpool, fork boundary).** The pool picks its gas rules from the header of the current tip: `crates/transaction-pool/src/validate/eth.rs#L961-L963` calls `transaction_validation_rules(&self.evm_config, new_tip_block)`, and the function is defined at `#L1478-L1492`. While the tip is the last Osaka block, the pool still applies Osaka intrinsic rules to transactions that will be included under Amsterdam.
  - A self-transfer or zero-value call sent with gas between 12,000/15,000 and 21,000 is rejected until the first Amsterdam block is imported.
  - In the other direction, an access-list transaction can pass the Osaka rules (2,400 per address) and then fail Amsterdam's 4,180 per address. Block building then skips it.
  - This has no effect on block validity.
  - Severity: cosmetic (mempool admission). Confidence: medium. I did not trace whether another path re-validates against the next timestamp.
  - Known check: related issue https://github.com/paradigmxyz/reth/issues/27619 ("txpool keeps transactions a fork made invalid, and peers get banned for them", closed).

## Cross-client rule table

E1–E20 refer to the numbered Erigon evidence rows below; R labels refer to the named Reth evidence rows. Geth is included only as the comparator present in item-reth. “Not checked” means the inputs provide no specific check supporting a spec-conformance verdict for that cell; it also applies to pool scheduling, for which item-reth says the spec defines no behaviour. It does not mean a failure. The Erigon item's blanket statement that Reth agrees on every row is preserved below, but cells without specific supporting Reth evidence are conservatively marked “not checked.” Geth's “pre-existing” entries are likewise not treated as completed checks.

| Rule and source evidence | Erigon | Reth | Geth (comparator only) |
|---|---|---|---|
| TX_BASE = 12000 (E1; R TX_BASE) | agrees with spec | agrees with spec | agrees with spec |
| Non-self recipient cold access = 3000 (E2; R Recipient) | agrees with spec | agrees with spec | agrees with spec |
| Value charge = 6000 only for non-self, non-creation calls (E3; R TX_VALUE_COST) | agrees with spec | agrees with spec | agrees with spec |
| Creation access = 12000, no extra value charge (E4; R Creation) | agrees with spec | agrees with spec | agrees with spec |
| Self-transfer recipient/value exemption, block path (E5; R Recipient) | agrees with spec | agrees with spec | agrees with spec |
| Self-transfer exemption, persisted txpool reload (Erigon-1 parts 2–4) | differs (see Erigon-1) | agrees with spec | not checked |
| Calldata execution cost: zero 4, nonzero 16 (E6; R Calldata) | agrees with spec | agrees with spec | not checked |
| Init-code word cost 2 × ceil(len/32), creation only (E7; R Init-code) | agrees with spec | agrees with spec | not checked |
| Access-list execution cost: address 2900, key 2000 (E8; R Access list) | agrees with spec | agrees with spec | agrees with spec |
| Floor calldata: 4 tokens per byte × 16 gas (E9; R EIP-7976) | agrees with spec | agrees with spec | agrees with spec |
| Access-list surcharge: address 80 tokens, key 128 (E10–11; R EIP-7981 intrinsic) | agrees with spec | agrees with spec | agrees with spec |
| Access-list surcharge included in floor (E10–11; R EIP-7981 floor) | agrees with spec | agrees with spec | agrees with spec |
| Floor base includes recipient/value; excludes init code and auth (E12–13; R Floor base/auth) | agrees with spec | agrees with spec | not checked |
| EIP-7702 auth intrinsic = 7816, excluded from floor (E13; R EIP-7702) | agrees with spec | agrees with spec | not checked |
| Gas limit covers intrinsic and floor separately (E14; R Gas limit) | agrees with spec | agrees with spec | not checked |
| Intrinsic and floor each bounded by TX_MAX_GAS_LIMIT (E14; R Intrinsic or floor) | agrees with spec | agrees with spec | not checked |
| TX_MAX_TOTAL_GAS_LIMIT rejection (E14) | agrees with spec | not checked | not checked |
| Validation check order: same validity despite error order (E14; R Check order) | agrees with spec | agrees with spec | not checked |
| Floor applied after refund (E15; R Floor applied) | agrees with spec | agrees with spec | not checked |
| Refund bounded by used/5; block execution gas uses pre-refund maximum (E15) | agrees with spec | not checked | not checked |
| Equality of floor and execution cost (E16; R Floor applied) | agrees with spec | agrees with spec | not checked |
| All three EIPs gated by first Amsterdam block rules (E17) | agrees with spec | not checked | not checked |
| Txpool/block shared formula and inputs (E18; R Txpool; Erigon-1) | differs (see Erigon-1) | agrees with spec | not checked |
| Txpool fork-boundary scheduling (E18 note; R O1; no spec pool rule supplied) | not checked | not checked | not checked |
| Protocol minimum gas and payload transaction-count filters (E19) | agrees with spec | not checked | not checked |
| Overflow handling under feasible transaction-size limits (E20) | agrees with spec | not checked | not checked |
| Legacy transactions count no access list (R Legacy) | not checked | agrees with spec | not checked |
| Worked 1000-byte calldata plus address/key floor (R worked check) | not checked | agrees with spec | not checked |

## Known-check summary

**Erigon-1 / source C1: not found in the searches reported by item-erigon.** Related open issues #21942 and #25273 do not establish that this specific missing-sender case is known. **Reth: no candidates; O1 has related closed issue #27619.** These are the source reviews' search outcomes, not proof that no duplicate exists.

Item-erigon's supporting Hive evidence (source §2, [Glamsterdam listing](https://hive.ethpandaops.io/glamsterdam/listing.jsonl)): every Erigon run of the `glamsterdam` group on 2026-10-26 had 0 failures; examples were `eels/consume-enginex` 83258/83258 (`3.9.0-dev-391edfe5`), `eels/consume-engine` 44225/44225, `eels/consume-rlp` 44161/44161, and `eels/execute, hive mode` 173/173. This is evidence for those runs, not a test of the pinned commit's persisted txpool reload. Reth's item did not fetch Hive or the release trackers; its cited 1626-failure count is an unattributed brief statement and is not verified here.

The complete known-check contents from both items follow. Issue/PR numbers in the Erigon query rows belong to the queried repository. Public source endpoints retained from that item for its key checks: [Erigon #25273](https://api.github.com/repos/erigontech/erigon/issues/25273), [execution-specs #3217](https://api.github.com/repos/ethereum/execution-specs/issues/3217), and [issue/PR search API](https://api.github.com/search/issues). The query strings below identify the searches; their counts are historical item observations.

### item-erigon — known-check (source C1 = Erigon-1)

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.

### item-reth — known check
- paradigmxyz/reth searches (api.github.com/search/issues, `repo:paradigmxyz/reth`):
  - `2780` found https://github.com/paradigmxyz/reth/pull/26536 (txpool EIP-2780 rules, closed), #26068 and #26875 (RPC estimate, closed).
  - `7976` and `7981` found only https://github.com/paradigmxyz/reth/pull/23732 (devnet4 integration, closed).
  - `intrinsic gas` found https://github.com/paradigmxyz/reth/pull/27587 (closed) and issue #27619 (closed, related to O1).
- I did not fetch the execution-specs "Test Release Tracker" issues or `hive.ethpandaops.io/glamsterdam/listing.jsonl` in this run.
- The brief says hive consume-engine had 1626 failures on 2026-10-26. I did not attribute any of them to these rules.


## Source rule evidence and disproof checks

### item-erigon — source §4

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.

### item-reth — rules checked and found correct

Spec references are in `src/ethereum/forks/amsterdam/`. "Spec tx" means `transactions.py#L689-L798` (`calculate_intrinsic_cost`).

| Rule | Spec | Reth / evm2 | Geth |
|---|---|---|---|
| TX_BASE 12000 | `vm/gas.py#L143` | `version/mod.rs#L869-L874` sets `TxBaseStipend: EIP2780_TX_BASE_COST`. The test at `version/gas_params.rs#L605` asserts 12_000. | `core/state_transition.go#L163-L184` uses `TxBaseCost2780` |
| Recipient: cold access 3000 for a call to another account, nothing for a self-transfer | spec tx `#L736-L747` | `ethereum/mod.rs#L892-L910` charges warm 100 + cold-additional 2900 = 3000 | `core/state_transition.go#L177-L184` |
| TX_VALUE_COST 6000, only for a non-creation, non-self call with value > 0 | spec tx `#L744-L747`; `vm/gas.py#L144` | `ethereum/mod.rs#L903-L907`; `interpreter/gas.rs#L132` | same function |
| Creation: CREATE_ACCESS = ACCOUNT_WRITE 9000 + COLD 3000 = 12000, and no value charge even when value is sent | `vm/gas.py#L77,#L85,#L90`; spec tx `#L741-L743` | `ethereum/mod.rs#L900-L902` (comment: "creates pay no value-based charge"); `interpreter/gas.rs#L67,#L81` | `CreateAccessAmsterdam`, `#L180-L181` |
| Init-code word cost 2 × ceil(len/32) | `vm/gas.py#L864-L883` | `ethereum/mod.rs#L861-L863`; `interpreter/gas.rs#L37` | (pre-existing) |
| Calldata cost: 4 per token, zero byte = 1 token, nonzero byte = 4 tokens | spec tx `#L732-L734,#L801-L810` | `version/gas_params.rs#L463-L471`; `ethereum/mod.rs#L845-L846` | (pre-existing) |
| EIP-7976 floor: every byte counts as 4 tokens, 16 gas per token | spec tx `#L775,#L782-L786`; `vm/gas.py#L146` | `TxFloorZeroByteMultiplier` = 4 and per-token = 16 (`version/mod.rs#L792-L799`; `interpreter/gas.rs#L36`) | `core/state_transition.go#L202-L210` |
| Floor base is the decomposed EIP-2780 base, not TX_BASE alone | spec tx `#L777-L786` | `ethereum/mod.rs#L874` uses `eip2780_base` | `core/state_transition.go#L243-L249` |
| Access list per entry: 2900 per address, 2000 per key (EIP-8038) | `vm/gas.py#L147-L152` (3000−100 and 2100−100) | `version/mod.rs` `TxAccessListAddressCost`/`StorageKeyCost` = 2900+20·64 and 2000+32·64. Test at `gas_params.rs#L624-L625`. | `#L127-L139`, `TxAccessListAddressGasAmsterdam` 2900 |
| EIP-7981 surcharge on the intrinsic side: 80 tokens per address and 128 per key, ×16 = 1280 / 2048 | spec tx `#L757-L766,#L789-L795` | Included in the per-item cost: 20·64 = 1280, 32·64 = 2048 (see the row above) | `#L141-…` adds it on top for Amsterdam |
| EIP-7981 surcharge on the floor side: same tokens | spec tx `#L782-L786` | `version/gas_params.rs#L497-L511`: (20·a + 32·k)·4 tokens ×16 | `core/state_transition.go#L211-L225` |
| EIP-7702 intrinsic per auth = 101·16 + 3000 + 3000 + 2·100 = 7816; not in the floor | `vm/gas.py#L157-L163`; spec tx `#L768-L772` | `ethereum/eip7702.rs#L68` adds it to intrinsic only; `gas_params.rs#L620` asserts 7_816; the pool path is `reth crates/evm/evm/src/lib.rs#L109-L112` | not re-checked |
| Gas limit must cover the execution intrinsic, and separately the floor | spec tx `#L674-L680` | `ethereum/mod.rs#L407-L426`. The Amsterdam `initial_state_gas` is 0 (`eip1559.rs#L61-L70`, `eip7702.rs#L69-L81`). | — |
| Intrinsic or floor above TX_MAX_GAS_LIMIT is rejected | spec tx `#L677-L684` | `ethereum/mod.rs#L347-L364` checks this only when `gas_limit > cap`. If `gas_limit ≤ cap`, the intrinsic and floor checks already bound both values, so the outcome is the same. | — |
| Check order (size, gas cap, fee, then intrinsic) | spec tx `#L630-L688` | `eip1559.rs#L44-L72`. The order differs only in which error is reported; validity is the same. | — |
| Floor applied after refund: gas used = max(spent − refund, floor); exactly equal counts as the floor | EIP-7623/7976 | `evm/tx.rs#L114-L118` (strict `>` then floor, so ties give the same value) | — |
| Txpool and block validation use one formula | — | The pool calls `evm2::ethereum::intrinsic_and_floor_gas` (`reth crates/evm/evm/src/lib.rs#L90-L114`; `crates/transaction-pool/src/validate/eth.rs#L1567-L1600`). See O1 for the fork boundary. | — |
| Legacy transactions: no access list counted | spec `has_access_list` | Legacy handler passes 0 | — |

Worked check: a zero-value call with 1000 bytes of `0x01` calldata, 1 address and 1 key.
- Spec floor = 15000 + (4000 + 80 + 128)·16.
- evm2 test `ethereum/mod.rs#L1099-L1125` asserts the same value, both for all-zero and for nonzero calldata.

I did not separately verify EIP-7981's prose ("flat surcharge") against the token form; the two give the same numbers (1280 / 2048).

