Skip to content

implement the Amsterdam EVM version #2730

Open

Description

@gzliudan

Summary

XDPoSChain implements Osaka but has no Amsterdam fork of its own, while upstream go-ethereum carries the whole Amsterdam (glamsterdam) set and schedules it on Sepolia at AmsterdamTime = 1791294816 (2026-10-06 13:53:36 UTC). Upstream mainnet and Hoodi do not schedule Amsterdam.

Solidity 0.8.36 started exposing Amsterdam and 0.8.37 added block.slotnum / slotnum() (EIP-7843), behind --evm-version amsterdam --experimental. Amsterdam is not the compiler's default target yet (osaka still is), so no artifact needs it by default today, but any user who opts in cannot run their contract here, and the chain falls behind the moment solc flips the default.

The port is smaller than it looks because part of it has already landed: EIP-8024 in #2268, the fork switch in #2756, the EIP-7954 size limits in #2757, EIP-7708 in #2842 and EIP-8246 in #2834.

This issue is the index for the whole port, including the plan, the decisions and the definition of done, so the plan does not live in a separate comment.

Landed so far

Item PR PR status
Fork switch: AmsterdamBlock, IsAmsterdam, Rules.IsAmsterdam, the fork-order entry after Osaka, and the jump-table selection in core/vm. No network schedules it. #2756 merged
EIP-7954 size limits move from IsOsaka to IsAmsterdam and take the current upstream values, 65536 of code and 131072 of initcode, which makes Osaka equivalent to upstream Osaka again. #2757 merged
Documentation: docs/solidity.md (support status, 0.8.36 / 0.8.37, the EIP-7954 limits) and the gas price notes folded into ### block.basefee. #2758 merged
EIP-8024 DUPN / SWAPN / EXCHANGE, wired into the instruction table by #2756. #2268 merged
EIP-7708 transfer and burn system logs, closing #2767. #2842 merged
EIP-8246 SELFDESTRUCT burn removal, closing #2766. #2834 merged

Still open, not landed: #2909, the EIP-8246 fall-out in prestateTracer diff mode; the fix is PR #2939 (open).

Still unimplemented

One issue per EIP:

Porting plan

How the order was chosen

  • The fork activates atomically, so intermediate states are not consensus-relevant. The order is chosen by porting risk and reviewability, not by intermediate consensus validity: nothing here is scheduled on a network, and every stage ends in a state that is not consensus-critical on its own.
  • The unit of work stays one EIP per issue and one EIP per PR, so a stage can be stopped and the fork can still be scheduled for the subset that is ready.
  • Two items are not ordered by difficulty but by dependency: the gas ledger (EIP-8037/8038) underpins the transaction-level cost model, and the BAL (EIP-7928) underpins the two P2P EIPs.
  • EIP-7843 is deferred to the end of the queue because it is the only item blocked on a non-technical decision (D1), but Amsterdam must not activate without it.

Stages

Stage Work Issues PR Status
M0 prestateTracer fall-out from EIP-8246 #2909 #2939 open
M0b supply tracer end-to-end tests #2904 #2942 open
M1a EIP-7976, floor-data gas pricing #2762 #2943 open
M1b EIP-7981, access-list token charging #2763 - blocked by M1a
M2 EIP-2780, intrinsic half #2759 - -
M3 EIP-7843 SLOTNUM #2760 - blocked by D1
M4 EIP-8037 and EIP-8038, the gas ledger #2764, #2765 - -
M5 EIP-2780, runtime half #2759 - blocked by M4
M6 EIP-7928 block-level access lists #2761 - blocked by M4
M7 EIP-7997 factory #2768 - -
M8 EIP-8159, snap/1, EIP-8189 snap/2 #2829, #2830 - blocked by M6
M9 activation and scheduling - - blocked by all of the above, D7

Stage details

M0 - prestateTracer fall-out from EIP-8246

M0b - supply tracer end-to-end tests

