Repository navigation
implement the Amsterdam EVM version #2730
Description
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:
- EIP-2780, resource-based intrinsic transaction gas - implement EIP-2780 resource-based intrinsic transaction gas (amsterdam) #2759
- EIP-7843, SLOTNUM and the slot number header field - implement EIP-7843 SLOTNUM and the slot number header field (amsterdam) #2760
- EIP-7928, block-level access lists - implement EIP-7928 block-level access lists (amsterdam) #2761
- EIP-7976, calldata pricing - implement EIP-7976 calldata pricing (amsterdam) #2762 (PR feat(core,params,ethclient): implement EIP-7976 calldata floor pricing #34748, close #2762 #2943, open)
- EIP-7981, access-list token charging - implement EIP-7981 access-list token charging (amsterdam) #2763
- EIP-8037, multidimensional state-gas metering - implement EIP-8037 multidimensional state-gas metering (amsterdam) #2764
- EIP-8038, state-access repricing - implement EIP-8038 state-access repricing (amsterdam) #2765
- EIP-7997, deterministic deployment factory - deploy the EIP-7997 deterministic deployment factory (amsterdam) #2768
- EIP-8282, builder execution requests - implement EIP-8282 builder execution requests (amsterdam) #2769 (decided out of scope, see below)
- EIP-8159, BAL exchange messages on the eth protocol - implement EIP-8159 BAL exchange messages on the eth protocol (amsterdam) #2829
- EIP-8189, snap/2 BAL-based state sync - implement EIP-8189 snap/2 BAL-based state sync (amsterdam) #2830
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
- eth/tracers: prestateTracer diff mode omits surviving self-destructed accounts under EIP-8246 #2909:
prestateTracerwithdiffMode: truedrops accounts that surviveSELFDESTRUCTunder EIP-8246, so an account created and self-destructed in the same transaction ends up in neitherprenorpost. - The fix is PR fix(eth/tracers): report surviving self-destructed accounts in prestate diff mode #33050, close #2909 #2939, porting upstream eth/tracers: fix prestateTracer for EIP-6780 SELFDESTRUCT ethereum/go-ethereum#33050: gate the EIP-6780 survivor handling on the fork and check
!Existinstead of the deletion flag.
M0b - supply tracer end-to-end tests
- eth/tracers: port the upstream supply tracer end-to-end tests #2904 ports upstream Supply delta live tracer ethereum/go-ethereum#29347; PR test(eth/tracers): port the supply tracer end-to-end tests #29347, close #2904 #2942.
- Not part of Amsterdam, but it is the regression net for M4 (gas ledger) and M6 (
StateDB.Finalisesignature): both change live tracer output. - The test chain has no merge, so the block reward is paid every block and the expectations differ from upstream. Legacy SELFDESTRUCT is unreachable here (
enable6780replaces the shared slot), so the fixtures are 6780-only.
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 = 16inparams/protocol_params.go:96, applied incore/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 incore/state_transition.goandcore/txpool/validation.go. ADR-0005 pins the per-EIP porting rule (D8). - The
IsPraguegate 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 changesFloorDataGas'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:
SLOTNUMis0x4b(core/vm/opcodes.go:108), enabled byenable7843(core/vm/eips.go:579); the header field isSlotNumber *uint64withrlp:"optional"(core/types/block.go:107), so it is part of the block hash; the value enters the EVM throughBlockContext.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.Hashhashes the struct, so the field enters the block hash by itself, butHashNoNonce/HashNoValidatorincore/types/block.goandsigHashinconsensus/XDPoS/engines/engine_v{1,2}/utils.goare 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/GasBudgetincore/vm/gascosts.go,Contract.Gasretyped fromuint64, a two-dimensionalGasPool(CheckGasAmsterdamincore/gaspool.go:64,Available(amsterdam)in:110),tracing.Gasand a gas-change hook. WithStateGaspinned 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 incore/vm/gas_table.goandcore/vm/operations_acl.go, and the shared activatorenable8037And8038(core/vm/eips.go:590). - M4c, transaction layer:
gasRemainingbecomes aGasBudget, the runtime budget is split into execution and state reservoirs, and the per-transaction cap switches from the EIP-7825MaxTxGastoMaxTxTotalGas(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 inStateDB(core/state/statedb.go:779,Finalise(rules params.Rules) *bal.ConstructionBlockAccessList), built inPreExecution/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
ApplySignTransactiondoes 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 ofProcessParentBlockHash. - Address
0x4e59b44847b379578588920cA78FbF26c0B4956Cand 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 isxdc/166and 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;protocolLengthsis 18 for eth/69 and eth/70 and 20 from eth/71,:49). - M8b, snap/1 base: this chain has no
snapcapability 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.goandbal_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
osakaBlockunset. The first deliverable of this stage is an Osaka decision, not an Amsterdam one. - The
AmsterdamBlockvalue 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
GasCostsandContract.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:linelist) in each PR body, so a later rebase can be checked against the snapshot the port was written from.
Validation
- Repository rule:
make allplusgofmt/goimportson the changed files; lint and tests targeted at the packages each commit touches, not the whole tree. - Three extra checks for this fork:
- Pre-fork immutability: replay a range of pre-Amsterdam blocks and compare state roots against a pre-port binary.
- Upstream vectors: opcode vectors for SLOTNUM, and the gas-table and state-transition vectors for the 2D gas model.
- 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
osakaBlockunset, 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 useblock.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
osakaBlockunset, and Amsterdam must not be scheduled before Osaka.
Upstream
go-ethereum master 2006f0836 (2026-10-23), rechecked 2026-10-29:
core/vm/jump_table.go:106-110:newAmsterdamInstructionSetis Osaka plusenable7843,enable8024andenable8037And8038.core/vm/eips.go:579(enable7843),:590(enable8037And8038).core/vm/evm.go:163(case evm.chainRules.IsAmsterdam),:530(chargeAccountCreation, account creation charged from the state-gas ledger),:675(code size checked before state gas).params/config.go:329(AmsterdamTime),:742(IsAmsterdam),:1089(ActiveSystemContractsfor EIP-8282).core/types/block.go:104,107(BlockAccessListHashadded by EIP-7928,SlotNumberadded by EIP-7843, both ignored in legacy headers).core/state_transition.go:144(EIP-7981),:202-213(EIP-7976),:592-595(EIP-7825 / EIP-8037 caps),core/vm/gascosts.go(GasBudget).- Only Sepolia schedules Amsterdam,
params/chainspecs/sepolia.json:25=1791294816; mainnet and Hoodi have noamsterdamTime. - Relevant geth PRs: core/vm: implement eip-7843: SLOTNUM ethereum/go-ethereum#33589 (EIP-7843 SLOTNUM), core/vm: add amsterdam instruction set etc. ethereum/go-ethereum#33643 (Amsterdam instruction set), core: genesis slot number parsing ethereum/go-ethereum#35464 (genesis slot number parsing), miner: supply a slot number when synthesising pending block post-Amsterdam ethereum/go-ethereum#34792 / eth/catalyst: use IsAmsterdam when sealing simulated beacon blocks ethereum/go-ethereum#35392 (miner and catalyst slot supply), core: implement EIP-8038 ethereum/go-ethereum#35216 (EIP-8038), core/vm: implements EIP-8037 spec change ethereum/go-ethereum#35173 / core: implement EIP-2780 and EIP-8037 changes ethereum/go-ethereum#35318 / core: remaining spec fixes for glamsterdam ethereum/go-ethereum#35279 (EIP-8037 and most of the 2D-gas spec changes, core: implement EIP-2780 and EIP-8037 changes ethereum/go-ethereum#35318 also carries EIP-2780), eth/downloader: implement BAL downloading ethereum/go-ethereum#35386 / eth/protocols/snap: verify raw BAL before single decode ethereum/go-ethereum#35890 (BAL), eth/protocols/eth: implement eth71 bal response ethereum/go-ethereum#34879 / cmd/devp2p/internal/ethtest: add eth/71 (EIP-8159) tests ethereum/go-ethereum#35389 (EIP-8159), core, eth/protocols/snap, eth/downloader: snap/2 sync logic ethereum/go-ethereum#34626 / eth/protocols/snap: introduce snapv2 skeleton ethereum/go-ethereum#35098 (EIP-8189), eth/tracers: fix prestateTracer for EIP-6780 SELFDESTRUCT ethereum/go-ethereum#33050 (prestateTracer survivor fix).
- Upstream master now also carries a
Bogotafork after Amsterdam (params/config.go:330,params/forks/forks.go:49), unscheduled everywhere. This issue stays Amsterdam-only.
Open questions
- What SLOTNUM returns (D1). EIP-7843 returns the beacon chain slot and this chain has no beacon chain, so like
block.prevrandaothe value has to be defined deterministically and documented before activation. Tracked in implement EIP-7843 SLOTNUM and the slot number header field (amsterdam) #2760. - When Osaka, and then Amsterdam, are scheduled (D7). No network schedules Osaka today, and the fork-order rule requires Osaka first.
- 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.
- 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 anddocs/solidity.md). - The beacon / engine API side of Amsterdam: EIP-8282, the
ExecutionPayloadV4family,fcuV4/getPayloadV4/newPayloadV4/newPayloadV5, and the CL-provided target gas limit. This chain has no engine API at all (beacon/carries onlymerkle,paramsandtypes). - 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
AmsterdamBlockset, after Osaka. docs/solidity.mdanddocs/upgrade.mdstate 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
feat(core,params): add the amsterdam fork switch
feat(core,docs,params): add the amsterdam fork switch
feat(core,docs,params): add the amsterdam fork switch
feat(core,params): add the amsterdam fork switch
feat(core,params): add the amsterdam fork switch
feat(core,params): add the amsterdam fork switch
feat(core,params): add the amsterdam fork switch
feat(core,params): add the amsterdam fork switch
feat(core,params): add the amsterdam fork switch
feat(core,params): add the amsterdam fork switch
feat(core,params): add the amsterdam fork switch
feat(core,params): add the amsterdam fork switch
feat(core,params): add the amsterdam fork switch
feat(core,params): add the amsterdam fork switch
feat(core,params): add the amsterdam fork switch
feat(core,params): add the amsterdam fork switch
feat(core,params): add the amsterdam fork switch
feat(core,params): add the amsterdam fork switch
feat(core,params): add the amsterdam fork switch
feat(core,params): add the amsterdam fork switch
feat(core,params): add the amsterdam fork switch (#2756)
chore: merge dev-upgrade into main (#2903)
[-]core/vm,core/types,params: implement the Amsterdam EVM version[/-][+]implement the Amsterdam EVM version[/+]