# Besu — EIP-7708 / EIP-8246 spec-conformance review (Amsterdam / Glamsterdam)

Public-page code review only; nothing was executed against a network or node.

## Pinned commits

| Repo | Branch | Commit (HEAD at review time) |
|---|---|---|
| `besu-eth/besu` | `main` | `aa06c4f8dfc461e483d46c6a1ac72b5bfef8d945` |
| `ethereum/execution-specs` | `forks/bogota` (repo default; no `master`) | `7f2f4c9bf6d73b09ecb8b24eb14d08bf0cf0247b` |
| `ethereum/go-ethereum` | `master` | `8817f99f92852f8ac53e596de6202dd9e3494065` |

Spec code reviewed under `src/ethereum/forks/amsterdam/` (`vm/__init__.py`, `vm/interpreter.py`,
`vm/instructions/system.py`, `state_tracker.py`, `fork.py`, `transactions.py`, `vm/gas.py`).
EIP text: https://eips.ethereum.org/EIPS/eip-7708, https://eips.ethereum.org/EIPS/eip-8246.
All file#line cites below are at the pinned Besu commit; paths are relative to the Besu repo root.

## Summary

Besu's Amsterdam wiring is spec-conformant on the consensus path. `EIP7708TransferLogEmitter`
(`evm/src/main/java/org/hyperledger/besu/evm/log/EIP7708TransferLogEmitter.java`) is wired only
into the Amsterdam `ProtocolSpecBuilder` (`ethereum/core/.../MainnetProtocolSpecs.java` ~L1117–1165);
pre-Amsterdam forks get `TransferLogEmitter.NOOP`. Emission sites cover exactly the spec's two
emission points (`process_call`-equivalent value transfers, and SELFDESTRUCT). No consensus-path
divergence was found. Two candidates exist in off-consensus code paths (tooling/test factories);
one is already a known open Besu issue.

## Candidates

### C1 — `EVMExecutor` / `evmtool run` still delete self-destructed accounts, burning the balance (EIP-8246)