M1a - EIP-7976, floor-data gas pricing

  • Calldata pricing moves from the EIP-7623 floor (10/40 gas per zero/non-zero byte) to 64/64. Upstream: TxCostFloorPerToken7976 = 16 in params/protocol_params.go:96, applied in core/state_transition.go:202-213.
  • The constant is half of it: upstream also re-weights the tokens, from one per zero byte and four per non-zero byte to four per byte. That re-weighting is what turns 10/40 into 64/64 — changing the constant alone would price a zero byte at 16 gas instead of 64.
  • Ported in PR feat(core,params,ethclient): implement EIP-7976 calldata floor pricing #34748, close #2762 #2943: FloorDataGas(rules, data) and the two production call sites in core/state_transition.go and core/txpool/validation.go. ADR-0005 pins the per-EIP porting rule (D8).
  • The IsPrague gate is unchanged: the fork-order table puts Amsterdam after Osaka and Prague, so Amsterdam implies Prague.
  • Fallout: the six all-forks test configs already carry AmsterdamBlock: 0, so the new pricing is live in tests while no network schedules it.

M1b - EIP-7981, access-list token charging

  • Access-list data is charged as tokens on top of the EIP-2930 access-list gas, at the same per-token rate. Upstream: core/state_transition.go:144.
  • Depends on M1a for TxCostFloorPerToken7976, and changes FloorDataGas's signature again by taking the access list.
  • Pitfall: the data floor is compared against the Osaka cap as max(intrinsicGas, floorDataGas) (core/state_transition.go:752); the access list must be charged once, not once through 7976's tokens and again through 7981.

M2 - EIP-2780, intrinsic half

  • Resource-based intrinsic gas: TxBaseCost2780 = 12000, TxValueCost2780 = 6000 (params/protocol_params.go:111-112).
  • The intrinsic half does not need the 2D ledger and can land before M4; the runtime half is M5.

M3 - EIP-7843 SLOTNUM (deferred, mandatory before activation)

  • Upstream: SLOTNUM is 0x4b (core/vm/opcodes.go:108), enabled by enable7843 (core/vm/eips.go:579); the header field is SlotNumber *uint64 with rlp:"optional" (core/types/block.go:107), so it is part of the block hash; the value enters the EVM through BlockContext.SlotNum; genesis parses it and defaults it to 0 when the fork is active (core/genesis.go:557-559).
  • This chain has no beacon chain and no engine API, so upstream's PayloadAttributes.SlotNumber -> miner path has no counterpart. The value has to come from data every masternode agrees on (D1).
  • Pitfall: the field is consensus-critical. Header.Hash hashes the struct, so the field enters the block hash by itself, but HashNoNonce / HashNoValidator in core/types/block.go and sigHash in consensus/XDPoS/engines/engine_v{1,2}/utils.go are explicit field lists, so the field only enters the signing hash if it is added there deliberately.

M4 - EIP-8037 and EIP-8038 (the gas ledger)

  • M4a, ledger skeleton: GasCosts / GasBudget in core/vm/gascosts.go, Contract.Gas retyped from uint64, a two-dimensional GasPool (CheckGasAmsterdam in core/gaspool.go:64, Available(amsterdam) in :110), tracing.Gas and a gas-change hook. With StateGas pinned to zero this is behaviour-preserving and the whole existing suite has to stay green.
  • M4b, EIP-8038 repricing: the Amsterdam price constants in params/protocol_params.go:114-125, the new per-opcode gas functions in core/vm/gas_table.go and core/vm/operations_acl.go, and the shared activator enable8037And8038 (core/vm/eips.go:590).
  • M4c, transaction layer: gasRemaining becomes a GasBudget, the runtime budget is split into execution and state reservoirs, and the per-transaction cap switches from the EIP-7825 MaxTxGas to MaxTxTotalGas (params/protocol_params.go:32, core/state_transition.go:592-595).
  • M4d, surrounding call sites: txpool (core/txpool/validation.go:96), miner, gas estimator, eth_call / eth_simulateV1, runtime helpers and t8ntool.
  • Pitfall: a wrong gas path shows up as a different state root, not as a crash, so coverage has to come from upstream's gas-table and state-transition vectors. The system-call state budget is the item most easily missed: this chain's EIP-2935 history-storage call still runs with a flat 30,000,000 gas limit and no state budget, a deviation already recorded in docs/upgrade.md.
  • Account creation is charged from the state ledger in core/vm/evm.go:530 (chargeAccountCreation), and code size is checked before state gas (:675).

M5 - EIP-2780, runtime half

  • chargeRuntimeGas, recipient handling for EIP-161 empty accounts, and EIP-7702 delegation charging. Depends on the M4 ledger.

