# Besu vs Amsterdam spec: EIP-7843 SLOTNUM, EIP-7954 max contract size, EIP-8024 DUPN/SWAPN/EXCHANGE

Item `besu` of batch `el-evm`. This review only read public source code. Nothing was run against any network or node.

## Bottom line

**No consensus candidates.** For EIP-7843, EIP-7954 and EIP-8024, every rule checked in Besu's Amsterdam code gives the same gas, state, block validity and encoding as execution-specs. The full list of rules checked is in the table below.

- Besu passes all of the EELS Glamsterdam tests for these three EIPs on hive.
- One item sits in code that already runs on mainnet. Under the brief's rule it is reported only as **Besu, EIP-7843: affects live code, details withheld**.
- Two cosmetic, non-consensus observations are listed separately (O-1, O-2). Neither is a candidate.

## Pins

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

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

Path abbreviations used below (all files are at the pins above):

- `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 uses some long path prefixes:

- `evm/…/evm/` = `evm/src/main/java/org/hyperledger/besu/evm/`
- `core/…/ethereum/` = `ethereum/core/src/main/java/org/hyperledger/besu/ethereum/`
- `api/…/internal/` = `ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/`

### Spec versions used

- **EIP-7843, EIP-7954, EIP-8024:** all three pages on eips.ethereum.org show status **Last Call**, with a Last Call deadline of 2026-11-01.
- **EIP-8024 differs from the brief's wording.** The brief describes the immediate as "n+1; n+1; high nibble+1, low nibble+1" and says jumpdest analysis treats it as data. The current EIP-8024 and EELS say something else:
  - DUPN and SWAPN use `decode_single`: `(x + 145) % 256`, and immediates 91–127 are invalid.
  - EXCHANGE uses `decode_pair`: `k = x ^ 143; q, r = divmod(k, 16)`, and immediates 82–127 are invalid.
  - The EIP states: "`JUMPDEST` analysis is unchanged." The example `600456e65b` "executes successfully", meaning a `0x5b` used as the immediate is still a valid jump target.
- **This review compares against the current EIP and EELS text, not the brief's older summary.** Besu, Geth and EELS all follow the current text, so the brief's summary is not used as a reference.

## Candidates

**None that meet the bar.** For each of the six parts the brief asks for, no place was found where Besu's gas, state, block validity or encoding differs from the spec at the pin for these three EIPs.

The strongest suspicions and how each was disproved:

1. **SLOTNUM or the header field truncating values ≥ 2^63, since Java `long` is signed.** Disproved:
   - RLP decode reads up to 8 bytes as unsigned bits: `B/ethereum/rlp/src/main/java/org/hyperledger/besu/ethereum/rlp/BytesValueRLPInput.java#L322-L337`.
   - The opcode pushes the 8 bytes unsigned through `Words.longBytes`: `B/evm/…/evm/internal/Words.java#L243-L253`.
   - Engine API parsing uses `UInt64.fromHexString(...).toBytes().toLong()`, in `B/api/…/internal/parameters/PayloadAttributesV4.java#L49-L58` and in `QuantityJson.UnsignedLongDeserializer` (`B/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/json/QuantityJson.java#L76-L93`).
   - Serialization uses `Long.toHexString`, which is unsigned (`QuantityJson.java#L35-L37`).
   - So the full uint64 range round-trips, the same as the spec's `U64`.
2. **EXCHANGE validity boundary.** The comment at `ExchangeOperation.java#L80` says "invalid immediate range (80-127)", which is wrong. Disproved: the table actually built marks only 82–127 as invalid (`B/evm/…/evm/operation/Eip8024Decoder.java#L119-L134`), the same as `decode_pair` in `S/vm/stack.py#L79-L84`. Only the comment is wrong.
3. **Gas charged after state mutation in the inlined fast path.** In `B/evm/…/evm/EVM.java#L464-L475` and `#L486-L494`, gas is deducted after DUPN, SWAPN or EXCHANGE has run. Disproved: when gas is insufficient, the frame ends in `EXCEPTIONAL_HALT` with `INSUFFICIENT_GAS`. That halt reverts the frame and consumes all of its gas, which is the same observable result as the spec's `charge_gas` raising `OutOfGasError` first.
4. **Code deposit check order.** Besu checks the size first (`MaxCodeSizeRule`, then `PrefixCodeRule`), while the spec checks the `0xEF` prefix first (`S/vm/interpreter.py#L363-L368`). Disproved: both failures are the same exceptional halt, and in Besu both fire before any deposit or state gas is charged (`B/evm/…/evm/processor/ContractCreationProcessor.java#L176-L192`), so the result is the same.