1. **Spec** (https://eips.ethereum.org/EIPS/eip-8246): "During transaction finalization, all
   accounts marked for selfdestruction, instead of being deleted, are modified as follows:
   1. nonce is reset to 0, 2. balance is unchanged, 3. code is cleared, 4. all storage is cleared."
   Spec code: `fork.py#L1088-L1089` calls `clear_account_preserving_balance`
   (`state_tracker.py#L494-L511` — sets `nonce = 0`, `code_hash = EMPTY_CODE_HASH`, destroys
   storage, keeps the balance; `modify_state` then deletes it only if it is left empty per
   EIP-161).

2. **Client code**:
   - `evm/src/main/java/org/hyperledger/besu/evm/fluent/EVMExecutor.java#L175`:
     `initialMessageFrame.getSelfDestructs().forEach(worldUpdater::deleteAccount);`
   - `ethereum/evmtool/src/main/java/org/hyperledger/besu/evmtool/EvmToolCommand.java#L567`:
     `initialMessageFrame.getSelfDestructs().forEach(updater::deleteAccount);`

   Both unconditionally delete the account (balance included) at end of execution, bypassing the
   fork-aware `MainnetTransactionProcessor.settleSelfDestructs`
   (`ethereum/core/.../MainnetTransactionProcessor.java#L987-L1001`, called at `#L650`), which
   under `isSelfDestructBalancePreserved()` correctly resets nonce, clears code/storage and
   preserves the balance.

3. **Divergence condition**: Any transaction executed through `EVMExecutor.execute` (the fluent
   EVM runner used by Besu's own benchmarks/tests) or `evmtool run` under the Amsterdam spec in
   which a same-transaction-created contract SELFDESTRUCTs while retaining a nonzero balance.
   The reported post-state (state root, account existence, balance) is wrong: the account is
   deleted and the ETH burned, where the spec keeps a balance-only account with nonce 0, empty
   code and empty storage. Not a block-validation path — `AbstractBlockProcessor`/
   `eth_call`'s `TransactionSimulator` go through `MainnetTransactionProcessor`, which is correct.

4. **Geth**: agrees with spec — `core/state/statedb.go#L857-L895` (`finaliseAmsterdam`):
   self-destructed accounts are replaced by a fresh object carrying only the balance
   (`o := newObject(s, obj.address, obj.origin); o.setBalance(...)`), or deleted only when the
   balance is zero. Geth has no separate ad-hoc executor path that bypasses finalisation.

5. **Confidence: high.** Code is unconditional `deleteAccount`; the bug is already acknowledged.
   Disproof attempt: checked whether `EVMExecutor`/`EvmToolCommand` reach a fork-aware settle step
   later — they do not; `worldUpdater.commit()` follows the delete directly.

6. **Severity: cosmetic** — affects `evmtool run` / fluent-API post-state output only; the
   consensus block-processing path implements EIP-8246 correctly.

   **Known-check: KNOWN.** besu-eth/besu issue #11474 "EVMExecutor and evmtool run still delete
   self-destructed accounts under Amsterdam, burning the balance EIP-8246 preserves" — open.
   Fix PR #11526 "Settle self-destructs per EIP-8246 in EVMExecutor and evmtool run" — open.
   https://github.com/besu-eth/besu/issues/11474 , https://github.com/besu-eth/besu/pull/11526

### C2 — `MainnetEVMs.amsterdam(BigInteger, EvmConfiguration)` builds the "Amsterdam" EVM on `PragueGasCalculator` (EIP-8246 + EIP-7708)

1. **Spec**: EIP-8246 spec quote as in C1; also EIP-7708 (https://eips.ethereum.org/EIPS/eip-7708):
   a LOG3 with `TRANSFER_TOPIC` is issued for "any nonzero-value-transferring SELFDESTRUCT to a
   different account" — the spec defines no Burn log. Spec code: `vm/instructions/system.py`
   `selfdestruct` (`#L705-L772`) emits `emit_transfer_log` only when `beneficiary != originator`
   (`#L766-L767`) and never zeroes the balance.

2. **Client code**: `evm/src/main/java/org/hyperledger/besu/evm/MainnetEVMs.java#L1096-L1097`:
   `public static EVM amsterdam(final BigInteger chainId, final EvmConfiguration evmConfiguration)`
   `return amsterdam(new PragueGasCalculator(), chainId, evmConfiguration);`
   `PragueGasCalculator` does not override `isSelfDestructBalancePreserved()`, so the
   `GasCalculator` default `false` applies
   (`evm/.../gascalculator/GasCalculator.java#L433-L435`). Consequently, in
   `SelfDestructOperation.execute`
   (`evm/.../operation/SelfDestructOperation.java#L135-L149`):
   `emitBurnLog = willBeDestroyed && !isSelfDestructBalancePreserved()` becomes true, so a
   same-tx-created contract self-destructing to itself emits the non-spec Burn LOG2
   (`EIP7708TransferLogEmitter.BURN_TOPIC`, `#L72`, `emitSelfDestructLog` `#L129-L143`), and
   `originatorAccount.setBalance(Wei.ZERO)` at `#L148-L149` burns the balance. All Amsterdam
   state-gas/EIP-8037 metering is also absent (Prague calculator: e.g. cold access 2600 vs the
   Amsterdam spec's 3000 in `vm/gas.py#L77`). The same pattern exists for the `bogota`, `polis`,
   `bangkok`, `futureEips`, `experimentalEips` overloads (`MainnetEVMs.java#L1180, 1251, 1322,
   1393, 1468`), which all pin `new PragueGasCalculator()`.

3. **Divergence condition**: Anything obtaining an "Amsterdam" EVM through `EvmSpec.amsterdam(...)`
   (`evm/.../fluent/EvmSpec.java#L486-L488`) — i.e. the fluent `EVMExecutor` API used by Besu
   tests and the `evmtool` benchmark executors — gets pre-Amsterdam SELFDESTRUCT semantics plus
   a spurious Burn log that EIP-7708 does not define. The production protocol spec is NOT
   affected: `MainnetProtocolSpecs.java#L1118` explicitly sets
   `.gasCalculator(AmsterdamGasCalculator::new)`, and `AmsterdamGasCalculator` returns
   `isSelfDestructBalancePreserved() == true` (`AmsterdamGasCalculator.java#L380-L382`).

4. **Geth**: agrees with spec — all Amsterdam behaviour keys off a single `chainRules.IsAmsterdam`
   flag (`core/vm/instructions.go#L964-L967`, `core/state/statedb.go#L780-L781`,
   `core/evm.go#L146-L147`); there is no second "Amsterdam-with-wrong-gas-calculator" EVM object.

5. **Confidence: medium.** The code plainly constructs Amsterdam opcodes over the Prague gas
   calculator (`new PragueGasCalculator()` is verbatim in the source). Disproof attempt: searched
   for non-test callers of `EvmSpec.amsterdam`/`EVMExecutor`; found only tests, JMH benchmarks and
   `evmtool` benchmarks — `evmtool run`/`t8n` use `component.getProtocolSpec()` (the real
   protocol spec), and no JSON-RPC path uses the fluent API. Reachability in any consensus or
   production RPC path could not be confirmed, hence medium rather than high.

6. **Severity: cosmetic** — wrong semantics confined to a test/bench factory; combining with C1,
   `evmtool`/`EVMExecutor` runs under "Amsterdam" both burn the balance and emit a non-spec log.

   **Known-check: not found.** Queries: `repo:besu-eth/besu EvmSpec amsterdam`,
   `repo:besu-eth/besu "PragueGasCalculator" amsterdam`, `repo:besu-eth/besu evmtool 8246`
   (returned only #11474/#11526 and unrelated items). Related root cause partially covered by
   issue #11474 (open) / PR #11526 (open).

## Rules checked and found correct (Besu vs spec; Geth agrees)

| # | Rule | Spec site (execution-specs) | Besu site | Geth site |
|---|------|-----------------------------|-----------|-----------|
| 1 | LOG3 with `TRANSFER_TOPIC` for a nonzero-value top-level transaction to a different account, before other logs | `vm/interpreter.py#L418-L429` (top frame has `should_transfer_value=True`, `caller=origin`) | `MessageCallProcessor.transferValue` `#L127-L172` (`start` `#L92` → `transferValue` before code runs) | `core/evm.go#L142-L148` (`Transfer` inside `evm.Call`) |
| 2 | CALL with value to a different account logs at transfer time | `vm/interpreter.py#L418-L429`; `vm/instructions/system.py` `call`/`generic_call` | `MessageCallProcessor.java#L171` + `AbstractCallOperation.java` child-frame `.value(transferValue)` | `core/vm/evm.go#L329` |
| 3 | CALLCODE: value moves to self ⇒ never logs | `system.py` `callcode`: `to = evm.current_target`, `caller == current_target` ⇒ `caller != current_target` fails | `CallCodeOperation.address()/sender()` return `frame.getRecipientAddress()` ⇒ `from == to` filtered in `emitTransferLog` `#L121-L126` | `core/vm/evm.go#L365-L396` (`CallCode` never calls `Transfer`) |
| 4 | DELEGATECALL / STATICCALL carry no value ⇒ no log | `system.py` `delegatecall`/`staticcall` use `should_transfer_value=False` | `DelegateCallOperation.value()`/`StaticCallOperation.value()` return `Wei.ZERO` | `core/vm/evm.go` DelegateCall/StaticCall paths have no `Transfer` |
| 5 | CREATE/CREATE2 endowment logs creator→created before initcode logs; nothing on collision | `system.py` `generic_create` `#L101-L172` + `process_call` | `ContractCreationProcessor.start` `#L139-L159` (log inside the non-collision `else`, before `CODE_EXECUTING`) | `core/vm/evm.go#L627` (`Transfer` in `create`) |
| 6 | SELFDESTRUCT logs only when `beneficiary != originator` and balance nonzero | `system.py#L763-L771` + `emit_transfer_log` zero-guard `vm/__init__.py#L268` | `SelfDestructOperation.java#L137-L139` + emitter `greaterThan(Wei.ZERO)` `#L133` | `instructions.go` `opSelfdestruct6780` `#L964-L967` (`!balance.IsZero() && this != beneficiary`) |
| 7 | SELFDESTRUCT-to-self keeps balance under EIP-8246 (no burn) | `system.py#L763` unconditional `move_ether` (self = no-op) | `SelfDestructOperation.java#L128-L129` decrement+increment; `#L148-L149` `setBalance(ZERO)` gated off by `isSelfDestructBalancePreserved()` | `instructions.go` `#L948-L954` (`!IsAmsterdam` guard on the burn branch) |
| 8 | Deletion only when the contract was created in the same tx (EIP-6780 preserved) | `system.py#L770-L771` `originator in tx_state.created_accounts` | `SelfDestructOperation.java#L124-L126` `frame.wasCreatedInTransaction` with `eip6780Semantics=true` (`MainnetEVMs.java#L1150-L1151`) | `instructions.go` `#L942-L945` `evm.StateDB.IsNewContract` |
| 9 | Finalization clears nonce/code/storage, preserves balance; empty result deleted (EIP-161) | `fork.py#L1088-L1089` → `state_tracker.py#L494-L511`; `modify_state` destroys empties | `MainnetTransactionProcessor.settleSelfDestructs` `#L987-L1001` + `clearAccountsThatAreEmpty` `#L653` | `core/state/statedb.go#L872-L895` `finaliseAmsterdam` |
| 10 | Log encoding: `address=0xff…fffe`, topics `[TRANSFER_TOPIC, from, to]` zero-padded, `data` = 32-byte big-endian amount | `vm/__init__.py#L35-L37, #L271-L282` | `EIP7708TransferLogEmitter.java#L56-L97` (`createTransferLog`) | `core/types/log.go#L68-L79`, `params/protocol_params.go#L279` |
| 11 | No gas charged for the emitted log | `emit_transfer_log` has no gas interaction (`vm/__init__.py#L247-L282`) | `frame.addLog` — none | `db.AddLog` — none |
| 12 | Logs dropped on revert; child-frame logs merge only on success; failed tx ⇒ no logs | `vm/__init__.py` `incorporate_child` merges `logs` only `if not child_evm.error`; `process_top_level` drops all on error | `AbstractMessageProcessor.clearAccumulatedStateBesidesGasAndOutput` `#L145` `frame.clearLogs()` + `txValues.undoChanges`; `MainnetTransactionProcessor` `#L629-L630` `txSucceeded ? selfDestructs : Set.of()` | state-journal snapshot/revert (`evm.Call` `RevertToSnapshot`) |
| 13 | No transfer logs for: priority-fee payment to coinbase, base-fee burn, withdrawals, system calls | `fork.py#L1000-L1001` `create_ether` (coinbase tip, sender refund); `fork.py#L1113` withdrawals | `MainnetTransactionProcessor` `#L623` `coinbase.incrementBalance` (no emitter); `WithdrawalsProcessor` has no log calls | `AddBalance`/`SetBalance` paths — no `Transfer` |
| 14 | SELFDESTRUCT gas: 5000 base + 3000 cold + 9000 ACCOUNT_WRITE + NEW_ACCOUNT state gas (120×1530) only when beneficiary dead and balance ≠ 0 | `system.py#L723-L761`; `vm/gas.py#L250,77,85,54-55` | `SelfDestructOperation.java#L81-L115` + `AmsterdamGasCalculator.selfDestructOperationGasCost` `#L388-L397` (5000+9000, cold added separately) + `consumeStateGas(newAccountStateGas())` `#L108-L112` | equivalent Amsterdam gas table |
| 15 | Zero-value transfers emit no log | `vm/__init__.py#L268` `if transfer_amount == 0: return` | `emitTransferLog` `#L123` `value.greaterThan(Wei.ZERO)` guard | `core/evm.go#L146` `!amount.IsZero()` |
| 16 | Self-transfers (CALL to self, tx to self, selfdestruct-to-self) emit no log | `vm/interpreter.py#L425` `caller != current_target`; `system.py#L766` | `emitTransferLog` `!from.equals(to)` `#L123`; `emitSelfDestructLog` originator==beneficiary branch unreachable under Amsterdam | `sender != recipient` `#L146`; `this != beneficiary` `#L966` |
| 17 | Transfers to precompiles and EIP-7702 delegated EOAs log to the recipient account | `process_call` moves+logs before the precompile/delegation dispatch (`vm/interpreter.py#L418-L436`); log uses `current_target` (= `to`, not `code_address`) | `MessageCallProcessor.start` `#L92-L107`: `transferValue` before `executePrecompile`; `recipientAddress` is `to` | `evm.Call` `Transfer` before precompile dispatch |
| 18 | BAL (EIP-7928) consistency: touched accounts recorded for beneficiary/originator; net-zero self-transfers produce no balance-change entry | `state_tracker.py` `get_account` → `account_reads`; `update_builder_from_tx` diffs pre/post (`block_access_lists.py#L765-L820`) | `AbstractOperation.getAccount/getOrCreateAccount` auto-`addTouchedAccount` (`AbstractOperation.java#L101-L140`); `PartialBlockAccessView` diffs updates | `recordAccessListChanges` in `finaliseAmsterdam` |
| 19 | Receipts/bloom/log index include transfer logs | `fork.py#L1074-L1086` receipt = `tx_output.logs`, `logs_bloom(block_logs)` | receipt built from `initialFrame.getLogs()`; `TransactionReceipt` builds bloom from the log list | `types.Log` list → receipt bloom |
| 20 | Pre-8246 "Burn" log machinery is dead code under Amsterdam, not emitted | n/a (spec defines no Burn log) | `emitClosureLogs` gated by `!isSelfDestructBalancePreserved()` at `MainnetTransactionProcessor.java#L635-L637`; `emitBurnLog` false at `SelfDestructOperation.java#L135-L137`; both branches unreachable with `AmsterdamGasCalculator` | geth never defined a burn log |

## Rules with a deliberate deviation, checked

- **CALLCODE value**: the EIP-7708 text mentions "CALL" transfers "to a different account"; the
  reference spec implements CALLCODE as a self-transfer (`to = evm.current_target`), so no log is
  ever emitted for CALLCODE in all three implementations. Verified consistent — Besu's
  `CallCodeOperation` makes sender and recipient the executing account, so the
  `!from.equals(to)` filter drops it, matching spec and geth (`CallCode` has no `Transfer` call).
- **`created_accounts`/`creates` revert handling**: the spec never un-marks `created_accounts` on
  revert (`state_tracker.py#L547-L560` comment: "The marker is not removed even if the account
  creation reverts"); Besu's `TxValues.creates` is an `UndoSet` rolled back on revert
  (`TxValues.java#L58, #L158`). Not a spec divergence in effect: a reverted creation leaves no
  executable code at the address, so no later same-tx SELFDESTRUCT can observe the difference.
  Not reported as a candidate.
- **`emitSelfDestructLog` Burn branch** (`EIP7708TransferLogEmitter.java#L129-L143`): emits a
  `BURN_TOPIC` LOG2 when `originator == beneficiary` — reachable only when
  `isSelfDestructBalancePreserved()` is false (i.e. non-Amsterdam calcs, or the C2 factory). On
  the wired Amsterdam path it is unreachable. Covered by C2.
- **`frame.addRefund(beneficiary, originatorBalance)`** (`SelfDestructOperation.java#L154`):
  feeds only tracers (`DebugOperationTracer`, `CallTracer`); gas refund
  `getSelfDestructRefundAmount()` is 0 post-London. Not consensus-visible.

## Known-check per candidate

- **C1**: KNOWN — https://github.com/besu-eth/besu/issues/11474 (open), fix
  https://github.com/besu-eth/besu/pull/11526 (open).
- **C2**: not found — queries `EvmSpec amsterdam`, `"PragueGasCalculator" amsterdam`,
  `evmtool 8246` in besu-eth/besu returned no dedicated issue/PR.
- **Besu hive run for Glamsterdam** (`hive.ethpandaops.io/glamsterdam`, `besu_default`,
  `eels/consume-enginex` 2026-10-26, `v26.10-develop-fc46028`): 83189/83258 pass; the 69 failures
  are `eip4844`/`eip7918` BPO-transition tests, `eip8037` two-dimensional header tests, and
  `test_unrecoverable_signature` — none EIP-7708/EIP-8246. `eels/execute` 173/173 pass. No
  7708/8246 test failure observed for Besu.
- Other relevant Besu history: PR #9654 (closed, initial EIP-7708 impl), PR #9672 (closed,
  Burn-log feature now dead under EIP-8246), PR #10766 (closed, EIP-8246 impl), PR #11154
  (closed, eth_simulate transfer logs), PR #11272 (closed, eth_estimateGas value-transfer fix).
  execution-specs tracker: issue #3217 "Test Release Tracker: tests@v21.0.0 (Glamsterdam
  Testnet)" (open); #1875 (EIP-7708 implementation tracker, closed); #3520 (BAL/`SYSTEM_ADDRESS`
  test, closed — consistent with Besu, which never records `SYSTEM_ADDRESS` in the BAL since
  `addLog` touches no account).

## Sources 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://eips.ethereum.org/EIPS/eip-7708 ; https://eips.ethereum.org/EIPS/eip-8246
- execution-specs `forks/bogota` @7f2f4c9: `src/ethereum/forks/amsterdam/vm/__init__.py`,
  `vm/interpreter.py`, `vm/instructions/system.py`, `state_tracker.py`, `fork.py`,
  `vm/gas.py`, `block_access_lists.py`, `transactions.py`
- besu @aa06c4f8 (shallow clone): `evm/.../log/EIP7708TransferLogEmitter.java`, `TransferLogEmitter.java`,
  `operation/SelfDestructOperation.java`, `v2/operation/SelfDestructOperationV2.java`,
  `operation/AbstractCallOperation.java`, `CallOperation.java`, `CallCodeOperation.java`,
  `DelegateCallOperation.java`, `StaticCallOperation.java`, `AbstractCreateOperation.java`,
  `processor/MessageCallProcessor.java`, `ContractCreationProcessor.java`,
  `AbstractMessageProcessor.java`, `gascalculator/AmsterdamGasCalculator.java`,
  `Eip8037StateGasCostCalculator.java`, `MainnetEVMs.java`, `fluent/EVMExecutor.java`,
  `fluent/EvmSpec.java`, `tracing/EthTransferLogOperationTracer.java`, `frame/TxValues.java`;
  `ethereum/core/.../MainnetProtocolSpecs.java`, `MainnetTransactionProcessor.java`,
  `block/access/list/AccessLocationTracker.java`; `evmtool/.../EvmToolCommand.java`,
  `T8nExecutor.java`; `transaction/BlockSimulator.java`, `BlockStateCallSimulationResult.java`
- geth @8817f99 (shallow clone): `core/evm.go`, `core/vm/evm.go`, `core/vm/instructions.go`,
  `core/state/statedb.go`, `core/types/log.go`, `params/protocol_params.go`
- https://api.github.com/search/issues (besu-eth/besu: `7708`, `8246`, `transfer log`,
  `selfdestruct`, `EvmSpec amsterdam`, `"PragueGasCalculator" amsterdam`, `evmtool 8246`)
- https://hive.ethpandaops.io/glamsterdam/listing.jsonl + Besu run result
  `1793018005-fa5bfa67da513c8de132d620958d92dc.json`
- https://api.github.com/search/issues (ethereum/execution-specs: `7708`)