M6 - EIP-7928 block-level access lists (largest consensus item)

  • The BAL is the per-block state access footprint: the hash goes into the header (core/types/block.go:104), it is collected during execution in StateDB (core/state/statedb.go:779, Finalise(rules params.Rules) *bal.ConstructionBlockAccessList), built in PreExecution / PostExecution (core/state_processor.go:168,190), stored in rawdb by (hash, number), and validated on import.
  • It also changes how EIP-7702 authorizations and self-destructs are recorded, and it is the payload EIP-8159 and EIP-8189 transport.
  • This chain's ApplySignTransaction does not go through the EVM, so its nonce and balance changes have to be recorded explicitly.
  • The BAL does not go into types.Body; it hangs off the block, like upstream.
  • Pitfall: the BAL is a consensus artefact and has to be reproducible byte for byte; the header hash and the data must agree or the chain diverges.

M7 - EIP-7997 deterministic deployment factory

  • Upstream puts the factory in the genesis allocation (core/genesis.go). Here the mainnet and testnet genesis are fixed constants, so the factory is injected at the fork instead, in the style of ProcessParentBlockHash.
  • Address 0x4e59b44847b379578588920cA78FbF26c0B4956C and its fixed code (params/protocol_params.go:272-273).
  • Injection is idempotent; a different pre-existing code at that address is an error; a manually deployed identical factory is accepted.

M8 - EIP-8159, snap/1, EIP-8189 (P2P, activation prerequisite)

  • M8a, EIP-8159: BAL exchange on the eth protocol. This chain runs its own protocol line (xdc/100, xdc/164, xdc/165), so the next version is xdc/166 and the message codes are allocated in this chain's code space, not upstream's (GetBlockAccessListsMsg = 0x12, BlockAccessListsMsg = 0x13, eth/protocols/eth/protocol.go:72-73; protocolLengths is 18 for eth/69 and eth/70 and 20 from eth/71, :49).
  • M8b, snap/1 base: this chain has no snap capability at all, so the capability, handshake and account/storage range serving have to be built first. Upstream's snap package is about 18k lines with tests; this is the single largest item in the plan.
  • M8c, EIP-8189 snap/2: BAL-based state sync on top of snap/1, upstream syncv2.go and bal_apply.go. This chain's DPoS snapshots and gap-epoch rules differ, so the syncer cannot be copied as is, and a pivot older than Amsterdam has to fall back to snap/1.
  • Validation: devp2p suites ported to xdc/166, plus a devnet sync from a pre-Amsterdam pivot.