### Live-code item (withheld)

- **Besu, EIP-7843: affects live code, details withheld.**
- Confidence: low. Practical reachability is doubtful. No file, line or trigger is given, per the brief.

## Rules checked and found consistent with the spec

| # | Rule (spec ref) | Besu at pin | Geth at pin | Result |
|---|---|---|---|---|
| 1 | SLOTNUM is opcode 0x4B, gas `OPCODE_SLOTNUM = BASE` (2), and pushes `U256(block_env.slot_number)` (`S/vm/instructions/block.py#L264-L294`, `S/vm/gas.py#L221`, `S/vm/instructions/__init__.py#L102`) | `B/evm/…/evm/operation/SlotNumOperation.java#L29-L39` (base tier); `B/evm/…/evm/gascalculator/FrontierGasCalculator.java#L68` (`BASE_TIER_GAS_COST = 2L`) | `G/core/vm/eips.go#L573-L585` (GasQuickStep = 2) | consistent |
| 2 | SLOTNUM is invalid before Amsterdam (only the Amsterdam opcode table maps it) | registered only in `registerAmsterdamOperations`: `B/evm/…/evm/MainnetEVMs.java#L1142-L1160`. Osaka and BPO forks use the Osaka EVM, so 0x4B falls through to `InvalidOperation` via the default branch of the switch at `EVM.java#L476-L479` | `G/core/vm/jump_table.go#L106-L112` | consistent |
| 3 | SLOTNUM stack overflow at 1024 (`push` raises StackOverflowError, `S/vm/stack.py#L128-L129`) | `pushStackItem` throws `OverflowException`, which is caught in `AbstractFixedCostOperation.execute`; V2 uses `stackHasSpaceV2(1)` at `B/evm/…/evm/v2/operation/SlotNumOperationV2.java#L36` | maxStack(0,1) | consistent |
| 4 | Header field `slot_number: U64` comes last, after `block_access_list_hash` (`S/blocks.py#L253-L269`); the block env takes it from the header (`S/fork.py#L316`) | RLP encode writes it last, after the BAL hash (`B/core/…/ethereum/core/BlockHeader.java#L311-L316`); decode reads it last (`#L357-L360`); the EVM value comes from `ProcessableBlockHeader.getSlotNumber` (`B/core/…/ethereum/core/ProcessableBlockHeader.java#L192-L194`) | `G/core/types/block.go#L106-L107`; `G/core/evm.go#L66-L82` | consistent |
| 5 | An Amsterdam header must carry slot_number. In the spec, RLP decoding into `Header` fails without the field | `SlotNumberPresentValidationRule` (`B/core/…/ethereum/mainnet/headervalidationrules/SlotNumberPresentValidationRule.java#L33-L40`), wired at `MainnetBlockHeaderValidator.java#L153-L159` and `MainnetProtocolSpecs.java#L1190-L1196` | `G/consensus/beacon/consensus.go#L274-L280` | consistent |
| 6 | Engine API: `ExecutionPayloadV4.slotNumber` goes into the header, and `PayloadAttributesV4.slotNumber` must be present (EIP-7843 text) | newPayloadV5 copies it into the header at `B/api/…/internal/methods/engine/EngineNewPayloadV5.java#L67-L73` and rejects it if missing at `#L88-L91`. FCUv4 rejects a missing value and passes it to block building at `B/api/…/internal/methods/engine/EngineForkchoiceUpdatedV4.java#L165-L183`. The value enters the payload id (PR #10242) | `G/eth/catalyst/api.go#L230`, `#L398` | consistent. The block hash covers the slot, so a mismatch between payload and header is caught by the block-hash check |
| 7 | First Amsterdam block: the spec gives no special value, only what the CL supplies | the header builder supplies 0 only when no slot is given; this is a non-engine path (`B/core/…/ethereum/core/BlockHeaderBuilder.java#L275-L279`). Amsterdam-at-genesis parses `slotNumber` from the genesis file (`B/core/…/ethereum/chain/GenesisState.java#L230-L231`) | n/a | consistent (no spec rule to violate) |
| 8 | `MAX_CODE_SIZE = 0x10000`, `MAX_INIT_CODE_SIZE = 2 * MAX_CODE_SIZE` (`S/vm/interpreter.py#L80-L81`; EIP-7954) | `B/evm/…/evm/EvmSpecVersion.java#L72-L76` and `#L107-L111` (0x10000 / 0x20000 for AMSTERDAM and later; Osaka keeps 0x6000 / 0xC000) | `G/params/protocol_params.go#L163-L164` | consistent |
| 9 | Code deposit fails only if `len(code) > MAX_CODE_SIZE`, so exactly 0x10000 is allowed (`S/vm/interpreter.py#L367-L368`) | `if (contractCodeSize <= maxCodeSize) return Optional.empty();` (`B/evm/…/evm/contractvalidation/MaxCodeSizeRule.java#L46-L58`). The limit comes from `MaxCodeSizeRule.from(evm)` of the Amsterdam EVM (`B/core/…/ethereum/mainnet/MainnetProtocolSpecs.java#L1124-L1132`; `EVM.java#L209-L210`) | `G/core/vm/common.go#L43-L54` | consistent |
| 10 | CREATE and CREATE2 halt when `memory_size > MAX_INIT_CODE_SIZE` after charging gas (`S/vm/instructions/system.py#L221-L222`, `#L285-L286`) | `if (getInputSize(frame) > evm.getMaxInitcodeSize())` halts with CODE_TOO_LARGE (`B/evm/…/evm/operation/AbstractCreateOperation.java#L88-L95`); V2 at `B/evm/…/evm/v2/operation/AbstractCreateOperationV2.java#L81-L90` | `G/core/vm/common.go#L29-L40`, used at `G/core/vm/gas_table.go#L336`, `#L359` | consistent (exceptional halt either way) |
| 11 | A create tx with `len(tx.data) > MAX_INIT_CODE_SIZE` is invalid (`S/transactions.py#L637-L638`) | `MainnetTransactionValidator.java#L138-L143` (`> maxInitcodeSize`). The value is `evm.getMaxInitcodeSize()` of the fork's EVM, through the inherited Osaka factory lambda (`MainnetProtocolSpecs.java#L1025-L1038`; `ProtocolSpecBuilder.java#L377-L387`) | `G/core/state_transition.go#L684` | consistent |
| 12 | Init-code word gas is unchanged: 2 per word (EIP-3860, EIP-7954 "Only the size limit is changed") | `B/evm/…/evm/gascalculator/ShanghaiGasCalculator.java#L44-L45`; CREATE cost at `B/evm/…/evm/operation/CreateOperation.java#L44-L52` | unchanged | consistent |
| 13 | EIP-7702 delegation: EIP-7954 says nothing about delegation, and the spec applies no size rule to the 23-byte designator | no `MaxCodeSizeRule` on the delegation path; the rule is used only in contract-creation processors (grep at pin) | n/a | consistent |
| 14 | DUPN, SWAPN, EXCHANGE are opcodes 0xE6, 0xE7, 0xE8 with gas VERY_LOW (3) (`S/vm/instructions/__init__.py#L193-L195`, `S/vm/gas.py#L227-L229`) | `DupNOperation.java#L36`, `#L59`; `SwapNOperation.java#L37`, `#L56`; `ExchangeOperation.java#L37`, `#L56`; `FrontierGasCalculator.java#L64` (3) | `G/core/vm/eips.go#L340-L358` (GasFastestStep = 3) | consistent |
| 15 | `decode_single`: valid for 0–90 and 128–255, returns `(x+145)%256` (`S/vm/stack.py#L25-L53`) | `B/evm/…/evm/operation/Eip8024Decoder.java#L95-L105` | `G/core/vm/instructions.go#L979-L985`, `#L1018-L1023` | consistent |
| 16 | `decode_pair`: valid for 0–81 and 128–255, `k=x^143`, upper triangle `(q+1, r+1)`, else `(r+1, 29-q)` (`S/vm/stack.py#L56-L91`) | `Eip8024Decoder.java#L119-L151` | `G/core/vm/instructions.go#L992-L1006`, `#L1077-L1082` | consistent |
| 17 | DUPN underflow if `n > len(stack)`; it pushes `stack[-n]`, and the push overflows at 1024 (`S/vm/instructions/stack.py#L213-L242`) | `frame.pushStackItem(frame.getStackItem(n - 1))` with Underflow and Overflow caught (`B/evm/…/evm/operation/DupNOperation.java#L75-L97`); V2 at `DupNOperationV2.java#L68-L82` | `G/core/vm/instructions.go#L1008-L1034` | consistent |
| 18 | SWAPN underflow if `n+1 > len(stack)`; swaps the top with `stack[-(n+1)]` (`S/vm/instructions/stack.py#L245-L278`) | `SwapNOperation.java#L72-L95` (index n is the (n+1)th item); V2 at `SwapNOperationV2.java#L67-L79` | `G/core/vm/instructions.go#L1036-L1064` | consistent |
| 19 | EXCHANGE underflow if `max(n,m)+1 > len(stack)`; swaps positions n+1 and m+1 (`S/vm/instructions/stack.py#L281-L315`) | `ExchangeOperation.java#L72-L100`; V2 at `ExchangeOperationV2.java#L67-L81` | `G/core/vm/instructions.go#L1066-L1097` | consistent |
| 20 | The immediate past the end of code reads as 0 (`buffer_read` zero-pads), and pc advances by 2 | `(pc + 1 >= code.length) ? 0 : code[pc + 1] & 0xFF` and `OperationResult(3, null, 2)` in all three ops, e.g. `DupNOperation.java#L39`, `#L78` | `G/core/vm/instructions.go#L1012-L1016` | consistent |
| 21 | Invalid immediate is an exceptional halt | `INVALID_IMMEDIATE` = `INVALID_OPERATION` halt (`DupNOperation.java#L42-L43`, `#L81-L83`) | `ErrInvalidOpCode` | consistent |
| 22 | Jumpdest analysis is unchanged and skips only PUSH data (`S/vm/runtime.py#L22-L70`; EIP-8024) | `B/evm/…/evm/Code.java#L274-L292` (walks PUSH opcodes only) | unchanged | consistent |
| 23 | DUPN, SWAPN, EXCHANGE are invalid before Amsterdam | inline switch is gated on `enableAmsterdam = AMSTERDAM.ordinal() <= evmSpecVersion.ordinal()` (`EVM.java#L186`, `#L464-L475`; V2 at `#L647-L658`); registry at `MainnetEVMs.java#L1156-L1159` | `G/core/vm/jump_table.go#L106-L112` | consistent |

## Cosmetic observations (not candidates; no effect on gas, state, validity or encoding)

- **O-1: the debug disassembler treats an EIP-8024 immediate as data.**
  - Where: `printInstruction` in `B/evm/…/evm/Code.java#L380-L401`, which uses `OpcodeInfo.validOpcode("DUPN", 0xe6, 0, 1, 2)` and the matching SWAPN and EXCHANGE entries (`B/evm/…/evm/code/OpcodeInfo.java#L309-L311`).
  - Effect: the immediate byte is printed as data and skipped. The EIP says: "Disassemblers must decode such `code[i]` as `INVALID` (or `INVALID_DUPN`, etc.) and `code[i + 1]` as `JUMPDEST`" when the immediate is invalid, such as `0x5b`.
  - Separately, 0x4B is listed as `unallocatedOpcode` (`OpcodeInfo.java#L154`), so SLOTNUM is not named in that listing.
  - Severity: cosmetic, debug output only.
- **O-2: a stale comment.** `ExchangeOperation.java#L80` says "invalid immediate range (80-127)", but the code rejects 82–127. Comment only.

## Known-check

Already indexed: none. GitHub search API, `repo:besu-eth/besu` (issues and PRs, open and closed):

| Query | Relevant hits (state) |
|---|---|
| `7843` | PR #9632 "EIP 7843: SLOTNUM op code" (closed, merged); IS #9541 (closed); PR #11084 "Enforce EIP-7843 slotNumber presence in Amsterdam header validation" (closed); PR #11038 "Fix genesis slotNumber parsing and require the header field from Amsterdam" (closed); PR #11143 "Glamsterdam: fixed payload validation edge cases" (closed, merged); IS #9661 FCUv4 (closed); PR #9718 (closed) |
| `SLOTNUM` | adds PR #10242 "Include slotNumber in payloadIdentifier generation" (merged); PR #11524 "EVM v2 MEGA PR" (merged) |
| `slotNumber` | adds PR #11394 "Fix eth_simulateV1 block fields after Amsterdam" (merged); PR #11176 "Treat engine API uint64 timestamps as unsigned" (merged); PR #11389 (merged) |
| `7954` | PR #10742 "EIP-7954: raise Amsterdam max code/initcode size to 64KiB/128KiB" (closed, merged); PR #9823 "add eip 7954 to Amsterdam" (merged); IS #9816 (closed) |
| `max code size Amsterdam`, `code size` | nothing on a 7954 boundary bug |
| `8024` | PR #9631 "Add EIP 8024 op codes" (merged); PR #9851 "bal-devnet-3: Update EIP-8024: switch to new decoding algo" (merged); IS #9827, IS #9542 (closed); PR #11483 "Add DUPN, SWAPN and EXCHANGE to EVM v2" (closed) |
| `DUPN` / `SWAPN` / `EXCHANGE` | IS #11246 "[JMH benchmarks] … stack underflow in DupN" (open; a benchmark harness issue, not consensus) |
| `slot number before Amsterdam` | nothing matching the withheld item |

Other sources checked:

- **execution-specs Test Release Tracker issues.** #3217 "`tests@v21.0.0` (Glamsterdam Testnet)" is open, last updated 2026-10-08. #3518 "`glamsterdam-devnet@v8.1.4`" is closed. Neither body nor comments name a Besu issue for EIP-7843, EIP-7954 or EIP-8024.
- **hive.ethpandaops.io/glamsterdam/listing.jsonl.** I pulled the latest Besu runs:
  - `eels/consume-engine` (besu v26.10-develop-bdb9e5c, 2026-10-25, file `1793007262-bcaea780….json`): 44216 passed, 9 failed.
  - `eels/consume-enginex` (besu develop-fc46028, 2026-10-26, file `1793018005-fa5bfa67….json`): 83189 passed, 69 failed.
  - In both runs, Besu passed every test under `tests/amsterdam/eip7843_slotnum`, `eip7954_increase_max_contract_size` and `eip8024_dupn_swapn_exchange`: 24/0, 85/0 and 294/0.
  - The failures were all in other areas: `test_unrecoverable_signature`, EIP-8037 `test_reject_incorrect_two_dimensional_header`, and BPO2→3 / BPO3→4 blob-transition tests.
  - Caveat: the hive builds (bdb9e5c, fc46028) are not the pinned commit aa06c4f8.

Status per item: no candidate was filed, so there is nothing to mark known. The withheld live-code item: **not found** under the queries above.

## 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
- Shallow clones: https://github.com/besu-eth/besu, https://github.com/ethereum/execution-specs (branch forks/bogota), https://github.com/ethereum/go-ethereum
- https://eips.ethereum.org/EIPS/eip-7843
- https://eips.ethereum.org/EIPS/eip-7954
- https://eips.ethereum.org/EIPS/eip-8024
- https://api.github.com/search/issues (queries listed above)
- https://api.github.com/repos/ethereum/execution-specs/issues/3217 (and its comments)
- https://api.github.com/repos/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

Not done: Besu's raw HTML pages were not used (per the brief). Other clients (Nethermind, Erigon, Reth) were not compared; they are outside this item.
