Feature/glamsterdam existing eip updates#12091
Conversation
glamsterdam-devnet-6 raises the EIP-7954 max contract code size from 32 KiB to 64 KiB (MAX_CODE_SIZE = 0x10000); MaxInitCodeSize derives as 2x. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
glamsterdam-devnet-6 activates EIP-2780 (reduce intrinsic tx gas) and EIP-8246 (SELFDESTRUCT no burn) in the Amsterdam fork; both now have for_amsterdam EEST fixtures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Set the EIP-8038 state-access gas costs to their final glamsterdam-devnet-6 values (COLD_ACCOUNT_ACCESS/COLD_STORAGE_ACCESS 3000, ACCOUNT_WRITE 8000, STORAGE_WRITE 10000 -> CALL_VALUE 10300, CREATE_ACCESS 11000, access-list entries 3000, STORAGE_CLEAR_REFUND 12480) and schedule the EIP into Amsterdam. WIP: SSTORE regular-write wiring (SSetRegular -> StorageWrite) and the source-based refund model still to follow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update EIP-2780 to its current spec as scheduled in glamsterdam-devnet-6: - TX_BASE_COST 4500 -> 12000 (ECDSA recovery + sender access + sender write) - recipient charge restructured to EELS calculate_intrinsic_cost: a flat COLD_ACCOUNT_ACCESS touch (via EIP-8038), plus TRANSFER_LOG_COST + TX_VALUE_COST (4244) for value transfers; CREATE adds CREATE_ACCESS (11000) + NEW_ACCOUNT state. Drops the earlier-draft two-tier no-code cold cost, STATE_UPDATE, and in-intrinsic new-account surcharge. Validated: value-moving transactions now match expected gas/state/receipts (remaining diff is the EIP-7928 BAL hash, addressed separately). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Charge the EIP-8038 STORAGE_WRITE (10000) regular cost on the first change to a storage slot (both fresh-slot creation and reset), replacing the EIP-8037 SSetRegular (2900) / SStoreResetCost. Validated: slotnum value tests now match expected gas/state/receipts (BAL hash still pending the EIP-8282 builder-request reads on the stacked branch). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Under EIP-8038 the SSTORE warm-access cost (WARM_ACCESS=100) is charged by the access step itself and the separate net-metered SSTORE charge is dropped, so a no-op or repeat write costs exactly the access (cold 3000 / warm 100) rather than access + 100. Matches EELS amsterdam sstore. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Align SSTORE refunds to EIP-8038: storage-clear refund 4800 -> 12480 ((STORAGE_WRITE + COLD_STORAGE_ACCESS) * 4800/5000), and restore-to-original refunds the STORAGE_WRITE (10000) regular charge taken on the first change (a freshly-created slot also refunds its state gas in-frame, unchanged). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @Marchhill's task in 7m 8s —— View job Review: Feature/glamsterdam existing eip updates
SummaryThis PR aligns the Nethermind implementation to glamsterdam devnet-6: final EIP-8038 repricing (COLD_ACCOUNT_ACCESS 2600→3000, COLD_STORAGE_ACCESS 2100→3000, STORAGE_WRITE replaces SSetRegular/SReset, StorageClearRefund formula), EIP-2780 intrinsic gas simplified to match EELS The arithmetic checks out: Findings
Medium1. Dead
2. Pre-EIP-8038 EIP-2780 standalone behavior changed — The old Low3.
4. EIP-8246 re-enabled without explanation — The prior comment explicitly blocked EIP-8246 because it wasn't in EEST
|
Under EIP-8038 the per-authorization regular gas is ACCOUNT_WRITE (8000) plus REGULAR_PER_AUTH_BASE_COST (auth-tuple calldata floor + ECRECOVER + cold account access + 2 warm accesses) = 15816, replacing the earlier draft's 7500. The state component (NEW_ACCOUNT + AUTH_BASE) is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| /// the recipient's existence, code, or access-list membership (so no <paramref name="worldState"/> lookup), | ||
| /// overriding EIP-2929's "all tx addresses are warm" rule. The recipient stays pre-warmed for execution. | ||
| /// </remarks> | ||
| private static long Eip2780ExtraGas(Transaction tx, IReleaseSpec spec, IReadOnlyStateProvider? worldState) |
There was a problem hiding this comment.
Medium — dead worldState parameter
worldState is no longer read anywhere in this method body (the old RecipientTouchCost/IsDeadAccount calls are gone). It also makes CalculateIntrinsicGas's optional worldState = null silently a no-op for EIP-2780 paths.
The XML doc acknowledges it ("no worldState lookup"), but callers who pass a non-null worldState now get different gas figures than they did pre-PR, with no compile-time warning. Consider removing the parameter from both this private method and the public overload to make the API honest, or at the minimum add a [Obsolete] annotation on the public overload.
| long regularWriteCost = spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : GasCostOf.SSetRegular; | ||
| if (!TIsSlotCreation.IsActive) | ||
| return UpdateGas(ref gas, spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : spec.GasCosts.SStoreResetCost); |
There was a problem hiding this comment.
Low — regularWriteCost is not used in the early-return path
regularWriteCost is computed on line 309 but the !TIsSlotCreation.IsActive branch on line 311 independently re-evaluates spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : spec.GasCosts.SStoreResetCost rather than using it. The values are numerically identical (pre-8038: SSetRegular == SStoreResetCost == 2900; post-8038: both StorageWrite), so this is not a correctness bug, but it would be cleaner to use regularWriteCost in the early return to make the intent self-evident:
| long regularWriteCost = spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : GasCostOf.SSetRegular; | |
| if (!TIsSlotCreation.IsActive) | |
| return UpdateGas(ref gas, spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : spec.GasCosts.SStoreResetCost); | |
| long regularWriteCost = spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : GasCostOf.SSetRegular; | |
| if (!TIsSlotCreation.IsActive) | |
| return UpdateGas(ref gas, spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : spec.GasCosts.SStoreResetCost); |
Actually — since SStoreResetCost and SSetRegular happen to be equal today, the cleaner fix is just to use regularWriteCost for both branches:
| long regularWriteCost = spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : GasCostOf.SSetRegular; | |
| if (!TIsSlotCreation.IsActive) | |
| return UpdateGas(ref gas, spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : spec.GasCosts.SStoreResetCost); | |
| long regularWriteCost = spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : GasCostOf.SSetRegular; | |
| if (!TIsSlotCreation.IsActive) | |
| return UpdateGas(ref gas, regularWriteCost); |
Note this is a semantic equivalence assumption: if SStoreResetCost and SSetRegular ever diverge in a future fork, the two branches would need different constants again.
| long cost = spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; | ||
| if (hasValue) | ||
| cost += GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780; |
There was a problem hiding this comment.
Medium — pre-EIP-8038 EIP-2780 behavior is significantly changed
When IsEip2780Enabled && !IsEip8038Enabled (a configuration that the flag structure still allows), this code now charges a flat GasCostOf.ColdAccountAccess for the recipient touch. The prior implementation charged:
GasCostOf.WarmStateRead(100) if the recipient was in the access listGasCostOf.ColdAccountAccessNoCodeEip2780(500) for code-less accountsGasCostOf.ColdAccountAccess(2600) for code-bearing accounts- An additional
GasCostOf.NewAccount(25 000) for value transfers to non-existent, non-precompile recipients - An additional
GasCostOf.StateUpdateEip2780(1 000) for value transfers to existing accounts - An additional EIP-7702 delegation-target touch when applicable
All five charges are now absent for standalone-EIP-2780 configurations, not just the EIP-8038 path.
If EIP-2780 is intentionally always co-activated with EIP-8038 (as in Amsterdam), the standalone path is dead code and the simplification is harmless. In that case, please add a brief comment here — e.g. // EIP-2780 is always co-activated with EIP-8038 (Amsterdam); the pre-8038 branch is kept only for defensive completeness. — so future readers don't reintroduce the old multi-tier logic. If standalone-EIP-2780 is a supported configuration, the missing new-account surcharge and EIP-7702 delegation charge need to be verified against the EELS spec.
| // EEST `for_amsterdam` fixtures, so enabling it would diverge from conformance tests. | ||
| // It can be activated via the Eip8246Transition chainspec parameter when scheduled. | ||
| spec.IsEip8038Enabled = true; | ||
| spec.IsEip8246Enabled = true; |
There was a problem hiding this comment.
Low — EIP-8246 re-enabled without visible implementation in this diff
The previous comment said EIP-8246 "is implemented but stays off here" due to it being a Draft not in the EEST for_amsterdam fixtures. This commit silently enables it alongside EIP-8038 with no explanation. Please confirm:
- EIP-8246 is now included in the EEST
for_amsterdamfixtures (or the conformance test exclusion is intentional). - No separate implementation PRs are needed — the existing implementation is complete and correct for devnet-6.
If this is just a "devnet-6 enables it" change and the implementation was already merged, a one-line comment here noting the devnet reference would help future auditors.
A value/regular call to an EIP-7702 delegated recipient also accesses the delegation target. The top-level frame only warms (does not gas-charge) that target during execution, so price it in the intrinsic: a cold account access (warm if the target is in the tx access list). Restores the charge dropped when the recipient touch was simplified to a flat cold cost. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sfers A top-level value transfer that materialises a new (dead, non-precompile) recipient now pays NEW_ACCOUNT state gas in execution, evaluated against pre-transfer state, routed through the EIP-8037 state reservoir so it lands in the state dimension of the block's max(regular, state) accounting. If the state gas cannot be covered the frame is out of gas: no value moves, no transfer log, and the sender forfeits all gas. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A value-bearing CALL now charges a flat CALL_VALUE (10300) regular under EIP-8038, and a value transfer to a dead recipient charges NEW_ACCOUNT (183600) state gas separately (routed through the state reservoir into the block's max(regular,state) accounting), replacing the earlier EIP-2780 three-tier value cost that folded the new-account surcharge in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A top-level call to an EIP-7702 delegated recipient charges a flat cold account access for the delegation target in execution (the target is already pre-warmed for the frame, so this is its only charge), per EELS amsterdam top-frame logic. On insufficient gas the frame is exhausted and the EVM halts out of gas. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The block's regular-gas dimension is the pre-refund regular gas consumed; the EIP-7623/7976 calldata floor is a sender-only minimum (tx_gas_used / receipts cumulative) and must not inflate the block header gasUsed. Drop the Math.Max(blockRegularGas, floorGas) from the EIP-8037 block-regular calculation so block gasUsed = max(sum regular, sum state) per the spec. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
For each authorization whose account leaf already exists, EELS refunds the worst-case ACCOUNT_WRITE charged in the intrinsic to the regular-gas refund counter (in addition to the NEW_ACCOUNT/AUTH_BASE state refunds applied pre-execution). Nethermind applied only the state refund and zeroed the delegation-refund count, dropping the regular refund. Now the count flows on to the refund counter as ACCOUNT_WRITE x existing-authorities, and the pre-execution state refund is no longer double-applied in ApplyCodeInsertRefunds. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sending a positive balance to an empty SELFDESTRUCT beneficiary charges ACCOUNT_WRITE (8000) regular gas in addition to the NEW_ACCOUNT state gas, per EELS amsterdam selfdestruct. Nethermind charged only the state gas. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CREATE/CREATE2 opcode charges CREATE_ACCESS (11000) regular gas for the new account under EIP-8038, replacing the EIP-8037 CreateRegular (9000), per EELS amsterdam create. (NEW_ACCOUNT state + init-code + memory unchanged.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ations An EIP-7702 authorization that fails validation touches no state, so the worst-case intrinsic charge is fully refunded: NEW_ACCOUNT + AUTH_BASE state gas and the ACCOUNT_WRITE regular gas (EELS set_delegation, invalid case). Nethermind refunded nothing for invalid auths, over-charging by up to the full per-auth state cost. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The top-level delegation-target cold charge now reads the recipient's delegation from post-authorization state, so a delegation installed by the same transaction's authorization list is charged (and one cleared by it is not), matching EELS which reads the recipient code at execution time. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes Closes Resolves #
Please choose one of the keywords above to refer to the issue this PR solves followed by the issue number (e.g. Fixes #000). If no issue number, remove the line. Also, remove everything marked optional that is not applicable. Remove this note after reading.
Changes
Types of changes
What types of changes does your code introduce?
Testing
Requires testing
If yes, did you write tests?
Notes on testing
Optional. Remove if not applicable.
Documentation
Requires documentation update
If yes, link the PR to the docs update or the issue with the details labeled
docs. Remove if not applicable.Requires explanation in Release Notes
If yes, fill in the details here. Remove if not applicable.
Remarks
Optional. Remove if not applicable.