M9 - Activation and scheduling

  • Amsterdam must not be scheduled before Osaka, and today every network leaves osakaBlock unset. The first deliverable of this stage is an Osaka decision, not an Amsterdam one.
  • The AmsterdamBlock value goes in its own PR together with fork-order and config-backfill work, never inside an EIP PR.
  • This chain forks by block number, so the switch is AmsterdamBlock (feat(core,params): add the amsterdam fork switch #2756) and needs no timestamp.

Hard dependencies

  • EIP-8038 cannot land without EIP-8037: they share GasCosts and Contract.Gas.ExecutionGas, and upstream enables both with a single activator.
  • EIP-7928 must follow EIP-8037: a block-level access list requires that an access which cannot afford its state gas never happens, so the charge order has to be fixed first.
  • The runtime half of EIP-2780 depends on EIP-8037; the intrinsic half does not.
  • EIP-8189 needs a snap capability this chain does not have, so snap/1 has to come first.
  • EIP-7843 cannot start before the slot value is decided (D1).

Decisions

# Decision Status
D1 What SLOTNUM returns on a chain with no beacon chain Open, blocks M3. Same class of problem as block.prevrandao; the rule has to be documented before activation.
D2 Whether this chain adopts BAL at all Resolved: yes. #2761 is in scope, and M6 plus M8 depend on it.
D3 What a "builder" is on a DPoS chain Resolved: no builder flow. EIP-8282 (#2769) is out of scope and needs an ADR.
D4 How the 7997 factory is introduced Resolved: fork-time injection, not a genesis allocation, so the genesis hash does not change.
D5 Whether this chain wants a snap capability Resolved: yes. The snap/1 base and snap/2 are in scope and are activation prerequisites.
D6 Whether this chain follows upstream Amsterdam as it stands Resolved in part: follow upstream except for documented deviations (no beacon chain or engine API, no blobs, 7997 injected at the fork, 8282 dropped).
D7 When Osaka, and then Amsterdam, are scheduled Open, M9. No network schedules Osaka today.
D8 Whether to port the Amsterdam EIPs one at a time or jump to upstream master's restructured shape Resolved: one EIP at a time, ported verbatim against the base the upstream commit was written on; see ADR-0005 (added in PR #2943). Reaching upstream master's shape is EIP-2780's job (M2/M5).

Branch and landing

  • Base is xdc/dev-upgrade. One branch per EIP, stacked in stage order and rebased on the previous merge; this umbrella stays the index.
  • One PR per EIP, not per stage: it keeps the issue mapping one-to-one, keeps every diff reviewable, and lets a stage be dropped without unwinding the rest.
  • Do not mix activation into an EIP PR.
  • Keep the upstream snapshot (sha plus the file:line list) in each PR body, so a later rebase can be checked against the snapshot the port was written from.

Validation

  • Repository rule: make all plus gofmt / goimports on the changed files; lint and tests targeted at the packages each commit touches, not the whole tree.
  • Three extra checks for this fork:
    1. Pre-fork immutability: replay a range of pre-Amsterdam blocks and compare state roots against a pre-port binary.
    2. Upstream vectors: opcode vectors for SLOTNUM, and the gas-table and state-transition vectors for the 2D gas model.
    3. Fork-boundary coverage: for every config that schedules both Osaka and Amsterdam, test the block at the activation boundary, not only configs where one of them is unset.

Sizing

Single developer: M4 roughly 3-4 weeks, M6 4-6 weeks, M8 6-8 weeks, everything else about 2 weeks. M8 is the largest variable.

Remaining work

  • Upstream test vectors: opcode vectors for SLOTNUM, and gas-table and state-transition vectors from the upstream fixtures.
  • The activation decision: every live network leaves osakaBlock unset, and Amsterdam must not be scheduled before Osaka. Activation waits for the EIPs above and for the SLOTNUM value question below.

Impact

  • A contract compiled with --evm-version amsterdam (the only way to use block.slotnum, which 0.8.37 introduced) cannot be deployed or executed here: the new target is neither activated nor implemented.
  • Low immediate severity, since Amsterdam is still an experimental, non-default target. It becomes a hard blocker on the release where solc switches its default target, exactly as Osaka did for 0.8.31.
  • No network schedules Amsterdam: every live network still leaves osakaBlock unset, and Amsterdam must not be scheduled before Osaka.

Upstream

go-ethereum master 2006f0836 (2026-10-23), rechecked 2026-10-29:

Open questions

  1. What SLOTNUM returns (D1). EIP-7843 returns the beacon chain slot and this chain has no beacon chain, so like block.prevrandao the value has to be defined deterministically and documented before activation. Tracked in implement EIP-7843 SLOTNUM and the slot number header field (amsterdam) #2760.
  2. When Osaka, and then Amsterdam, are scheduled (D7). No network schedules Osaka today, and the fork-order rule requires Osaka first.
  3. Whether EIP-8282 ever comes back. It is out of scope for the current plan (no beacon chain, no engine API) and needs an ADR; if the builder flow is wanted here later, the EIP and its system contracts have to be reconsidered from scratch.
  4. Fork switch form. Resolved: XDC forks are block-number based, so Amsterdam is AmsterdamBlock, added by feat(core,params): add the amsterdam fork switch #2756.

Out of scope

  • The blob-side parts of Amsterdam (BPO schedules, cell sidecars, BlobScheduleConfig): this chain has no blob transactions by design (see PR fix(core/vm,params): fail 0x0a calls instead of faking success #2666 and docs/solidity.md).
  • The beacon / engine API side of Amsterdam: EIP-8282, the ExecutionPayloadV4 family, fcuV4 / getPayloadV4 / newPayloadV4 / newPayloadV5, and the CL-provided target gas limit. This chain has no engine API at all (beacon/ carries only merkle, params and types).
  • EIP-7825 and EIP-7934: Osaka items, and Osaka is not scheduled either.
  • Scheduling Amsterdam on any live network: Osaka is unset everywhere and must come first.

Definition of done

  • Every EIP in the adopted set has its own merged PR, with the upstream reference in the body.
  • The fork is scheduled on the target network with AmsterdamBlock set, after Osaka.
  • docs/solidity.md and docs/upgrade.md state the Amsterdam support status, the deviations from upstream (if any) and the compiler-facing guidance.
  • The regression nets from M0 and M0b are green against the final tree.

Activity

  1. self-assigned this
    on Oct 29, 2026
  2. changed the title [-]core/vm,core/types,params: implement the Amsterdam EVM version[/-] [+]implement the Amsterdam EVM version[/+] on Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions