From 0aa235fb75087c548e3f64af32f8816342b1db45 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:34:15 +0100 Subject: [PATCH 01/39] feat: WIP EIP-2780 reduce intrinsic transaction gas Snapshot of in-progress EIP-2780 work (TX_BASE_COST reduction and state-work-based repricing) so a separate EIP-8246 branch can build cleanly on top of it. Co-Authored-By: Claude Opus 4.8 --- .../Tracing/GasEstimator.cs | 2 +- src/Nethermind/Nethermind.Core/GasCostOf.cs | 9 ++ .../Nethermind.Core/Specs/IReleaseSpec.cs | 6 ++ .../Specs/ReleaseSpecDecorator.cs | 1 + .../Nethermind.Evm.Test/Eip2780Tests.cs | 101 ++++++++++++++++++ .../IntrinsicGasCalculatorTests.cs | 91 ++++++++++++++++ .../GasPolicy/EthereumGasPolicy.cs | 52 ++++++++- .../Nethermind.Evm/GasPolicy/IGasPolicy.cs | 27 +++-- .../Instructions/EvmInstructions.Call.cs | 23 ++-- .../Instructions/EvmInstructions.CodeCopy.cs | 6 +- .../EvmInstructions.ControlFlow.cs | 3 +- .../EvmInstructions.Environment.cs | 6 +- .../Nethermind.Evm/IntrinsicGasCalculator.cs | 10 +- .../TransactionProcessor.cs | 2 +- .../OverridableReleaseSpec.cs | 1 + .../ChainSpecStyle/ChainParameters.cs | 1 + .../ChainSpecBasedSpecProvider.cs | 2 + .../ChainSpecStyle/ChainSpecLoader.cs | 1 + .../Json/ChainSpecParamsJson.cs | 1 + .../Nethermind.Specs/ReleaseSpec.cs | 1 + 20 files changed, 316 insertions(+), 30 deletions(-) create mode 100644 src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs diff --git a/src/Nethermind/Nethermind.Blockchain/Tracing/GasEstimator.cs b/src/Nethermind/Nethermind.Blockchain/Tracing/GasEstimator.cs index 89d531542cf8..b39f46a822c8 100644 --- a/src/Nethermind/Nethermind.Blockchain/Tracing/GasEstimator.cs +++ b/src/Nethermind/Nethermind.Blockchain/Tracing/GasEstimator.cs @@ -74,7 +74,7 @@ private EstimationResult EstimateInternal( if (CheckFunds(tx, spec, gasTracer, senderBalance, out UInt256 available) is { } fundsResult) return fundsResult; - long intrinsicGas = IntrinsicGasCalculator.Calculate(tx, spec, header.GasLimit).MinimalGas; + long intrinsicGas = IntrinsicGasCalculator.Calculate(tx, spec, header.GasLimit, stateProvider).MinimalGas; long leftBound = Math.Max(gasTracer.GasSpent - 1, intrinsicGas - 1); long rightBound = Math.Min( tx.GasLimit != 0 && tx.GasLimit >= intrinsicGas ? tx.GasLimit : header.GasLimit, diff --git a/src/Nethermind/Nethermind.Core/GasCostOf.cs b/src/Nethermind/Nethermind.Core/GasCostOf.cs index d57348f280f3..d02784ad04d3 100644 --- a/src/Nethermind/Nethermind.Core/GasCostOf.cs +++ b/src/Nethermind/Nethermind.Core/GasCostOf.cs @@ -92,5 +92,14 @@ public static class GasCostOf public const long MinModExpEip2565 = 200; // eip-2565 public const long MinModExpEip7883 = 500; // eip-7883 + // eip-2780: reduce intrinsic transaction gas and reprice state-touching primitives. + public const long TransactionEip2780 = 4500; // TX_BASE_COST = 3000 ECRECOVER + 1000 STATE_UPDATE + 500 sender cold no-code + public const long StateUpdateEip2780 = 1000; // one account-leaf write (nonce/balance coalesced) + public const long ColdAccountAccessNoCodeEip2780 = 500; // cold touch of an account known to have no code + public const long TransferLogEip2780 = 1756; // eip-7708 LOG3 transfer event: 375 + 3*375 + 32*8 + // CALL value-transfer cost tiers replacing the legacy CallValue (9000) + NewAccount (25000). + public const long CallValueSelfEip2780 = StateUpdateEip2780; // 1000 + public const long CallValueExistingEip2780 = 2 * StateUpdateEip2780 + TransferLogEip2780; // 3756 + public const long CallValueNewAccountEip2780 = StateUpdateEip2780 + NewAccount + TransferLogEip2780; // 27756 } } diff --git a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs index e61a2763291f..7efcbff77029 100644 --- a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs @@ -475,6 +475,12 @@ public interface IReleaseSpec : IEip1559Spec, IReceiptSpec /// public bool IsEip7954Enabled { get; } + /// + /// EIP-2780: Reduce intrinsic transaction gas (TX_BASE_COST) and reprice value-transfer + /// and cold-account costs against actual state work. + /// + public bool IsEip2780Enabled { get; } + /// /// Precomputed gas cost and refund constants derived from this spec. /// Values are cached per spec instance (singletons per fork) to avoid diff --git a/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs b/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs index bccc4cd62cf7..e78f65dfa023 100644 --- a/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs +++ b/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs @@ -120,6 +120,7 @@ public class ReleaseSpecDecorator(IReleaseSpec spec) : IReleaseSpec public virtual bool IsEip7778Enabled => spec.IsEip7778Enabled; public virtual bool IsEip7843Enabled => spec.IsEip7843Enabled; public virtual bool IsEip7954Enabled => spec.IsEip7954Enabled; + public virtual bool IsEip2780Enabled => spec.IsEip2780Enabled; public virtual bool IsEip8024Enabled => spec.IsEip8024Enabled; public SpecGasCosts GasCosts => spec.GasCosts; } diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs new file mode 100644 index 000000000000..58e2d93ecada --- /dev/null +++ b/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs @@ -0,0 +1,101 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Threading.Tasks; +using Nethermind.Core; +using Nethermind.Core.Specs; +using Nethermind.Core.Test.Blockchain; +using Nethermind.Core.Extensions; +using Nethermind.Core.Test.Builders; +using Nethermind.Evm.GasPolicy; +using Nethermind.Int256; +using Nethermind.Specs; +using Nethermind.Specs.Forks; +using Nethermind.Specs.Test; +using Nethermind.State; +using NUnit.Framework; + +namespace Nethermind.Evm.Test; + +/// +/// EIP-2780 reprices the value-moving call cost and cold-account touches. These tests pin the +/// gas-policy primitives to the EIP's reference values and exercise the intrinsic path end-to-end. +/// +[TestFixture] +public class Eip2780Tests +{ + private static readonly IReleaseSpec Eip2780Spec = new OverridableReleaseSpec(Prague.Instance) { IsEip2780Enabled = true }; + + private static long ChargeCallValue(bool isSelfCall, bool recipientEmpty) + { + EthereumGasPolicy gas = EthereumGasPolicy.FromLong(1_000_000); + Assert.That(EthereumGasPolicy.ConsumeCallValueTransferEip2780(ref gas, isSelfCall, recipientEmpty), Is.True); + return 1_000_000 - EthereumGasPolicy.GetRemainingGas(in gas); + } + + [TestCase(true, false, GasCostOf.CallValueSelfEip2780, TestName = "self-call → STATE_UPDATE (1000)")] + [TestCase(false, false, GasCostOf.CallValueExistingEip2780, TestName = "existing recipient → 2*STATE_UPDATE + log (3756)")] + [TestCase(false, true, GasCostOf.CallValueNewAccountEip2780, TestName = "empty recipient → STATE_UPDATE + NEW_ACCOUNT + log (27756)")] + public void Call_value_cost_uses_eip2780_tiers(bool isSelfCall, bool recipientEmpty, long expected) => + Assert.That(ChargeCallValue(isSelfCall, recipientEmpty), Is.EqualTo(expected)); + + [Test] + public void Call_value_tiers_have_expected_absolute_values() + { + // Guards against the constants drifting from the EIP-2780 specification. + Assert.That(GasCostOf.CallValueSelfEip2780, Is.EqualTo(1000)); + Assert.That(GasCostOf.CallValueExistingEip2780, Is.EqualTo(3756)); + Assert.That(GasCostOf.CallValueNewAccountEip2780, Is.EqualTo(27756)); + } + + private static long ChargeAccountAccess(IReleaseSpec spec, bool hasCode, bool prewarm) + { + EthereumGasPolicy gas = EthereumGasPolicy.FromLong(1_000_000); + using StackAccessTracker tracker = new(); + if (prewarm) tracker.WarmUp(TestItem.AddressB); + Assert.That(EthereumGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in tracker, isTracingAccess: false, TestItem.AddressB, hasCode: hasCode), Is.True); + return 1_000_000 - EthereumGasPolicy.GetRemainingGas(in gas); + } + + [Test] + public void Cold_account_touch_is_two_tier_under_eip2780() + { + Assert.That(ChargeAccountAccess(Eip2780Spec, hasCode: true, prewarm: false), Is.EqualTo(GasCostOf.ColdAccountAccess), "cold account with code"); + Assert.That(ChargeAccountAccess(Eip2780Spec, hasCode: false, prewarm: false), Is.EqualTo(GasCostOf.ColdAccountAccessNoCodeEip2780), "cold account without code"); + Assert.That(ChargeAccountAccess(Eip2780Spec, hasCode: false, prewarm: true), Is.EqualTo(GasCostOf.WarmStateRead), "warm account stays at WARM_STATE_READ"); + } + + [Test] + public void Cold_account_touch_stays_flat_without_eip2780() + { + // Pre-EIP-2780 the code-less hint is ignored: every cold touch costs ColdAccountAccess. + Assert.That(ChargeAccountAccess(Prague.Instance, hasCode: false, prewarm: false), Is.EqualTo(GasCostOf.ColdAccountAccess)); + Assert.That(ChargeAccountAccess(Prague.Instance, hasCode: true, prewarm: false), Is.EqualTo(GasCostOf.ColdAccountAccess)); + } + + private static Task CreateChain() => + BasicTestBlockchain.Create(b => b.AddSingleton( + new TestSpecProvider(new OverridableReleaseSpec(Prague.Instance) { IsEip2780Enabled = true, IsEip7708Enabled = true }))); + + [TestCase(false, GasCostOf.TransactionEip2780 + GasCostOf.TransferLogEip2780, TestName = "existing recipient: base + transfer log (6256)")] + [TestCase(true, GasCostOf.TransactionEip2780 + GasCostOf.TransferLogEip2780 + GasCostOf.NewAccount, TestName = "new recipient: base + log + new-account surcharge (31256)")] + public async Task Simple_value_transfer_spends_eip2780_intrinsic_gas(bool recipientIsNew, long expectedGas) + { + using BasicTestBlockchain chain = await CreateChain(); + UInt256 nonce = chain.StateReader.GetNonce(chain.BlockTree.Head!.Header, TestItem.AddressA); + + // AddressB is a funded code-less EOA in genesis; AddressF is never funded (dead per EIP-161). + Address recipient = recipientIsNew ? TestItem.AddressF : TestItem.AddressB; + Transaction tx = Build.A.Transaction + .WithTo(recipient) + .WithValue(1.Ether) + .WithNonce(nonce) + .WithGasLimit(60000) + .SignedAndResolved(TestItem.PrivateKeyA) + .TestObject; + + Block block = await chain.AddBlock(tx); + + Assert.That(chain.ReceiptStorage.Get(block)[0].GasUsed, Is.EqualTo(expectedGas)); + } +} diff --git a/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs b/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs index dc2b65d64693..4b4213f2685f 100644 --- a/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs @@ -11,8 +11,11 @@ using Nethermind.Core.Specs; using Nethermind.Core.Test.Builders; using Nethermind.Evm.GasPolicy; +using Nethermind.Evm.State; using Nethermind.Int256; using Nethermind.Specs.Forks; +using Nethermind.Specs.Test; +using NSubstitute; using NUnit.Framework; namespace Nethermind.Evm.Test @@ -275,5 +278,93 @@ public void Eip8037_nongeneric_minimal_gas_is_at_least_regular_plus_state() long regularPlusState = GasCostOf.Transaction + GasCostOf.CreateRegular + GasCostOf.CreateState; Assert.That(gas.MinimalGas, Is.GreaterThanOrEqualTo(regularPlusState)); } + + // EIP-2780 intrinsic-gas vectors. The new-account case matches the normative pseudocode (31,256); + // the EIP's stated 31,756 total adds a 500 recipient cold-lookup charged at execution time. + public enum Recipient { NewAccount, ExistingEoa, Precompile, SelfTransfer, EmptyZeroValue } + + private const long TxBaseEip2780 = GasCostOf.TransactionEip2780; + private const long TransferLogEip2780 = GasCostOf.TransferLogEip2780; + + public static IEnumerable Eip2780IntrinsicCases() + { + yield return new TestCaseData(Recipient.NewAccount, (UInt256)1, TxBaseEip2780 + TransferLogEip2780 + GasCostOf.NewAccount) + .SetName("Eip2780_intrinsic_value_to_new_account_31256"); + yield return new TestCaseData(Recipient.Precompile, (UInt256)1, TxBaseEip2780 + TransferLogEip2780) + .SetName("Eip2780_intrinsic_value_to_precompile_6256"); + yield return new TestCaseData(Recipient.ExistingEoa, (UInt256)1, TxBaseEip2780 + TransferLogEip2780) + .SetName("Eip2780_intrinsic_value_to_existing_eoa_6256"); + yield return new TestCaseData(Recipient.SelfTransfer, (UInt256)1, TxBaseEip2780) + .SetName("Eip2780_intrinsic_self_transfer_4500"); + yield return new TestCaseData(Recipient.EmptyZeroValue, (UInt256)0, TxBaseEip2780) + .SetName("Eip2780_intrinsic_zero_value_to_empty_4500"); + } + + [TestCaseSource(nameof(Eip2780IntrinsicCases))] + public void Eip2780_intrinsic_gas_is_calculated_properly(Recipient recipient, UInt256 value, long expectedStandard) + { + OverridableReleaseSpec spec = new(Prague.Instance) { IsEip2780Enabled = true }; + Address to = recipient switch + { + Recipient.Precompile => Address.FromNumber(1), // 0x01 ECRECOVER precompile + Recipient.SelfTransfer => TestItem.AddressA, // == sender (PrivateKeyA) + _ => TestItem.AddressB, + }; + Transaction tx = Build.A.Transaction.WithValue(value).WithTo(to) + .SignedAndResolved(TestItem.PrivateKeyA).TestObject; + + IReadOnlyStateProvider state = Substitute.For(); + // Only an unfunded recipient is nonexistent per EIP-161 (drives the new-account surcharge). + state.IsDeadAccount(Arg.Any
()).Returns(recipient is Recipient.NewAccount); + + EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, spec, 0, state); + + Assert.That(gas.Standard, Is.EqualTo(expectedStandard)); + Assert.That(gas.MinimalGas, Is.EqualTo(Math.Max(expectedStandard, TxBaseEip2780))); + } + + [Test] + public void Eip2780_intrinsic_gas_for_create_charges_transfer_log_only_when_value_positive() + { + OverridableReleaseSpec spec = new(Prague.Instance) { IsEip2780Enabled = true }; + IReadOnlyStateProvider state = Substitute.For(); + + Transaction createZero = Build.A.Transaction.WithValue(0).WithCode(Array.Empty()) + .SignedAndResolved(TestItem.PrivateKeyA).TestObject; + Transaction createValue = Build.A.Transaction.WithValue(1).WithCode(Array.Empty()) + .SignedAndResolved(TestItem.PrivateKeyA).TestObject; + + Assert.That(IntrinsicGasCalculator.Calculate(createZero, spec, 0, state).Standard, + Is.EqualTo(TxBaseEip2780 + GasCostOf.TxCreate)); + // CREATE endows a fresh, sender-distinct address, so value > 0 pays the transfer log. + Assert.That(IntrinsicGasCalculator.Calculate(createValue, spec, 0, state).Standard, + Is.EqualTo(TxBaseEip2780 + GasCostOf.TxCreate + TransferLogEip2780)); + } + + [Test] + public void Eip2780_reduces_the_calldata_floor_base() + { + // Without reducing the floor base, the legacy 21,000 floor would dominate and negate the EIP. + OverridableReleaseSpec spec = new(Prague.Instance) { IsEip2780Enabled = true }; + Transaction tx = Build.A.Transaction.WithData([1]).SignedAndResolved().TestObject; + + EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, spec); + + // Same per-token floor as Prague (DataTestCaseSource maps [1] to 21,040), rebased to 4,500. + Assert.That(gas.FloorGas, Is.EqualTo(TxBaseEip2780 + (21040 - GasCostOf.Transaction))); + } + + [Test] + public void Eip2780_disabled_keeps_legacy_intrinsic_base() + { + Transaction tx = Build.A.Transaction.WithValue(1).WithTo(TestItem.AddressB) + .SignedAndResolved(TestItem.PrivateKeyA).TestObject; + IReadOnlyStateProvider state = Substitute.For(); + state.IsDeadAccount(Arg.Any
()).Returns(true); + + EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, Prague.Instance, 0, state); + + Assert.That(gas.Standard, Is.EqualTo(GasCostOf.Transaction)); + } } } diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 8ee0dcc5ed8b..8aa6d1c5f55b 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using Nethermind.Core; using Nethermind.Core.Specs; +using Nethermind.Evm.State; using Nethermind.Int256; namespace Nethermind.Evm.GasPolicy; @@ -213,7 +214,8 @@ public static bool ConsumeAccountAccessGas(ref EthereumGasPolicy gas, ref readonly StackAccessTracker accessTracker, bool isTracingAccess, Address address, - AccountAccessKind kind = AccountAccessKind.Default) + AccountAccessKind kind = AccountAccessKind.Default, + bool hasCode = true) { if (!spec.UseHotAndColdStorage) return true; if (isTracingAccess) @@ -226,12 +228,17 @@ public static bool ConsumeAccountAccessGas(ref EthereumGasPolicy gas, // Precompiles are pre-warmed at tx start, so WarmUp(precompile) is already-warm and the reorder is moot. return (accessTracker.WarmUp(address) && !spec.IsPrecompile(address)) switch { - true => UpdateGas(ref gas, GasCostOf.ColdAccountAccess), + true => UpdateGas(ref gas, ColdAccountAccessCost(spec, hasCode)), false when kind == AccountAccessKind.SelfDestructBeneficiary => true, false => UpdateGas(ref gas, GasCostOf.WarmStateRead) }; } + // EIP-2780 prices a cold touch of a code-less account cheaper than one with code. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long ColdAccountAccessCost(IReleaseSpec spec, bool hasCode) => + spec.IsEip2780Enabled && !hasCode ? GasCostOf.ColdAccountAccessNoCodeEip2780 : GasCostOf.ColdAccountAccess; + public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, ref readonly StackAccessTracker accessTracker, bool isTracingAccess, @@ -405,6 +412,16 @@ public static long ApplyCodeInsertRefunds(ref EthereumGasPolicy gas, int codeIns public static bool ConsumeCallValueTransfer(ref EthereumGasPolicy gas) => UpdateGas(ref gas, GasCostOf.CallValue); + // EIP-2780 value-moving call cost: subsumes the legacy CallValue + NewAccount charges. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ConsumeCallValueTransferEip2780(ref EthereumGasPolicy gas, bool isSelfCall, bool recipientEmpty) + { + long cost = isSelfCall ? GasCostOf.CallValueSelfEip2780 + : recipientEmpty ? GasCostOf.CallValueNewAccountEip2780 + : GasCostOf.CallValueExistingEip2780; + return UpdateGas(ref gas, cost); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ConsumeNewAccountCreation(ref EthereumGasPolicy gas) where TEip8037 : struct, IFlag => TEip8037.IsActive switch { @@ -452,18 +469,20 @@ public static EthereumGasPolicy CreateChildFrameGas(ref EthereumGasPolicy parent public static IntrinsicGas CalculateIntrinsicGas(Transaction tx, IReleaseSpec spec) => CalculateIntrinsicGas(tx, spec, blockGasLimit: 0); - public static IntrinsicGas CalculateIntrinsicGas(Transaction tx, IReleaseSpec spec, long blockGasLimit) + public static IntrinsicGas CalculateIntrinsicGas(Transaction tx, IReleaseSpec spec, long blockGasLimit, IReadOnlyStateProvider? worldState = null) { long tokensInCallData = IGasPolicy.CalculateTokensInCallData(tx, spec); long floorTokensInAccessList = IGasPolicy.CalculateFloorTokensInAccessList(tx, spec); (long authRegularCost, long authStateCost) = IGasPolicy.AuthorizationListCost(tx, spec); long accessListCost = IGasPolicy.AccessListCost(tx, spec, floorTokensInAccessList); - long regularGas = GasCostOf.Transaction + long baseCost = spec.IsEip2780Enabled ? GasCostOf.TransactionEip2780 : GasCostOf.Transaction; + long regularGas = baseCost + DataCost(tx, spec, tokensInCallData) + CreateCost(tx, spec) + accessListCost - + authRegularCost; + + authRegularCost + + Eip2780Surcharges(tx, spec, worldState); long floorCost = IGasPolicy.CalculateFloorCost(tx, spec, tokensInCallData, floorTokensInAccessList); long createStateCost = CreateStateCost(tx, spec); long totalStateCost = authStateCost + createStateCost; @@ -516,4 +535,27 @@ private static long CreateStateCost(Transaction tx, IReleaseSpec spec) => private static long DataCost(Transaction tx, IReleaseSpec spec, long tokensInCallData) => spec.GetBaseDataCost(tx) + tokensInCallData * GasCostOf.TxDataZero; + /// + /// EIP-2780 intrinsic surcharges: the EIP-7708 transfer-log cost and the new-account surcharge. + /// Mirrors the normative pseudocode; the recipient cold-touch is charged at execution, not here. + /// + private static long Eip2780Surcharges(Transaction tx, IReleaseSpec spec, IReadOnlyStateProvider? worldState) + { + if (!spec.IsEip2780Enabled || tx.Value.IsZero) return 0; + + long cost = 0; + Address? to = tx.To; + bool isCreate = tx.IsContractCreation; + + // CREATE endows a freshly computed address distinct from the sender, so the transfer log + // applies whenever value > 0; otherwise only when the recipient differs from the sender. + if (isCreate || tx.SenderAddress != to) + cost += GasCostOf.TransferLogEip2780; + + // New-account surcharge: non-create value transfer to a nonexistent, non-precompile recipient. + if (!isCreate && to is not null && worldState is not null && !spec.IsPrecompile(to) && worldState.IsDeadAccount(to)) + cost += GasCostOf.NewAccount; + + return cost; + } } diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs index 76ea2de0f9f1..69841ed45035 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs @@ -9,6 +9,7 @@ using Nethermind.Core.Eip2930; using Nethermind.Core.Extensions; using Nethermind.Core.Specs; +using Nethermind.Evm.State; using Nethermind.Int256; namespace Nethermind.Evm.GasPolicy; @@ -71,7 +72,8 @@ static abstract bool ConsumeAccountAccessGas(ref TSelf gas, ref readonly StackAccessTracker accessTracker, bool isTracingAccess, Address address, - AccountAccessKind kind = AccountAccessKind.Default); + AccountAccessKind kind = AccountAccessKind.Default, + bool hasCode = true); static abstract bool ConsumeStorageAccessGas(ref TSelf gas, ref readonly StackAccessTracker accessTracker, @@ -130,13 +132,18 @@ static virtual long ApplyCodeInsertRefunds(ref TSelf gas, int codeInsertRefunds, TSelf.GetCodeInsertRegularRefund(codeInsertRefunds, spec); static abstract bool ConsumeCallValueTransfer(ref TSelf gas); + + // EIP-2780 three-tier value-moving call cost replacing the legacy CallValue + NewAccount charges. + static abstract bool ConsumeCallValueTransferEip2780(ref TSelf gas, bool isSelfCall, bool recipientEmpty); static abstract bool ConsumeNewAccountCreation(ref TSelf gas) where TEip8037 : struct, IFlag; static abstract bool ConsumeLogEmission(ref TSelf gas, long topicCount, long dataSize); static abstract TSelf Max(in TSelf a, in TSelf b); static virtual IntrinsicGas CalculateIntrinsicGas(Transaction tx, IReleaseSpec spec) => TSelf.CalculateIntrinsicGas(tx, spec, blockGasLimit: 0); - static abstract IntrinsicGas CalculateIntrinsicGas(Transaction tx, IReleaseSpec spec, long blockGasLimit); + // EIP-2780 needs the pre-execution state to price the new-account surcharge; worldState is + // optional so callers without state (and pre-2780 specs) keep working. + static abstract IntrinsicGas CalculateIntrinsicGas(Transaction tx, IReleaseSpec spec, long blockGasLimit, IReadOnlyStateProvider? worldState = null); static abstract TSelf CreateAvailableFromIntrinsic(long gasLimit, in TSelf intrinsicGas, IReleaseSpec spec); @@ -210,12 +217,18 @@ static void ThrowAuthorizationListNotEnabled(IReleaseSpec releaseSpec) => private static long CalculateFloorTokensInCallData(Transaction transaction, IReleaseSpec spec) => transaction.Data.Length * spec.GasCosts.TxDataNonZeroMultiplier; - protected static long CalculateFloorCost(Transaction transaction, IReleaseSpec spec, long tokensInCallData, long floorTokensInAccessList) => spec switch + protected static long CalculateFloorCost(Transaction transaction, IReleaseSpec spec, long tokensInCallData, long floorTokensInAccessList) { - { IsEip7976Enabled: true } => GasCostOf.Transaction + (CalculateFloorTokensInCallData(transaction, spec) + floorTokensInAccessList) * spec.GasCosts.TotalCostFloorPerToken, - { IsEip7623Enabled: true } => GasCostOf.Transaction + tokensInCallData * spec.GasCosts.TotalCostFloorPerToken, - _ => 0L - }; + // EIP-2780 reduces the intrinsic base; the calldata floor must track it, otherwise the + // legacy 21,000 floor would dominate and negate the reduction for value transfers. + long floorBase = spec.IsEip2780Enabled ? GasCostOf.TransactionEip2780 : GasCostOf.Transaction; + return spec switch + { + { IsEip7976Enabled: true } => floorBase + (CalculateFloorTokensInCallData(transaction, spec) + floorTokensInAccessList) * spec.GasCosts.TotalCostFloorPerToken, + { IsEip7623Enabled: true } => floorBase + tokensInCallData * spec.GasCosts.TotalCostFloorPerToken, + _ => 0L + }; + } } public readonly record struct IntrinsicGas(TGasPolicy Standard, TGasPolicy FloorGas) diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs index e5a465e04868..18a31f275e31 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs @@ -139,13 +139,19 @@ public static EvmExceptionType InstructionCall !state.AccountExists(target), true => hasValueTransfer && state.IsDeadAccount(target), - }; + }); bool newAccountOutOfGas = chargesNewAccount && !TGasPolicy.ConsumeNewAccountCreation(ref gas); diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs index 5bc88f743229..217c17aaa691 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs @@ -168,7 +168,8 @@ public static EvmExceptionType InstructionExtCodeCopy( if (outOfGas) goto OutOfGas; // Charge gas for account access (considering hot/cold storage costs). - if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address)) + if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address, + hasCode: !spec.IsEip2780Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; if (!result.IsZero) @@ -242,7 +243,8 @@ public static EvmExceptionType InstructionExtCodeSize( if (address is null) goto StackUnderflow; // Charge gas for accessing the account's state. - if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address)) + if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address, + hasCode: !spec.IsEip2780Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; vm.WorldState.AddAccountRead(address); diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs index ddc59e557940..1bcdb50fccbf 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs @@ -224,7 +224,8 @@ public static EvmExceptionType InstructionSelfDestruct(Virt if (address is null) goto StackUnderflow; // Charge gas for account access. If insufficient gas remains, abort. - if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address)) goto OutOfGas; + if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address, + hasCode: !spec.IsEip2780Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; UInt256 result = vm.WorldState.GetBalance(address); return stack.PushUInt256(in result); @@ -610,7 +611,8 @@ public static EvmExceptionType InstructionExtCodeHash( Address address = stack.PopAddress(); if (address is null) goto StackUnderflow; // Check if enough gas for account access and charge accordingly. - if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address)) goto OutOfGas; + if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address, + hasCode: !spec.IsEip2780Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; IWorldState state = vm.WorldState; // For dead accounts, the specification requires pushing zero. diff --git a/src/Nethermind/Nethermind.Evm/IntrinsicGasCalculator.cs b/src/Nethermind/Nethermind.Evm/IntrinsicGasCalculator.cs index a64b055fb60f..88477c882e0f 100644 --- a/src/Nethermind/Nethermind.Evm/IntrinsicGasCalculator.cs +++ b/src/Nethermind/Nethermind.Evm/IntrinsicGasCalculator.cs @@ -5,6 +5,7 @@ using Nethermind.Core; using Nethermind.Core.Specs; using Nethermind.Evm.GasPolicy; +using Nethermind.Evm.State; namespace Nethermind.Evm; @@ -24,15 +25,16 @@ public static class IntrinsicGasCalculator /// /// Calculates intrinsic gas with TGasPolicy type, allowing MultiGas breakdown for Arbitrum. /// - private static IntrinsicGas Calculate(Transaction transaction, IReleaseSpec releaseSpec, long blockGasLimit = 0) + private static IntrinsicGas Calculate(Transaction transaction, IReleaseSpec releaseSpec, long blockGasLimit = 0, IReadOnlyStateProvider? worldState = null) where TGasPolicy : struct, IGasPolicy => - TGasPolicy.CalculateIntrinsicGas(transaction, releaseSpec, blockGasLimit); + TGasPolicy.CalculateIntrinsicGas(transaction, releaseSpec, blockGasLimit, worldState); /// /// Non-generic backward-compatible Calculate method. /// - public static EthereumIntrinsicGas Calculate(Transaction transaction, IReleaseSpec releaseSpec, long blockGasLimit = 0) => - Calculate(transaction, releaseSpec, blockGasLimit); + /// Pre-execution state used by EIP-2780 to price the new-account surcharge; optional. + public static EthereumIntrinsicGas Calculate(Transaction transaction, IReleaseSpec releaseSpec, long blockGasLimit = 0, IReadOnlyStateProvider? worldState = null) => + Calculate(transaction, releaseSpec, blockGasLimit, worldState); public static long AccessListCost(Transaction transaction, IReleaseSpec releaseSpec) => IGasPolicy.AccessListCost(transaction, releaseSpec, diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index b75dfaf072e1..951537269031 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -971,7 +971,7 @@ protected virtual bool RecoverSenderIfNeeded(Transaction tx, IReleaseSpec spec, } protected virtual IntrinsicGas CalculateIntrinsicGas(Transaction tx, IReleaseSpec spec, long blockGasLimit) - => TGasPolicy.CalculateIntrinsicGas(tx, spec, blockGasLimit); + => TGasPolicy.CalculateIntrinsicGas(tx, spec, blockGasLimit, WorldState); protected virtual UInt256 CalculateEffectiveGasPrice(Transaction tx, bool eip1559Enabled, in UInt256 baseFee, out UInt256 opcodeGasPrice) { diff --git a/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs b/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs index c2e63f31ece3..c52dd71adb36 100644 --- a/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs @@ -128,6 +128,7 @@ public class OverridableReleaseSpec(IReleaseSpec spec) : IReleaseSpec public bool IsEip7778Enabled { get; set; } = spec.IsEip7778Enabled; public bool IsEip7843Enabled => spec.IsEip7843Enabled; public bool IsEip7954Enabled { get; set; } = spec.IsEip7954Enabled; + public bool IsEip2780Enabled { get; set; } = spec.IsEip2780Enabled; public SpecGasCosts GasCosts => new(this); FrozenSet IReleaseSpec.Precompiles => spec.Precompiles; } diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs index c42d9f609ad8..986f0da934ac 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs @@ -189,4 +189,5 @@ public class ChainParameters public ulong? Eip8024TransitionTimestamp { get; set; } public ulong? Eip7843TransitionTimestamp { get; set; } public ulong? Eip7954TransitionTimestamp { get; set; } + public ulong? Eip2780TransitionTimestamp { get; set; } } diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs index 95893bc8e2ea..bcde29f96f95 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs @@ -351,6 +351,8 @@ protected virtual ReleaseSpec CreateReleaseSpec(ChainSpec chainSpec, long releas releaseSpec.MaxCodeSize = CodeSizeConstants.MaxCodeSizeEip7954; } + releaseSpec.IsEip2780Enabled = (chainSpec.Parameters.Eip2780TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; + foreach (IChainSpecEngineParameters item in _chainSpec.EngineChainSpecParametersProvider .AllChainSpecParameters) { diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs index c3a3e6895df1..b95736f13381 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs @@ -218,6 +218,7 @@ bool GetForInnerPathExistence(KeyValuePair o) => Eip8024TransitionTimestamp = chainSpecJson.Params.Eip8024TransitionTimestamp, Eip7843TransitionTimestamp = chainSpecJson.Params.Eip7843TransitionTimestamp, Eip7954TransitionTimestamp = chainSpecJson.Params.Eip7954TransitionTimestamp, + Eip2780TransitionTimestamp = chainSpecJson.Params.Eip2780TransitionTimestamp, }; chainSpec.Parameters.ExpandAll(chainSpecJson.Params); diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs index 1f7fb82c259a..f01307b19bc7 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs @@ -196,6 +196,7 @@ public class ChainSpecParamsJson : IHasNamedForks public ulong? Eip8024TransitionTimestamp { get; set; } public ulong? Eip7843TransitionTimestamp { get; set; } public ulong? Eip7954TransitionTimestamp { get; set; } + public ulong? Eip2780TransitionTimestamp { get; set; } /// /// Catch-all for top-level chainspec params keys that don't map to an explicit property — diff --git a/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs b/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs index 7f2cfd47e2f0..b03188dd7bfe 100644 --- a/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs @@ -173,6 +173,7 @@ public virtual FrozenSet BuildPrecompilesCache() public bool IsEip7708Enabled { get; set; } public bool IsEip7954Enabled { get; set; } + public bool IsEip2780Enabled { get; set; } private ReleaseSpec? _systemSpec; From 87191c87ba92fd9fde8d7b6ed9e297a86bf319aa Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:56:23 +0100 Subject: [PATCH 02/39] feat: implement EIP-8246 (Remove SELFDESTRUCT Burn) Removes the residual ETH-burn cases left by EIP-6780: - A self-targeting SELFDESTRUCT now moves no ETH and emits no log, regardless of whether the account was created in the same transaction. - At transaction finalization, accounts marked for destruction keep their balance: storage and code are cleared and the nonce is reset to 0. A resulting zero-balance account is still removed as empty per EIP-161, matching the spec and keeping CREATE2 redeployment unblocked. Wires a new IsEip8246Enabled flag through IReleaseSpec / ReleaseSpec / decorators / chainspec (Eip8246TransitionTimestamp) and enables it in the Amsterdam fork. Adds Eip8246Tests covering same-transaction self-destruct to self/other, post-destruct funding, the zero-balance empty-account case, and the unchanged not-in-same-tx no-op, each pinned with EIP on and off. Co-Authored-By: Claude Opus 4.8 --- .../Nethermind.Core/Specs/IReleaseSpec.cs | 5 + .../Specs/IReleaseSpecExtensions.cs | 1 + .../Specs/ReleaseSpecDecorator.cs | 1 + .../Nethermind.Evm.Test/Eip8246Tests.cs | 207 ++++++++++++++++++ .../EvmInstructions.ControlFlow.cs | 7 +- .../TransactionProcessor.cs | 36 ++- .../OverridableReleaseSpec.cs | 1 + .../ChainSpecStyle/ChainParameters.cs | 1 + .../ChainSpecBasedSpecProvider.cs | 1 + .../ChainSpecStyle/ChainSpecLoader.cs | 1 + .../Json/ChainSpecParamsJson.cs | 1 + .../Nethermind.Specs/Forks/25_Amsterdam.cs | 1 + .../Nethermind.Specs/ReleaseSpec.cs | 1 + 13 files changed, 251 insertions(+), 13 deletions(-) create mode 100644 src/Nethermind/Nethermind.Evm.Test/Eip8246Tests.cs diff --git a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs index 7efcbff77029..9f5e2f85e1a8 100644 --- a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs @@ -316,6 +316,11 @@ public interface IReleaseSpec : IEip1559Spec, IReceiptSpec /// bool IsEip6780Enabled { get; } + /// + /// EIP-8246: SELFDESTRUCT no longer burns ETH + /// + bool IsEip8246Enabled { get; } + /// /// EIP-8024: Backward-compatible SWAPN, DUPN, EXCHANGE /// diff --git a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpecExtensions.cs b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpecExtensions.cs index 9c09379e8b9b..19d56158d7b5 100644 --- a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpecExtensions.cs +++ b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpecExtensions.cs @@ -52,6 +52,7 @@ public static class IReleaseSpecExtensions public bool TransientStorageEnabled => spec.IsEip1153Enabled; public bool WithdrawalsEnabled => spec.IsEip4895Enabled; public bool SelfdestructOnlyOnSameTransaction => spec.IsEip6780Enabled; + public bool RemoveSelfdestructBurn => spec.IsEip8246Enabled; public bool IsBeaconBlockRootAvailable => spec.IsEip4788Enabled; public bool IsBlockHashInStateAvailable => spec.IsEip7709Enabled; public bool MCopyIncluded => spec.IsEip5656Enabled; diff --git a/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs b/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs index e78f65dfa023..f894d6b51b8b 100644 --- a/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs +++ b/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs @@ -80,6 +80,7 @@ public class ReleaseSpecDecorator(IReleaseSpec spec) : IReleaseSpec public virtual Address? Eip2935ContractAddress => spec.Eip2935ContractAddress; public virtual long Eip2935RingBufferSize => spec.Eip2935RingBufferSize; public virtual bool IsEip6780Enabled => spec.IsEip6780Enabled; + public virtual bool IsEip8246Enabled => spec.IsEip8246Enabled; public virtual bool IsEip7702Enabled => spec.IsEip7702Enabled; public virtual bool IsEip7823Enabled => spec.IsEip7823Enabled; public virtual bool IsEip7825Enabled => spec.IsEip7825Enabled; diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip8246Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip8246Tests.cs new file mode 100644 index 000000000000..4a4da53fc84e --- /dev/null +++ b/src/Nethermind/Nethermind.Evm.Test/Eip8246Tests.cs @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using Nethermind.Core.Extensions; +using Nethermind.Core.Specs; +using Nethermind.Core.Test.Builders; +using Nethermind.Crypto; +using Nethermind.Evm.State; +using Nethermind.Evm.Tracing; +using Nethermind.Evm.TransactionProcessing; +using Nethermind.Int256; +using Nethermind.Specs; +using Nethermind.Specs.Forks; +using Nethermind.Specs.Test; +using NUnit.Framework; + +namespace Nethermind.Evm.Test; + +/// +/// EIP-8246: Remove SELFDESTRUCT Burn. With the EIP active, the residual burn cases left by +/// EIP-6780 are removed: a self-targeting SELFDESTRUCT moves no ETH, and accounts marked for +/// destruction keep their balance while their code/storage are cleared and nonce reset to 0. +/// A resulting zero-balance account is still removed as empty per EIP-161. +/// +/// +/// The true fixture runs with EIP-8246 enabled; the false fixture keeps the +/// pre-8246 (EIP-6780-only) behaviour as a baseline so every test pins both sides of the change. +/// +[TestFixture(true)] +[TestFixture(false)] +public class Eip8246Tests(bool eip8246Enabled) : VirtualMachineTestsBase +{ + private readonly ISpecProvider _specProvider = + new TestSpecProvider(new OverridableReleaseSpec(Cancun.Instance) { IsEip8246Enabled = eip8246Enabled }); + + protected override long BlockNumber => MainnetSpecProvider.ParisBlockNumber; + protected override ulong Timestamp => MainnetSpecProvider.CancunBlockTimestamp; + protected override ISpecProvider SpecProvider => _specProvider; + + private const long GasLimit = 1_000_000; + private static readonly byte[] Salt = new UInt256(123).ToBigEndian(); + + private EthereumEcdsa _ecdsa; + // Runtime code that self-destructs to its own address (ADDRESS; SELFDESTRUCT). + private byte[] _selfDestructToSelf; + private byte[] _selfDestructToSelfInit; + + [SetUp] + public override void Setup() + { + base.Setup(); + _ecdsa = new EthereumEcdsa(SpecProvider.ChainId); + TestState.CreateAccount(TestItem.PrivateKeyA.Address, 1000.Ether); + TestState.Commit(SpecProvider.GenesisSpec); + TestState.CommitTree(0); + + _selfDestructToSelf = Prepare.EvmCode + .Op(Instruction.ADDRESS) + .Op(Instruction.SELFDESTRUCT) + .Done; + _selfDestructToSelfInit = Prepare.EvmCode + .ForInitOf(_selfDestructToSelf) + .Done; + } + + [TestCase(99, TestName = "same-tx self-destruct to self, non-zero balance")] + [TestCase(0, TestName = "same-tx self-destruct to self, zero balance")] + public void Same_tx_self_destruct_to_self_does_not_burn(int balanceEther) + { + UInt256 balance = balanceEther.Ether; + Address createTxAddress = ContractAddress.From(TestItem.PrivateKeyA.Address, 0); + Address child = ContractAddress.From(createTxAddress, Salt, _selfDestructToSelfInit); + + byte[] code = Prepare.EvmCode + .Create2(_selfDestructToSelfInit, Salt, balance) + .Call(child, 100000) + .STOP() + .Done; + + ExecuteTopLevel(code, value: 100.Ether); + + if (eip8246Enabled && !balance.IsZero) + { + // Balance preserved; account survives as a fresh balance-only account. + AssertBalanceOnly(child, balance); + } + else + { + // Pre-8246 the self-burn empties the account; a zero-balance account is empty either way. + Assert.That(TestState.AccountExists(child), Is.False); + } + } + + [Test] + public void Same_tx_created_then_receives_eth_does_not_burn() + { + // Contract A self-destructs to an inheritor when called with zero value, otherwise just + // accepts ETH. Contract B creates A, calls it (selfdestruct), then sends it more ETH. + Address inheritor = TestItem.AddressE; + byte[] contractACode = Prepare.EvmCode + .CALLVALUE() + .Op(Instruction.ISZERO) + .PushData(6) + .JUMPI() + .STOP() + .JUMPDEST() + .SELFDESTRUCT(inheritor) + .Done; + byte[] initCodeA = Prepare.EvmCode.ForInitOf(contractACode).Done; + + UInt256 initialBalance = 3.Ether; + UInt256 ethReceivedAfter = 2.Ether; + + // The create-tx body itself acts as contract B: it creates A, calls it, then funds it. + Address contractB = ContractAddress.From(TestItem.PrivateKeyA.Address, 0); + Address contractA = ContractAddress.From(contractB, 1); + + byte[] contractBCode = Prepare.EvmCode + .Create(initCodeA, initialBalance) + .Call(contractA, 100000) // triggers selfdestruct-to-inheritor + .CallWithValue(contractA, 100000, ethReceivedAfter) // sends ETH after selfdestruct + .STOP() + .Done; + + ExecuteTopLevel(contractBCode, value: 10.Ether); + + // Initial balance always leaves via the inheritor (transfer, not burn). + Assert.That(TestState.GetBalance(inheritor), Is.EqualTo(initialBalance)); + + if (eip8246Enabled) + { + // The ETH received after SELFDESTRUCT is preserved rather than burned at finalization. + AssertBalanceOnly(contractA, ethReceivedAfter); + } + else + { + Assert.That(TestState.AccountExists(contractA), Is.False); + } + } + + [Test] + public void Same_tx_self_destruct_to_other_still_transfers() + { + // EIP-8246 must not change the transfer path: balance goes to the inheritor and the + // now-empty account is removed regardless of the flag. + Address inheritor = TestItem.AddressE; + byte[] runtime = Prepare.EvmCode.SELFDESTRUCT(inheritor).Done; + byte[] init = Prepare.EvmCode.ForInitOf(runtime).Done; + + Address createTxAddress = ContractAddress.From(TestItem.PrivateKeyA.Address, 0); + Address child = ContractAddress.From(createTxAddress, Salt, init); + + byte[] code = Prepare.EvmCode + .Create2(init, Salt, 5.Ether) + .Call(child, 100000) + .STOP() + .Done; + + ExecuteTopLevel(code, value: 10.Ether); + + Assert.That(TestState.GetBalance(inheritor), Is.EqualTo((UInt256)5.Ether)); + Assert.That(TestState.AccountExists(child), Is.False); + } + + [Test] + public void Self_destruct_to_self_not_in_same_tx_is_unchanged_no_op() + { + // Already a no-op since EIP-6780; EIP-8246 leaves it untouched (balance kept, code intact). + byte[] runtime = _selfDestructToSelf; + byte[] init = Prepare.EvmCode.ForInitOf(runtime).Done; + Address contract = ContractAddress.From(TestItem.PrivateKeyA.Address, 0); + + Transaction deployTx = Build.A.Transaction.WithCode(init).WithValue(7.Ether) + .WithGasLimit(GasLimit).SignedAndResolved(_ecdsa, TestItem.PrivateKeyA).TestObject; + byte[] call = Prepare.EvmCode.Call(contract, 100000).STOP().Done; + Transaction callTx = Build.A.Transaction.WithCode(call).WithGasLimit(GasLimit) + .WithNonce(1).SignedAndResolved(_ecdsa, TestItem.PrivateKeyA).TestObject; + + Block block = Build.A.Block.WithNumber(BlockNumber).WithTimestamp(Timestamp) + .WithTransactions(deployTx, callTx).WithGasLimit(2 * GasLimit).TestObject; + BlockExecutionContext blCtx = new(block.Header, SpecProvider.GetSpec(block.Header)); + + _processor.Execute(deployTx, blCtx, NullTxTracer.Instance); + _processor.Execute(callTx, blCtx, NullTxTracer.Instance); + + Assert.That(TestState.GetBalance(contract), Is.EqualTo((UInt256)7.Ether)); + Assert.That(TestState.IsContract(contract), Is.True); + } + + private void ExecuteTopLevel(byte[] code, UInt256 value) + { + Transaction tx = Build.A.Transaction.WithCode(code).WithValue(value) + .WithGasLimit(GasLimit).SignedAndResolved(_ecdsa, TestItem.PrivateKeyA).TestObject; + Block block = Build.A.Block.WithNumber(BlockNumber).WithTimestamp(Timestamp) + .WithTransactions(tx).WithGasLimit(2 * GasLimit).TestObject; + _processor.Execute(tx, new BlockExecutionContext(block.Header, SpecProvider.GetSpec(block.Header)), NullTxTracer.Instance); + } + + private void AssertBalanceOnly(Address address, UInt256 expectedBalance) + { + Assert.That(TestState.AccountExists(address), Is.True); + Assert.That(TestState.GetBalance(address), Is.EqualTo(expectedBalance), "balance preserved"); + Assert.That(TestState.GetNonce(address), Is.EqualTo(UInt256.Zero), "nonce reset"); + Assert.That(TestState.IsContract(address), Is.False, "code cleared"); + } +} diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs index 1bcdb50fccbf..26d19f732aba 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs @@ -263,9 +263,10 @@ public static EvmExceptionType InstructionSelfDestruct(executingAccount, inheritor, result); diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index 951537269031..2e652c130e2e 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -313,6 +313,7 @@ private TransactionResult ExecuteEvmTransaction( if (destroyList is not null) { int count = destroyList.Count; + bool removeSelfdestructBurn = spec.IsEip8246Enabled; if (count > 1) { Address[] buffer = SafeArrayPool
.Shared.Rent(count); @@ -320,26 +321,34 @@ private TransactionResult ExecuteEvmTransaction( buffer.AsSpan(0, count).Sort(default(AddressByBytesComparer)); for (int i = 0; i < count; i++) { - FinalizeDestroyedAccount(WorldState, in substate, buffer[i]); + FinalizeDestroyedAccount(WorldState, in substate, buffer[i], removeSelfdestructBurn); } SafeArrayPool
.Shared.Return(buffer); } else if (count == 1) { - FinalizeDestroyedAccount(WorldState, in substate, destroyList.First); + FinalizeDestroyedAccount(WorldState, in substate, destroyList.First, removeSelfdestructBurn); } } - static void FinalizeDestroyedAccount(IWorldState worldState, in TransactionSubstate substate, Address toBeDestroyed) + static void FinalizeDestroyedAccount(IWorldState worldState, in TransactionSubstate substate, Address toBeDestroyed, bool removeSelfdestructBurn) { UInt256 balance = worldState.GetBalance(toBeDestroyed); - if (!balance.IsZero) + // EIP-7708 logs the burn; suppressed once EIP-8246 stops burning. + if (!balance.IsZero && !removeSelfdestructBurn) { substate.Logs.Add(TransferLog.CreateBurn(toBeDestroyed, balance)); } worldState.ClearStorage(toBeDestroyed); worldState.DeleteAccount(toBeDestroyed); + + // EIP-8246: preserve any remaining balance as a fresh nonce-0, + // code-less account; an empty account stays deleted via EIP-161. + if (removeSelfdestructBurn && !balance.IsZero) + { + worldState.CreateAccount(toBeDestroyed, balance); + } } } @@ -1255,23 +1264,30 @@ private int ExecuteEvmCall( if (!deferFinalization && destroyList?.Count > 0) { bool eip7708Enabled = spec.IsEip7708Enabled; + bool removeSelfdestructBurn = spec.IsEip8246Enabled; bool tracingRefunds = tracer.IsTracingRefunds; foreach (Address toBeDestroyed in destroyList) { if (Logger.IsTrace) Logger.Trace($"Destroying account {toBeDestroyed}"); - if (eip7708Enabled) + UInt256 balance = eip7708Enabled || removeSelfdestructBurn ? WorldState.GetBalance(toBeDestroyed) : default; + + // EIP-7708 logs the burn; suppressed once EIP-8246 stops burning. + if (eip7708Enabled && !removeSelfdestructBurn && !balance.IsZero) { - UInt256 balance = WorldState.GetBalance(toBeDestroyed); - if (!balance.IsZero) - { - substate.Logs.Add(TransferLog.CreateSelfDestruct(toBeDestroyed, balance)); - } + substate.Logs.Add(TransferLog.CreateSelfDestruct(toBeDestroyed, balance)); } WorldState.ClearStorage(toBeDestroyed); WorldState.DeleteAccount(toBeDestroyed); + // EIP-8246: preserve any remaining balance as a fresh nonce-0, + // code-less account; an empty account stays deleted via EIP-161. + if (removeSelfdestructBurn && !balance.IsZero) + { + WorldState.CreateAccount(toBeDestroyed, balance); + } + if (tracingRefunds) { tracer.ReportRefund(spec.GasCosts.DestroyRefund); diff --git a/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs b/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs index c52dd71adb36..7628cba8051e 100644 --- a/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs @@ -98,6 +98,7 @@ public class OverridableReleaseSpec(IReleaseSpec spec) : IReleaseSpec public bool IsEip5656Enabled { get; set; } = spec.IsEip5656Enabled; public long Eip2935RingBufferSize { get; set; } = spec.Eip2935RingBufferSize; public bool IsEip6780Enabled { get; set; } = spec.IsEip6780Enabled; + public bool IsEip8246Enabled { get; set; } = spec.IsEip8246Enabled; public bool IsEip4788Enabled { get; set; } = spec.IsEip4788Enabled; public bool IsEip4844FeeCollectorEnabled { get; set; } = spec.IsEip4844FeeCollectorEnabled; public Address? Eip4788ContractAddress { get; set; } = spec.Eip4788ContractAddress; diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs index 986f0da934ac..6143e411c81b 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs @@ -187,6 +187,7 @@ public class ChainParameters public ulong? Eip7708TransitionTimestamp { get; set; } public ulong? Eip8024TransitionTimestamp { get; set; } + public ulong? Eip8246TransitionTimestamp { get; set; } public ulong? Eip7843TransitionTimestamp { get; set; } public ulong? Eip7954TransitionTimestamp { get; set; } public ulong? Eip2780TransitionTimestamp { get; set; } diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs index bcde29f96f95..e837d0ac5b11 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs @@ -326,6 +326,7 @@ protected virtual ReleaseSpec CreateReleaseSpec(ChainSpec chainSpec, long releas releaseSpec.IsEip7825Enabled = (chainSpec.Parameters.Eip7825TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; releaseSpec.IsEip7918Enabled = (chainSpec.Parameters.Eip7918TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; releaseSpec.IsEip8024Enabled = (chainSpec.Parameters.Eip8024TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; + releaseSpec.IsEip8246Enabled = (chainSpec.Parameters.Eip8246TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; bool eip1559FeeCollector = releaseSpec.IsEip1559Enabled && (chainSpec.Parameters.Eip1559FeeCollectorTransition ?? long.MaxValue) <= releaseStartBlock; bool eip4844FeeCollector = releaseSpec.IsEip4844Enabled && (chainSpec.Parameters.Eip4844FeeCollectorTransitionTimestamp ?? long.MaxValue) <= releaseStartTimestamp; diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs index b95736f13381..29d1f341c8b9 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs @@ -216,6 +216,7 @@ bool GetForInnerPathExistence(KeyValuePair o) => Eip7708TransitionTimestamp = chainSpecJson.Params.Eip7708TransitionTimestamp, Eip8024TransitionTimestamp = chainSpecJson.Params.Eip8024TransitionTimestamp, + Eip8246TransitionTimestamp = chainSpecJson.Params.Eip8246TransitionTimestamp, Eip7843TransitionTimestamp = chainSpecJson.Params.Eip7843TransitionTimestamp, Eip7954TransitionTimestamp = chainSpecJson.Params.Eip7954TransitionTimestamp, Eip2780TransitionTimestamp = chainSpecJson.Params.Eip2780TransitionTimestamp, diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs index f01307b19bc7..920062a605ce 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs @@ -194,6 +194,7 @@ public class ChainSpecParamsJson : IHasNamedForks public ulong? Eip7928TransitionTimestamp { get; set; } public ulong? Eip7708TransitionTimestamp { get; set; } public ulong? Eip8024TransitionTimestamp { get; set; } + public ulong? Eip8246TransitionTimestamp { get; set; } public ulong? Eip7843TransitionTimestamp { get; set; } public ulong? Eip7954TransitionTimestamp { get; set; } public ulong? Eip2780TransitionTimestamp { get; set; } diff --git a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs index 11fafa37baf3..8e206d1111e7 100644 --- a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs +++ b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs @@ -21,6 +21,7 @@ public override void Apply(NamedReleaseSpec spec) spec.MaxCodeSize = CodeSizeConstants.MaxCodeSizeEip7954; spec.IsEip8024Enabled = true; spec.IsEip8037Enabled = true; + spec.IsEip8246Enabled = true; } public static IReleaseSpec NoEip8037Instance { get; } = new Amsterdam { IsEip8037Enabled = false }; diff --git a/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs b/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs index b03188dd7bfe..e7d4f9d1d35a 100644 --- a/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs @@ -93,6 +93,7 @@ public class ReleaseSpec : IReleaseSpec public bool IsEip7883Enabled { get; set; } public bool IsEip5656Enabled { get; set; } public bool IsEip6780Enabled { get; set; } + public bool IsEip8246Enabled { get; set; } public bool IsEip4788Enabled { get; set; } public bool IsEip7702Enabled { get; set; } public bool IsEip7823Enabled { get; set; } From 662508faa2736c2d8b4f7e5e0e7e05c9c62aebf3 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Tue, 9 Jun 2026 20:21:32 +0100 Subject: [PATCH 03/39] feat: EIP-2780 charge top-level recipient cold touch and STATE_UPDATE Implements the previously-deferred execution-layer recipient charges so a transaction's total fixed cost matches the EIP-2780 reference-case table (5,000 / 7,100 / 7,756 / 31,756). On top of TX_BASE_COST + transfer log + new-account surcharge, the intrinsic calculation now adds the recipient cold/warm touch (COLD_ACCOUNT_COST_NOCODE 500, COLD_ACCOUNT_COST_CODE 2,600, WARM_STATE_READ 100 when in the access list, including a 7702 delegation target) and the value-transfer STATE_UPDATE (1,000), overriding EIP-2929's "all tx addresses warm" rule. Self-transfers and precompiles are charged zero. The recipient stays pre-warmed, so no opcode re-charges it. Priced where pre-state is available rather than mid-execution to avoid out-of-gas-path surgery; net total gas is equivalent. Adds the contract recipient, access-list warm, and no-transfer reference cases to the tests. Co-Authored-By: Claude Opus 4.8 --- .../Nethermind.Evm.Test/Eip2780Tests.cs | 13 +-- .../IntrinsicGasCalculatorTests.cs | 44 +++++++--- .../GasPolicy/EthereumGasPolicy.cs | 81 ++++++++++++++++--- 3 files changed, 110 insertions(+), 28 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs index 58e2d93ecada..f4bdf7af6d97 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs @@ -77,18 +77,21 @@ private static Task CreateChain() => BasicTestBlockchain.Create(b => b.AddSingleton( new TestSpecProvider(new OverridableReleaseSpec(Prague.Instance) { IsEip2780Enabled = true, IsEip7708Enabled = true }))); - [TestCase(false, GasCostOf.TransactionEip2780 + GasCostOf.TransferLogEip2780, TestName = "existing recipient: base + transfer log (6256)")] - [TestCase(true, GasCostOf.TransactionEip2780 + GasCostOf.TransferLogEip2780 + GasCostOf.NewAccount, TestName = "new recipient: base + log + new-account surcharge (31256)")] - public async Task Simple_value_transfer_spends_eip2780_intrinsic_gas(bool recipientIsNew, long expectedGas) + // Whole-transaction gas (base + recipient cold touch + value STATE_UPDATE + transfer log), + // matching the EIP-2780 reference-case table; recipient AddressF is unfunded (dead per EIP-161). + [TestCase(false, 1ul, GasCostOf.TransactionEip2780 + GasCostOf.ColdAccountAccessNoCodeEip2780 + GasCostOf.StateUpdateEip2780 + GasCostOf.TransferLogEip2780, TestName = "value transfer to existing EOA (7756)")] + [TestCase(true, 1ul, GasCostOf.TransactionEip2780 + GasCostOf.ColdAccountAccessNoCodeEip2780 + GasCostOf.NewAccount + GasCostOf.TransferLogEip2780, TestName = "value transfer to new account (31756)")] + [TestCase(false, 0ul, GasCostOf.TransactionEip2780 + GasCostOf.ColdAccountAccessNoCodeEip2780, TestName = "no-transfer to existing EOA (5000)")] + [TestCase(true, 0ul, GasCostOf.TransactionEip2780 + GasCostOf.ColdAccountAccessNoCodeEip2780, TestName = "no-transfer to empty account (5000)")] + public async Task Simple_transfer_spends_eip2780_total_gas(bool recipientIsNew, ulong value, long expectedGas) { using BasicTestBlockchain chain = await CreateChain(); UInt256 nonce = chain.StateReader.GetNonce(chain.BlockTree.Head!.Header, TestItem.AddressA); - // AddressB is a funded code-less EOA in genesis; AddressF is never funded (dead per EIP-161). Address recipient = recipientIsNew ? TestItem.AddressF : TestItem.AddressB; Transaction tx = Build.A.Transaction .WithTo(recipient) - .WithValue(1.Ether) + .WithValue(value) .WithNonce(nonce) .WithGasLimit(60000) .SignedAndResolved(TestItem.PrivateKeyA) diff --git a/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs b/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs index 4b4213f2685f..3326658cf5da 100644 --- a/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs @@ -279,25 +279,30 @@ public void Eip8037_nongeneric_minimal_gas_is_at_least_regular_plus_state() Assert.That(gas.MinimalGas, Is.GreaterThanOrEqualTo(regularPlusState)); } - // EIP-2780 intrinsic-gas vectors. The new-account case matches the normative pseudocode (31,256); - // the EIP's stated 31,756 total adds a 500 recipient cold-lookup charged at execution time. - public enum Recipient { NewAccount, ExistingEoa, Precompile, SelfTransfer, EmptyZeroValue } + // EIP-2780 total fixed-cost vectors (TX_BASE_COST + transfer log + new-account surcharge + + // recipient cold/warm touch + value STATE_UPDATE), matching the spec's reference-case table. + public enum Recipient { NewAccount, ExistingEoa, Contract, Precompile, SelfTransfer, EmptyZeroValue } - private const long TxBaseEip2780 = GasCostOf.TransactionEip2780; - private const long TransferLogEip2780 = GasCostOf.TransferLogEip2780; + private const long TxBaseEip2780 = GasCostOf.TransactionEip2780; // 4500 + private const long TransferLogEip2780 = GasCostOf.TransferLogEip2780; // 1756 + private const long ColdNoCode = GasCostOf.ColdAccountAccessNoCodeEip2780; // 500 + private const long ColdCode = GasCostOf.ColdAccountAccess; // 2600 + private const long StateUpdate = GasCostOf.StateUpdateEip2780; // 1000 public static IEnumerable Eip2780IntrinsicCases() { - yield return new TestCaseData(Recipient.NewAccount, (UInt256)1, TxBaseEip2780 + TransferLogEip2780 + GasCostOf.NewAccount) - .SetName("Eip2780_intrinsic_value_to_new_account_31256"); + yield return new TestCaseData(Recipient.NewAccount, (UInt256)1, TxBaseEip2780 + ColdNoCode + GasCostOf.NewAccount + TransferLogEip2780) + .SetName("Eip2780_intrinsic_value_to_new_account_31756"); + yield return new TestCaseData(Recipient.ExistingEoa, (UInt256)1, TxBaseEip2780 + ColdNoCode + StateUpdate + TransferLogEip2780) + .SetName("Eip2780_intrinsic_value_to_existing_eoa_7756"); + yield return new TestCaseData(Recipient.Contract, (UInt256)1, TxBaseEip2780 + ColdCode + StateUpdate + TransferLogEip2780) + .SetName("Eip2780_intrinsic_value_to_contract_9856"); yield return new TestCaseData(Recipient.Precompile, (UInt256)1, TxBaseEip2780 + TransferLogEip2780) .SetName("Eip2780_intrinsic_value_to_precompile_6256"); - yield return new TestCaseData(Recipient.ExistingEoa, (UInt256)1, TxBaseEip2780 + TransferLogEip2780) - .SetName("Eip2780_intrinsic_value_to_existing_eoa_6256"); yield return new TestCaseData(Recipient.SelfTransfer, (UInt256)1, TxBaseEip2780) .SetName("Eip2780_intrinsic_self_transfer_4500"); - yield return new TestCaseData(Recipient.EmptyZeroValue, (UInt256)0, TxBaseEip2780) - .SetName("Eip2780_intrinsic_zero_value_to_empty_4500"); + yield return new TestCaseData(Recipient.EmptyZeroValue, (UInt256)0, TxBaseEip2780 + ColdNoCode) + .SetName("Eip2780_intrinsic_no_transfer_to_empty_5000"); } [TestCaseSource(nameof(Eip2780IntrinsicCases))] @@ -316,6 +321,7 @@ public void Eip2780_intrinsic_gas_is_calculated_properly(Recipient recipient, UI IReadOnlyStateProvider state = Substitute.For(); // Only an unfunded recipient is nonexistent per EIP-161 (drives the new-account surcharge). state.IsDeadAccount(Arg.Any
()).Returns(recipient is Recipient.NewAccount); + state.IsContract(Arg.Any
()).Returns(recipient is Recipient.Contract); EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, spec, 0, state); @@ -323,6 +329,22 @@ public void Eip2780_intrinsic_gas_is_calculated_properly(Recipient recipient, UI Assert.That(gas.MinimalGas, Is.EqualTo(Math.Max(expectedStandard, TxBaseEip2780))); } + [Test] + public void Eip2780_recipient_warm_via_access_list_is_charged_warm_read() + { + // EIP-2780 test vector 7: a recipient present in the access list is touched at WARM_STATE_READ. + OverridableReleaseSpec spec = new(Prague.Instance) { IsEip2780Enabled = true }; + AccessList accessList = new AccessList.Builder().AddAddress(TestItem.AddressB).Build(); + Transaction tx = Build.A.Transaction.WithValue(1).WithTo(TestItem.AddressB).WithAccessList(accessList) + .SignedAndResolved(TestItem.PrivateKeyA).TestObject; + IReadOnlyStateProvider state = Substitute.For(); + + long warmTouch = IntrinsicGasCalculator.Calculate(tx, spec, 0, state).Standard; + + long expected = TxBaseEip2780 + GasCostOf.AccessAccountListEntry + GasCostOf.WarmStateRead + StateUpdate + TransferLogEip2780; + Assert.That(warmTouch, Is.EqualTo(expected)); + } + [Test] public void Eip2780_intrinsic_gas_for_create_charges_transfer_log_only_when_value_positive() { diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 8aa6d1c5f55b..8685619487fe 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -4,6 +4,7 @@ using System; using System.Runtime.CompilerServices; using Nethermind.Core; +using Nethermind.Core.Eip2930; using Nethermind.Core.Specs; using Nethermind.Evm.State; using Nethermind.Int256; @@ -482,7 +483,7 @@ public static IntrinsicGas CalculateIntrinsicGas(Transaction + CreateCost(tx, spec) + accessListCost + authRegularCost - + Eip2780Surcharges(tx, spec, worldState); + + Eip2780ExtraGas(tx, spec, worldState); long floorCost = IGasPolicy.CalculateFloorCost(tx, spec, tokensInCallData, floorTokensInAccessList); long createStateCost = CreateStateCost(tx, spec); long totalStateCost = authStateCost + createStateCost; @@ -536,26 +537,82 @@ private static long DataCost(Transaction tx, IReleaseSpec spec, long tokensInCal spec.GetBaseDataCost(tx) + tokensInCallData * GasCostOf.TxDataZero; /// - /// EIP-2780 intrinsic surcharges: the EIP-7708 transfer-log cost and the new-account surcharge. - /// Mirrors the normative pseudocode; the recipient cold-touch is charged at execution, not here. + /// EIP-2780 charges on top of TX_BASE_COST: the EIP-7708 transfer log, the new-account surcharge, + /// and the recipient cold/warm touch plus its value-transfer STATE_UPDATE. /// - private static long Eip2780Surcharges(Transaction tx, IReleaseSpec spec, IReadOnlyStateProvider? worldState) + /// + /// The recipient touch overrides EIP-2929's "all tx addresses are warm" rule. It is priced here + /// (where pre-state is available) rather than mid-execution; the recipient remains pre-warmed, so + /// no opcode re-charges it. Requires for the state-dependent parts. + /// + private static long Eip2780ExtraGas(Transaction tx, IReleaseSpec spec, IReadOnlyStateProvider? worldState) { - if (!spec.IsEip2780Enabled || tx.Value.IsZero) return 0; + if (!spec.IsEip2780Enabled) return 0; - long cost = 0; - Address? to = tx.To; bool isCreate = tx.IsContractCreation; + Address? to = tx.To; + bool hasValue = !tx.Value.IsZero; + bool senderIsRecipient = !isCreate && tx.SenderAddress == to; - // CREATE endows a freshly computed address distinct from the sender, so the transfer log - // applies whenever value > 0; otherwise only when the recipient differs from the sender. - if (isCreate || tx.SenderAddress != to) + long cost = 0; + // EIP-7708 transfer log on the top-level value transfer; CREATE endows a distinct address. + if (hasValue && (isCreate || !senderIsRecipient)) cost += GasCostOf.TransferLogEip2780; - // New-account surcharge: non-create value transfer to a nonexistent, non-precompile recipient. - if (!isCreate && to is not null && worldState is not null && !spec.IsPrecompile(to) && worldState.IsDeadAccount(to)) + if (isCreate || to is null || worldState is null) return cost; + + bool isPrecompile = spec.IsPrecompile(to); + bool recipientDead = worldState.IsDeadAccount(to); + + // New-account surcharge: value transfer to a nonexistent, non-precompile recipient. + if (hasValue && !isPrecompile && recipientDead) cost += GasCostOf.NewAccount; + // Self-transfers coalesce into the sender leaf write already priced into TX_BASE_COST; + // precompiles are warm at tx start and charged zero. + if (!senderIsRecipient && !isPrecompile) + { + cost += RecipientTouchCost(tx, spec, worldState, to); + // The new-account surcharge already covers the recipient leaf write. + if (hasValue && !recipientDead) + cost += GasCostOf.StateUpdateEip2780; + } + + return cost; + } + + private static long RecipientTouchCost(Transaction tx, IReleaseSpec spec, IReadOnlyStateProvider worldState, Address to) + { + long cost = InAccessList(tx, to) + ? GasCostOf.WarmStateRead + : worldState.IsContract(to) ? GasCostOf.ColdAccountAccess : GasCostOf.ColdAccountAccessNoCodeEip2780; + + // EIP-7702: a delegated recipient also touches its delegation target (always carries code). + if (spec.IsEip7702Enabled && TryGetDelegationTarget(worldState, to, out Address target)) + cost += InAccessList(tx, target) ? GasCostOf.WarmStateRead : GasCostOf.ColdAccountAccess; + return cost; } + + private static bool InAccessList(Transaction tx, Address address) + { + if (tx.AccessList is null) return false; + foreach ((Address entry, _) in tx.AccessList) + { + if (entry == address) return true; + } + return false; + } + + private static bool TryGetDelegationTarget(IReadOnlyStateProvider worldState, Address to, out Address target) + { + byte[]? code = worldState.GetCode(to); + if (code is not null && Eip7702Constants.IsDelegatedCode(code)) + { + target = new Address(code[Eip7702Constants.DelegationHeader.Length..]); + return true; + } + target = null!; + return false; + } } From d19f08a846fea82036fd8b1d218211f3feb7ed9e Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 10 Jun 2026 00:58:39 +0100 Subject: [PATCH 04/39] test: cover deferred finalization path; address review feedback - Add an EIP-8037 + EIP-7708 fixture dimension so every EIP-8246 scenario also runs through the deferred FinalizeDestroyedAccount path (as in Amsterdam), covering the balance-preservation logic there. - Add a CREATE2 redeployment test: a factory re-creates a self-destructed child at the same address across transactions, verifying the nonce reset keeps redeployment unblocked and the preserved balance accumulates. - Report the destroy refund to the tracer in the deferred path, matching the inline path. - Document the pre-existing Burn-vs-SelfDestruct log distinction between the post-fee and pre-fee finalization paths. Co-Authored-By: Claude Opus 4.8 --- .../Nethermind.Evm.Test/Eip8246Tests.cs | 81 ++++++++++++++++--- .../TransactionProcessor.cs | 9 ++- 2 files changed, 77 insertions(+), 13 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip8246Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip8246Tests.cs index 4a4da53fc84e..0eb55ffaa9e6 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Eip8246Tests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Eip8246Tests.cs @@ -24,21 +24,32 @@ namespace Nethermind.Evm.Test; /// A resulting zero-balance account is still removed as empty per EIP-161. ///
/// -/// The true fixture runs with EIP-8246 enabled; the false fixture keeps the -/// pre-8246 (EIP-6780-only) behaviour as a baseline so every test pins both sides of the change. +/// The first fixture argument toggles EIP-8246 (the false fixtures keep the pre-8246, +/// EIP-6780-only baseline). The second toggles EIP-8037 + EIP-7708, which routes destruction +/// through the deferred FinalizeDestroyedAccount path (as in Amsterdam) instead of the +/// inline path; every scenario therefore runs against both finalization paths. /// -[TestFixture(true)] -[TestFixture(false)] -public class Eip8246Tests(bool eip8246Enabled) : VirtualMachineTestsBase +[TestFixture(true, false)] +[TestFixture(false, false)] +[TestFixture(true, true)] +[TestFixture(false, true)] +public class Eip8246Tests(bool eip8246Enabled, bool deferredFinalization) : VirtualMachineTestsBase { private readonly ISpecProvider _specProvider = - new TestSpecProvider(new OverridableReleaseSpec(Cancun.Instance) { IsEip8246Enabled = eip8246Enabled }); + new TestSpecProvider(new OverridableReleaseSpec(Cancun.Instance) + { + IsEip8246Enabled = eip8246Enabled, + // EIP-8037 + EIP-7708 together select the deferred finalization path. + IsEip8037Enabled = deferredFinalization, + IsEip7708Enabled = deferredFinalization, + }); protected override long BlockNumber => MainnetSpecProvider.ParisBlockNumber; protected override ulong Timestamp => MainnetSpecProvider.CancunBlockTimestamp; protected override ISpecProvider SpecProvider => _specProvider; - private const long GasLimit = 1_000_000; + // Generous limit so the EIP-8037 state-byte charges in the deferred-path fixtures don't run out of gas. + private const long GasLimit = 5_000_000; private static readonly byte[] Salt = new UInt256(123).ToBigEndian(); private EthereumEcdsa _ecdsa; @@ -74,7 +85,7 @@ public void Same_tx_self_destruct_to_self_does_not_burn(int balanceEther) byte[] code = Prepare.EvmCode .Create2(_selfDestructToSelfInit, Salt, balance) - .Call(child, 100000) + .Call(child, 500_000) .STOP() .Done; @@ -118,8 +129,8 @@ public void Same_tx_created_then_receives_eth_does_not_burn() byte[] contractBCode = Prepare.EvmCode .Create(initCodeA, initialBalance) - .Call(contractA, 100000) // triggers selfdestruct-to-inheritor - .CallWithValue(contractA, 100000, ethReceivedAfter) // sends ETH after selfdestruct + .Call(contractA, 500_000) // triggers selfdestruct-to-inheritor + .CallWithValue(contractA, 500_000, ethReceivedAfter) // sends ETH after selfdestruct .STOP() .Done; @@ -153,7 +164,7 @@ public void Same_tx_self_destruct_to_other_still_transfers() byte[] code = Prepare.EvmCode .Create2(init, Salt, 5.Ether) - .Call(child, 100000) + .Call(child, 500_000) .STOP() .Done; @@ -173,7 +184,7 @@ public void Self_destruct_to_self_not_in_same_tx_is_unchanged_no_op() Transaction deployTx = Build.A.Transaction.WithCode(init).WithValue(7.Ether) .WithGasLimit(GasLimit).SignedAndResolved(_ecdsa, TestItem.PrivateKeyA).TestObject; - byte[] call = Prepare.EvmCode.Call(contract, 100000).STOP().Done; + byte[] call = Prepare.EvmCode.Call(contract, 500_000).STOP().Done; Transaction callTx = Build.A.Transaction.WithCode(call).WithGasLimit(GasLimit) .WithNonce(1).SignedAndResolved(_ecdsa, TestItem.PrivateKeyA).TestObject; @@ -188,6 +199,52 @@ public void Self_destruct_to_self_not_in_same_tx_is_unchanged_no_op() Assert.That(TestState.IsContract(contract), Is.True); } + [Test] + public void Create2_redeploy_to_same_address_unblocked_after_self_destruct() + { + // A factory CREATE2s a child whose init code self-destructs to itself, so the child is + // created and destroyed within the same transaction. The factory is called twice and + // hits the same CREATE2 address both times. Under EIP-8246 the child survives as a + // nonce-0, code-less, balance-only account; because the nonce is reset, the second + // CREATE2 is not blocked and its endowment accumulates onto the preserved balance. + UInt256 endowment = 1.Ether; + + Address factory = ContractAddress.From(TestItem.PrivateKeyA.Address, 0); + Address child = ContractAddress.From(factory, Salt, _selfDestructToSelf); + + byte[] factoryRuntime = Prepare.EvmCode + .Create2(_selfDestructToSelf, Salt, endowment) + .STOP() + .Done; + byte[] factoryInit = Prepare.EvmCode.ForInitOf(factoryRuntime).Done; + + Transaction deploy = Build.A.Transaction.WithCode(factoryInit).WithValue(5.Ether) + .WithNonce(0).WithGasLimit(GasLimit).SignedAndResolved(_ecdsa, TestItem.PrivateKeyA).TestObject; + Transaction call1 = Build.A.Transaction.WithTo(factory) + .WithNonce(1).WithGasLimit(GasLimit).SignedAndResolved(_ecdsa, TestItem.PrivateKeyA).TestObject; + Transaction call2 = Build.A.Transaction.WithTo(factory) + .WithNonce(2).WithGasLimit(GasLimit).SignedAndResolved(_ecdsa, TestItem.PrivateKeyA).TestObject; + + Block block = Build.A.Block.WithNumber(BlockNumber).WithTimestamp(Timestamp) + .WithTransactions(deploy, call1, call2).WithGasLimit(4 * GasLimit).TestObject; + BlockExecutionContext blCtx = new(block.Header, SpecProvider.GetSpec(block.Header)); + + _processor.Execute(deploy, blCtx, NullTxTracer.Instance); + _processor.Execute(call1, blCtx, NullTxTracer.Instance); + _processor.Execute(call2, blCtx, NullTxTracer.Instance); + + if (eip8246Enabled) + { + // Both redeployments funded the same preserved address: 2 x endowment. + AssertBalanceOnly(child, 2.Ether); + } + else + { + // Pre-8246 the self-burn empties the child on each pass, so it ends deleted. + Assert.That(TestState.AccountExists(child), Is.False); + } + } + private void ExecuteTopLevel(byte[] code, UInt256 value) { Transaction tx = Build.A.Transaction.WithCode(code).WithValue(value) diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index 2e652c130e2e..6a82e3779855 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -314,6 +314,8 @@ private TransactionResult ExecuteEvmTransaction( { int count = destroyList.Count; bool removeSelfdestructBurn = spec.IsEip8246Enabled; + bool tracingRefunds = tracer.IsTracingRefunds; + long destroyRefund = spec.GasCosts.DestroyRefund; if (count > 1) { Address[] buffer = SafeArrayPool
.Shared.Rent(count); @@ -322,19 +324,24 @@ private TransactionResult ExecuteEvmTransaction( for (int i = 0; i < count; i++) { FinalizeDestroyedAccount(WorldState, in substate, buffer[i], removeSelfdestructBurn); + if (tracingRefunds) tracer.ReportRefund(destroyRefund); } SafeArrayPool
.Shared.Return(buffer); } else if (count == 1) { FinalizeDestroyedAccount(WorldState, in substate, destroyList.First, removeSelfdestructBurn); + if (tracingRefunds) tracer.ReportRefund(destroyRefund); } } static void FinalizeDestroyedAccount(IWorldState worldState, in TransactionSubstate substate, Address toBeDestroyed, bool removeSelfdestructBurn) { UInt256 balance = worldState.GetBalance(toBeDestroyed); - // EIP-7708 logs the burn; suppressed once EIP-8246 stops burning. + // Pre-EIP-8246 this is a burn. This post-fee path emits a Burn log (the whole + // balance, including priority fees credited at PayFees, leaves supply), whereas + // the pre-fee path emits a SelfDestruct log. EIP-8246 removes the burn entirely, + // so no log is emitted on either path. if (!balance.IsZero && !removeSelfdestructBurn) { substate.Logs.Add(TransferLog.CreateBurn(toBeDestroyed, balance)); From e881dc504cf0a5d93b51e44033d035e57bcc7176 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 10 Jun 2026 01:17:55 +0100 Subject: [PATCH 05/39] test: address EIP-2780 review comments - Lazy-cache SpecGasCosts in OverridableReleaseSpec to avoid allocating on every hot-path access, mirroring ReleaseSpec. - Document on IReleaseSpec.IsEip2780Enabled that it must be co-activated with EIP-7708, and enable EIP-7708 in the intrinsic tests for a realistic config. - Note in SELFDESTRUCT that EIP-2780 reprices only the CALL value tiers; the EIP-7708 log cost is EIP-7708's responsibility, not pre-charged there. - Add EVM-opcode-level tests (Eip2780VmTests): two-tier cold account access via BALANCE, the CALL new-account vs existing value tier, and the CALLCODE self-call value tier. Co-Authored-By: Claude Opus 4.8 --- .../Nethermind.Core/Specs/IReleaseSpec.cs | 4 + .../Nethermind.Evm.Test/Eip2780VmTests.cs | 85 +++++++++++++++++++ .../IntrinsicGasCalculatorTests.cs | 8 +- .../EvmInstructions.ControlFlow.cs | 3 +- .../OverridableReleaseSpec.cs | 3 +- 5 files changed, 97 insertions(+), 6 deletions(-) create mode 100644 src/Nethermind/Nethermind.Evm.Test/Eip2780VmTests.cs diff --git a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs index 7efcbff77029..88cc01b2f965 100644 --- a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs @@ -479,6 +479,10 @@ public interface IReleaseSpec : IEip1559Spec, IReceiptSpec /// EIP-2780: Reduce intrinsic transaction gas (TX_BASE_COST) and reprice value-transfer /// and cold-account costs against actual state work. /// + /// + /// Must be co-activated with EIP-7708: the value-transfer cost includes the transfer-log + /// charge, so enabling EIP-2780 without EIP-7708 would price a log that is never emitted. + /// public bool IsEip2780Enabled { get; } /// diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip2780VmTests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip2780VmTests.cs new file mode 100644 index 000000000000..5a42e9cadb6f --- /dev/null +++ b/src/Nethermind/Nethermind.Evm.Test/Eip2780VmTests.cs @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using Nethermind.Core.Extensions; +using Nethermind.Core.Specs; +using Nethermind.Core.Test.Builders; +using Nethermind.Evm.State; +using Nethermind.Specs; +using Nethermind.Specs.Forks; +using Nethermind.Specs.Test; +using NUnit.Framework; + +namespace Nethermind.Evm.Test; + +/// +/// EVM-opcode-level coverage for EIP-2780 repricing. Each test is a differential between two runs +/// that differ only in the operation under test, so the (identical) intrinsic and recipient costs +/// cancel and the assertion isolates the EIP-2780 delta. +/// +public class Eip2780VmTests : VirtualMachineTestsBase +{ + protected override ISpecProvider SpecProvider { get; } = + new TestSpecProvider(new OverridableReleaseSpec(Prague.Instance) { IsEip2780Enabled = true, IsEip7708Enabled = true }); + + private long GasSpent(byte[] code) + { + TestAllTracerWithOutput result = Execute(code); + Assert.That(result.StatusCode, Is.EqualTo(StatusCode.Success), result.Error); + return result.GasSpent; + } + + // Gas charged at the given opcode's step, per the Geth-style trace. + private long OpCost(string opcode, byte[] code) + { + foreach (global::Nethermind.Blockchain.Tracing.GethStyle.GethTxTraceEntry e in ExecuteAndTrace(code).Entries) + { + if (e.Opcode == opcode) return e.GasCost; + } + return -1; + } + + [Test] + public void Cold_account_access_via_balance_is_two_tier() + { + Address codeless = TestItem.AddressC; // exists, no code -> COLD_ACCOUNT_COST_NOCODE + Address withCode = TestItem.AddressF; // has code -> COLD_ACCOUNT_COST_CODE + TestState.CreateAccount(codeless, 1.Ether); + TestState.CreateAccount(withCode, 1.Ether); + TestState.InsertCode(withCode, Prepare.EvmCode.Op(Instruction.STOP).Done, Spec); + + long codelessCost = OpCost("BALANCE", Prepare.EvmCode.PushData(codeless).Op(Instruction.BALANCE).STOP().Done); + long withCodeCost = OpCost("BALANCE", Prepare.EvmCode.PushData(withCode).Op(Instruction.BALANCE).STOP().Done); + + Assert.That((codelessCost, withCodeCost), Is.EqualTo((GasCostOf.ColdAccountAccessNoCodeEip2780, GasCostOf.ColdAccountAccess))); + } + + [Test] + public void Call_value_cost_new_account_tier_is_24000_above_existing_tier() + { + Address existing = TestItem.AddressC; // exists -> CallValueExistingEip2780 (3756) + Address newAccount = TestItem.AddressF; // dead -> CallValueNewAccountEip2780 (27756) + TestState.CreateAccount(existing, 1.Ether); + + long existingGas = GasSpent(Prepare.EvmCode.CallWithValue(existing, 50000, 1).STOP().Done); + long newAccountGas = GasSpent(Prepare.EvmCode.CallWithValue(newAccount, 50000, 1).STOP().Done); + + Assert.That(newAccountGas - existingGas, Is.EqualTo(GasCostOf.CallValueNewAccountEip2780 - GasCostOf.CallValueExistingEip2780)); + } + + [Test] + public void Callcode_with_value_is_charged_self_call_tier() + { + // CALLCODE keeps caller == target (the executing account), so any value transfer is a + // self-call priced at a single STATE_UPDATE (1000) instead of the legacy CallValue (9000). + Address codeSource = TestItem.AddressC; + TestState.CreateAccount(codeSource, 1.Ether); + TestState.InsertCode(codeSource, Prepare.EvmCode.Op(Instruction.STOP).Done, Spec); + + long noValueOp = OpCost("CALLCODE", Prepare.EvmCode.CallCode(codeSource, 50000, 0).STOP().Done); + long withValueOp = OpCost("CALLCODE", Prepare.EvmCode.CallCode(codeSource, 50000, 1).STOP().Done); + + Assert.That(withValueOp - noValueOp, Is.EqualTo(GasCostOf.CallValueSelfEip2780)); + } +} diff --git a/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs b/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs index 3326658cf5da..e31ba2ba44cd 100644 --- a/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs @@ -308,7 +308,7 @@ public static IEnumerable Eip2780IntrinsicCases() [TestCaseSource(nameof(Eip2780IntrinsicCases))] public void Eip2780_intrinsic_gas_is_calculated_properly(Recipient recipient, UInt256 value, long expectedStandard) { - OverridableReleaseSpec spec = new(Prague.Instance) { IsEip2780Enabled = true }; + OverridableReleaseSpec spec = new(Prague.Instance) { IsEip2780Enabled = true, IsEip7708Enabled = true }; Address to = recipient switch { Recipient.Precompile => Address.FromNumber(1), // 0x01 ECRECOVER precompile @@ -333,7 +333,7 @@ public void Eip2780_intrinsic_gas_is_calculated_properly(Recipient recipient, UI public void Eip2780_recipient_warm_via_access_list_is_charged_warm_read() { // EIP-2780 test vector 7: a recipient present in the access list is touched at WARM_STATE_READ. - OverridableReleaseSpec spec = new(Prague.Instance) { IsEip2780Enabled = true }; + OverridableReleaseSpec spec = new(Prague.Instance) { IsEip2780Enabled = true, IsEip7708Enabled = true }; AccessList accessList = new AccessList.Builder().AddAddress(TestItem.AddressB).Build(); Transaction tx = Build.A.Transaction.WithValue(1).WithTo(TestItem.AddressB).WithAccessList(accessList) .SignedAndResolved(TestItem.PrivateKeyA).TestObject; @@ -348,7 +348,7 @@ public void Eip2780_recipient_warm_via_access_list_is_charged_warm_read() [Test] public void Eip2780_intrinsic_gas_for_create_charges_transfer_log_only_when_value_positive() { - OverridableReleaseSpec spec = new(Prague.Instance) { IsEip2780Enabled = true }; + OverridableReleaseSpec spec = new(Prague.Instance) { IsEip2780Enabled = true, IsEip7708Enabled = true }; IReadOnlyStateProvider state = Substitute.For(); Transaction createZero = Build.A.Transaction.WithValue(0).WithCode(Array.Empty()) @@ -367,7 +367,7 @@ public void Eip2780_intrinsic_gas_for_create_charges_transfer_log_only_when_valu public void Eip2780_reduces_the_calldata_floor_base() { // Without reducing the floor base, the legacy 21,000 floor would dominate and negate the EIP. - OverridableReleaseSpec spec = new(Prague.Instance) { IsEip2780Enabled = true }; + OverridableReleaseSpec spec = new(Prague.Instance) { IsEip2780Enabled = true, IsEip7708Enabled = true }; Transaction tx = Build.A.Transaction.WithData([1]).SignedAndResolved().TestObject; EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, spec); diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs index 1bcdb50fccbf..e1358f4b824c 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs @@ -241,7 +241,8 @@ public static EvmExceptionType InstructionSelfDestruct spec.IsEip7843Enabled; public bool IsEip7954Enabled { get; set; } = spec.IsEip7954Enabled; public bool IsEip2780Enabled { get; set; } = spec.IsEip2780Enabled; - public SpecGasCosts GasCosts => new(this); + private SpecGasCosts? _gasCosts; + public SpecGasCosts GasCosts => _gasCosts ??= new(this); FrozenSet IReleaseSpec.Precompiles => spec.Precompiles; } } From 908fb596fb069f510b7b7f7cb5aad45582cb8739 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 10 Jun 2026 01:42:51 +0100 Subject: [PATCH 06/39] fix: don't cache GasCosts in OverridableReleaseSpec The earlier lazy-cache (from a review suggestion) broke ChainSpecTest, which mutates an OverridableReleaseSpec's EIP flags after construction and expects GasCosts to reflect them. OverridableReleaseSpec is a mutable test-only helper, not an EVM hot-path type, so per-access computation is correct here. Co-Authored-By: Claude Opus 4.8 --- .../Nethermind.Specs.Test/OverridableReleaseSpec.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs b/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs index a16ca5cd1186..9059eb528a22 100644 --- a/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs @@ -129,8 +129,8 @@ public class OverridableReleaseSpec(IReleaseSpec spec) : IReleaseSpec public bool IsEip7843Enabled => spec.IsEip7843Enabled; public bool IsEip7954Enabled { get; set; } = spec.IsEip7954Enabled; public bool IsEip2780Enabled { get; set; } = spec.IsEip2780Enabled; - private SpecGasCosts? _gasCosts; - public SpecGasCosts GasCosts => _gasCosts ??= new(this); + // Not cached: this test spec is mutated after construction, so GasCosts must reflect current flags. + public SpecGasCosts GasCosts => new(this); FrozenSet IReleaseSpec.Precompiles => spec.Precompiles; } } From 9cc7bf0906d1cb88c9bd6437cdc154d4bedd71e8 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:04:49 +0100 Subject: [PATCH 07/39] fix: do not enable EIP-8246 in Amsterdam fork EIP-8246 is a Draft and is not part of the EEST `for_amsterdam` conformance fixtures. Enabling it in the Amsterdam fork changed SELFDESTRUCT behaviour for that fork, diverging from the reference post-states and failing the Pyspec tests across all shards. Keep the full implementation and spec/chainspec plumbing so the EIP can be activated via the Eip8246Transition chainspec parameter once it is scheduled, but leave it disabled in the named forks. Unit tests toggle the flag directly via OverridableReleaseSpec, so coverage is unaffected. Co-Authored-By: Claude Opus 4.8 --- src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs index 8e206d1111e7..da51591aa6d9 100644 --- a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs +++ b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs @@ -21,7 +21,9 @@ public override void Apply(NamedReleaseSpec spec) spec.MaxCodeSize = CodeSizeConstants.MaxCodeSizeEip7954; spec.IsEip8024Enabled = true; spec.IsEip8037Enabled = true; - spec.IsEip8246Enabled = true; + // EIP-8246 is implemented but stays off here: it is still a Draft and not part of the + // EEST `for_amsterdam` fixtures, so enabling it would diverge from conformance tests. + // It can be activated via the Eip8246Transition chainspec parameter when scheduled. } public static IReleaseSpec NoEip8037Instance { get; } = new Amsterdam { IsEip8037Enabled = false }; From 0f12809d67743852c0caf647db3f50abec611c36 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 10 Jun 2026 12:28:16 +0100 Subject: [PATCH 08/39] refactor: address EIP-2780 review follow-ups - Build the access-list address set once (IReadOnlySet) instead of rescanning tx.AccessList per lookup in the recipient-touch pricing. - Reuse ICodeInfoRepository.TryGetDelegatedAddress instead of re-implementing the EIP-7702 designator parsing. - Add a test proving a top-level delegated recipient charges the delegation target's cold touch exactly once: the EVM warms (does not gas-charge) the target for the top-level frame, so the intrinsic charge is the sole one and there is no double-charge. Co-Authored-By: Claude Opus 4.8 --- .../Nethermind.Evm.Test/Eip2780VmTests.cs | 17 +++++++++ .../GasPolicy/EthereumGasPolicy.cs | 35 +++++++------------ 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip2780VmTests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip2780VmTests.cs index 5a42e9cadb6f..471ab633ea01 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Eip2780VmTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Eip2780VmTests.cs @@ -82,4 +82,21 @@ public void Callcode_with_value_is_charged_self_call_tier() Assert.That(withValueOp - noValueOp, Is.EqualTo(GasCostOf.CallValueSelfEip2780)); } + + [Test] + public void Delegated_recipient_charges_delegation_target_cold_touch_once() + { + // A delegated recipient pays COLD_ACCOUNT_COST_CODE for itself and its delegation target. + // The EVM only warms (does not gas-charge) the target for the top-level frame, so the total + // exceeds a plain-contract recipient by exactly one cold-code touch, not two (no double-charge). + Address target = TestItem.AddressC; + TestState.CreateAccount(target, 1.Ether); + TestState.InsertCode(target, Prepare.EvmCode.Op(Instruction.STOP).Done, Spec); + byte[] delegated = [.. Eip7702Constants.DelegationHeader, .. target.Bytes]; + + long delegatedGas = GasSpent(delegated); + long plainContractGas = GasSpent(Prepare.EvmCode.Op(Instruction.STOP).Done); + + Assert.That(delegatedGas - plainContractGas, Is.EqualTo(GasCostOf.ColdAccountAccess)); + } } diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 8685619487fe..00d12e3d05a6 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; using Nethermind.Core; using Nethermind.Core.Eip2930; @@ -572,7 +573,7 @@ private static long Eip2780ExtraGas(Transaction tx, IReleaseSpec spec, IReadOnly // precompiles are warm at tx start and charged zero. if (!senderIsRecipient && !isPrecompile) { - cost += RecipientTouchCost(tx, spec, worldState, to); + cost += RecipientTouchCost(spec, worldState, to, AccessListAddresses(tx)); // The new-account surcharge already covers the recipient leaf write. if (hasValue && !recipientDead) cost += GasCostOf.StateUpdateEip2780; @@ -581,38 +582,28 @@ private static long Eip2780ExtraGas(Transaction tx, IReleaseSpec spec, IReadOnly return cost; } - private static long RecipientTouchCost(Transaction tx, IReleaseSpec spec, IReadOnlyStateProvider worldState, Address to) + private static long RecipientTouchCost(IReleaseSpec spec, IReadOnlyStateProvider worldState, Address to, IReadOnlySet
? accessList) { - long cost = InAccessList(tx, to) + long cost = accessList?.Contains(to) == true ? GasCostOf.WarmStateRead : worldState.IsContract(to) ? GasCostOf.ColdAccountAccess : GasCostOf.ColdAccountAccessNoCodeEip2780; // EIP-7702: a delegated recipient also touches its delegation target (always carries code). - if (spec.IsEip7702Enabled && TryGetDelegationTarget(worldState, to, out Address target)) - cost += InAccessList(tx, target) ? GasCostOf.WarmStateRead : GasCostOf.ColdAccountAccess; + // The EVM warms (does not gas-charge) this target for the top-level frame, so this is the sole charge. + if (spec.IsEip7702Enabled && ICodeInfoRepository.TryGetDelegatedAddress(worldState.GetCode(to).AsSpan(), out Address? target)) + cost += accessList?.Contains(target) == true ? GasCostOf.WarmStateRead : GasCostOf.ColdAccountAccess; return cost; } - private static bool InAccessList(Transaction tx, Address address) + private static IReadOnlySet
? AccessListAddresses(Transaction tx) { - if (tx.AccessList is null) return false; - foreach ((Address entry, _) in tx.AccessList) + if (tx.AccessList is null) return null; + HashSet
set = []; + foreach ((Address address, _) in tx.AccessList) { - if (entry == address) return true; + set.Add(address); } - return false; - } - - private static bool TryGetDelegationTarget(IReadOnlyStateProvider worldState, Address to, out Address target) - { - byte[]? code = worldState.GetCode(to); - if (code is not null && Eip7702Constants.IsDelegatedCode(code)) - { - target = new Address(code[Eip7702Constants.DelegationHeader.Length..]); - return true; - } - target = null!; - return false; + return set; } } From d7596378c167e950194d058c7712afd020ecd99f Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:47:24 +0100 Subject: [PATCH 09/39] feat: implement EIP-8038 (State-access Gas Cost Update) Lays down the structure for EIP-8038 on top of the EIP-8037 two-dimensional gas model. The EIP is a Draft with all repriced values still TBD and no test vectors, so the new constants are placeholders equal to the current costs and the EIP is not enabled in any fork. - Add Eip8038Constants capturing the full parameter set and the spec's derivation formulas (CALL_VALUE, CREATE_ACCESS, access-list costs, STORAGE_CLEAR_REFUND), with base values as documented placeholders. - Wire IsEip8038Enabled through IReleaseSpec / ReleaseSpec / decorator / OverridableReleaseSpec / chainspec (Eip8038TransitionTimestamp). - Gate the two rules that are well-defined observable deltas at placeholder values: EXTCODESIZE/EXTCODECOPY charge an extra WARM_ACCESS, and access-list entry costs rise to the cold-access costs they pre-warm. Route cold account/storage access through the EIP-8038 constants behind the flag. Adds Eip8038ConstantsTests pinning every derivation relationship, plus Eip8038Tests/Eip8038IntrinsicGasTests covering the EXT* extra access and the access-list cost change with the EIP on and off. Co-Authored-By: Claude Opus 4.8 --- .../Eip8038ConstantsTests.cs | 92 ++++++++++++ .../Nethermind.Core/Eip8038Constants.cs | 54 +++++++ .../Nethermind.Core/Specs/IReleaseSpec.cs | 5 + .../Specs/ReleaseSpecDecorator.cs | 1 + .../Nethermind.Evm.Test/Eip8038Tests.cs | 139 ++++++++++++++++++ .../GasPolicy/EthereumGasPolicy.cs | 7 +- .../Nethermind.Evm/GasPolicy/IGasPolicy.cs | 7 +- .../Instructions/EvmInstructions.CodeCopy.cs | 8 + .../OverridableReleaseSpec.cs | 1 + .../ChainSpecStyle/ChainParameters.cs | 1 + .../ChainSpecBasedSpecProvider.cs | 1 + .../ChainSpecStyle/ChainSpecLoader.cs | 1 + .../Json/ChainSpecParamsJson.cs | 1 + .../Nethermind.Specs/ReleaseSpec.cs | 1 + 14 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs create mode 100644 src/Nethermind/Nethermind.Core/Eip8038Constants.cs create mode 100644 src/Nethermind/Nethermind.Evm.Test/Eip8038Tests.cs diff --git a/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs b/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs new file mode 100644 index 000000000000..8ae66c23c393 --- /dev/null +++ b/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using NUnit.Framework; + +namespace Nethermind.Core.Test; + +/// +/// Pins the EIP-8038 gas parameters to the spec's derivation formulas. The base values are +/// placeholders equal to the current (pre-8038) costs while the EIP is a Draft; these tests +/// guard the relationships so the derived values stay correct when the final figures land. +/// +public class Eip8038ConstantsTests +{ + [Test] + public void Base_parameters_match_their_current_placeholder_values() + { + long coldAccountAccess = Eip8038Constants.ColdAccountAccess; + long warmAccess = Eip8038Constants.WarmAccess; + long coldStorageAccess = Eip8038Constants.ColdStorageAccess; + long accountWrite = Eip8038Constants.AccountWrite; + long storageWrite = Eip8038Constants.StorageWrite; + long callStipend = Eip8038Constants.CallStipend; + + Assert.Multiple(() => + { + Assert.That(coldAccountAccess, Is.EqualTo(2600)); + Assert.That(warmAccess, Is.EqualTo(100)); + Assert.That(coldStorageAccess, Is.EqualTo(2100)); + Assert.That(accountWrite, Is.EqualTo(6700)); + Assert.That(storageWrite, Is.EqualTo(2800)); + Assert.That(callStipend, Is.EqualTo(2300)); + }); + } + + [Test] + public void Account_write_is_call_value_minus_stipend() + { + long accountWrite = Eip8038Constants.AccountWrite; + Assert.That(accountWrite, Is.EqualTo(GasCostOf.CallValue - GasCostOf.CallStipend)); + } + + [Test] + public void Call_value_is_account_write_plus_stipend() + { + long callValue = Eip8038Constants.CallValue; + Assert.That(callValue, Is.EqualTo(Eip8038Constants.AccountWrite + Eip8038Constants.CallStipend)); + } + + [Test] + public void Create_access_is_account_write_plus_cold_storage_access() + { + long createAccess = Eip8038Constants.CreateAccess; + Assert.That(createAccess, Is.EqualTo(Eip8038Constants.AccountWrite + Eip8038Constants.ColdStorageAccess)); + } + + [Test] + public void Access_list_address_cost_equals_cold_account_access() + { + long addressCost = Eip8038Constants.AccessListAddressCost; + Assert.That(addressCost, Is.EqualTo(Eip8038Constants.ColdAccountAccess)); + } + + [Test] + public void Access_list_storage_key_cost_equals_cold_storage_access() + { + long storageKeyCost = Eip8038Constants.AccessListStorageKeyCost; + Assert.That(storageKeyCost, Is.EqualTo(Eip8038Constants.ColdStorageAccess)); + } + + [Test] + public void Access_list_costs_are_raised_above_the_eip2930_values() + { + long addressCost = Eip8038Constants.AccessListAddressCost; + long storageKeyCost = Eip8038Constants.AccessListStorageKeyCost; + + Assert.Multiple(() => + { + Assert.That(addressCost, Is.GreaterThan(GasCostOf.AccessAccountListEntry)); + Assert.That(storageKeyCost, Is.GreaterThan(GasCostOf.AccessStorageListEntry)); + }); + } + + [Test] + public void Storage_clear_refund_follows_the_derivation_formula() + { + long storageClearRefund = Eip8038Constants.StorageClearRefund; + long expected = (Eip8038Constants.StorageWrite + Eip8038Constants.ColdStorageAccess) * 4800 / 5000; + Assert.That(storageClearRefund, Is.EqualTo(expected)); + } +} diff --git a/src/Nethermind/Nethermind.Core/Eip8038Constants.cs b/src/Nethermind/Nethermind.Core/Eip8038Constants.cs new file mode 100644 index 000000000000..c85d78caf55f --- /dev/null +++ b/src/Nethermind/Nethermind.Core/Eip8038Constants.cs @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Core; + +/// +/// EIP-8038 state-access gas cost parameters, layered on top of the EIP-8037 two-dimensional +/// (regular + state) gas model. +/// +/// +/// The EIP is a Draft: the final repriced values are still TBD. The base parameters below are +/// therefore placeholders equal to the current (pre-8038) costs, so that enabling the EIP is a no-op +/// for the base constants until the final figures land. The derived parameters are expressed via the +/// EIP's derivation formulas so they recompute automatically once the base values are finalized. +/// +public static class Eip8038Constants +{ + // Base parameters (placeholders == current values; final values TBD). + + /// Cold account-touch cost (EIP-2929 COLD_ACCOUNT_ACCESS). + public const long ColdAccountAccess = GasCostOf.ColdAccountAccess; // 2600 + + /// Warm state-access cost (EIP-2929 WARM_ACCESS). + public const long WarmAccess = GasCostOf.WarmStateRead; // 100 + + /// Cold storage-slot access cost (EIP-2929 COLD_STORAGE_ACCESS). + public const long ColdStorageAccess = GasCostOf.ColdSLoad; // 2100 + + /// The account-write component of value-bearing *CALLs (CALL_VALUE - CALL_STIPEND). + public const long AccountWrite = GasCostOf.CallValue - GasCostOf.CallStipend; // 6700 + + /// The regular-gas write component of SSTORE (GAS_STORAGE_UPDATE - COLD_STORAGE_ACCESS - WARM_ACCESS). + public const long StorageWrite = GasCostOf.SReset - ColdStorageAccess - WarmAccess; // 2800 + + /// Stipend forwarded with a value-bearing call (unchanged from EIP-2929). + public const long CallStipend = GasCostOf.CallStipend; // 2300 + + // Derived parameters (EIP-8038 derivation formulas; recompute from the base values above). + + /// CALL_VALUE = ACCOUNT_WRITE + CALL_STIPEND. + public const long CallValue = AccountWrite + CallStipend; // 9000 + + /// CREATE_ACCESS = ACCOUNT_WRITE + COLD_STORAGE_ACCESS, charged in regular gas by CREATE/CREATE2. + public const long CreateAccess = AccountWrite + ColdStorageAccess; + + /// Access-list address entry cost, redefined to COLD_ACCOUNT_ACCESS. + public const long AccessListAddressCost = ColdAccountAccess; // 2600 (was 2400) + + /// Access-list storage-key entry cost, redefined to COLD_STORAGE_ACCESS. + public const long AccessListStorageKeyCost = ColdStorageAccess; // 2100 (was 1900) + + /// STORAGE_CLEAR_REFUND = (STORAGE_WRITE + COLD_STORAGE_ACCESS) * 4800 / 5000. + public const long StorageClearRefund = (StorageWrite + ColdStorageAccess) * 4800 / 5000; +} diff --git a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs index 9f5e2f85e1a8..34851908ec91 100644 --- a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs @@ -321,6 +321,11 @@ public interface IReleaseSpec : IEip1559Spec, IReceiptSpec ///
bool IsEip8246Enabled { get; } + /// + /// EIP-8038: State-access gas cost update + /// + bool IsEip8038Enabled { get; } + /// /// EIP-8024: Backward-compatible SWAPN, DUPN, EXCHANGE /// diff --git a/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs b/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs index f894d6b51b8b..b022ab3317dd 100644 --- a/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs +++ b/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs @@ -81,6 +81,7 @@ public class ReleaseSpecDecorator(IReleaseSpec spec) : IReleaseSpec public virtual long Eip2935RingBufferSize => spec.Eip2935RingBufferSize; public virtual bool IsEip6780Enabled => spec.IsEip6780Enabled; public virtual bool IsEip8246Enabled => spec.IsEip8246Enabled; + public virtual bool IsEip8038Enabled => spec.IsEip8038Enabled; public virtual bool IsEip7702Enabled => spec.IsEip7702Enabled; public virtual bool IsEip7823Enabled => spec.IsEip7823Enabled; public virtual bool IsEip7825Enabled => spec.IsEip7825Enabled; diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip8038Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip8038Tests.cs new file mode 100644 index 000000000000..b77338c00107 --- /dev/null +++ b/src/Nethermind/Nethermind.Evm.Test/Eip8038Tests.cs @@ -0,0 +1,139 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using Nethermind.Core.Eip2930; +using Nethermind.Core.Extensions; +using Nethermind.Core.Specs; +using Nethermind.Core.Test.Builders; +using Nethermind.Evm.State; +using Nethermind.Int256; +using Nethermind.Specs; +using Nethermind.Specs.Forks; +using Nethermind.Specs.Test; +using NUnit.Framework; + +namespace Nethermind.Evm.Test; + +/// +/// EIP-8038: State-access gas cost update. With the EIP active, EXTCODESIZE and EXTCODECOPY pay an +/// additional WARM_ACCESS for the extra database read they perform. +/// +/// +/// Final repriced values are TBD while the EIP is a Draft, so the cold/warm base costs are placeholders +/// equal to their current values; the only observable change at placeholder values is the extra +/// EXT* warm access, asserted here against both the EIP-on and EIP-off baselines. +/// +[TestFixture(true)] +[TestFixture(false)] +public class Eip8038Tests(bool eip8038Enabled) : VirtualMachineTestsBase +{ + private readonly ISpecProvider _specProvider = + new TestSpecProvider(new OverridableReleaseSpec(Cancun.Instance) { IsEip8038Enabled = eip8038Enabled }); + + protected override long BlockNumber => MainnetSpecProvider.ParisBlockNumber; + protected override ulong Timestamp => MainnetSpecProvider.CancunBlockTimestamp; + protected override ISpecProvider SpecProvider => _specProvider; + + // The EXT* target; a third address that stays cold (Sender=A, Recipient=B, Miner=D). + private static readonly Address Target = TestItem.AddressC; + + private long ExtraWarmAccess => eip8038Enabled ? Eip8038Constants.WarmAccess : 0; + + [SetUp] + public override void Setup() + { + base.Setup(); + // Cold-access cost is independent of whether the target has code (EIP-2780 is off here). + TestState.CreateAccount(Target, 1.Ether); + TestState.Commit(SpecProvider.GenesisSpec); + TestState.CommitTree(0); + } + + protected override TestAllTracerWithOutput CreateTracer() + { + TestAllTracerWithOutput tracer = base.CreateTracer(); + tracer.IsTracingAccess = false; + return tracer; + } + + [Test] + public void ExtCodeSize_charges_extra_warm_access() + { + byte[] code = Prepare.EvmCode + .PushData(Target) + .Op(Instruction.EXTCODESIZE) + .Op(Instruction.POP) + .STOP() + .Done; + + TestAllTracerWithOutput result = Execute(code); + + Assert.That(result.StatusCode, Is.EqualTo(StatusCode.Success)); + long expected = GasCostOf.Transaction + + GasCostOf.VeryLow // PUSH20 target + + GasCostOf.ColdAccountAccess // cold EXTCODESIZE access + + ExtraWarmAccess // EIP-8038 extra access + + GasCostOf.Base; // POP + AssertGas(result, expected); + } + + [Test] + public void ExtCodeCopy_charges_extra_warm_access() + { + // EXTCODECOPY pops address, destOffset, srcOffset, length (address on top); length 0 skips the copy. + byte[] code = Prepare.EvmCode + .PushData(0) + .PushData(0) + .PushData(0) + .PushData(Target) + .Op(Instruction.EXTCODECOPY) + .STOP() + .Done; + + TestAllTracerWithOutput result = Execute(code); + + Assert.That(result.StatusCode, Is.EqualTo(StatusCode.Success)); + long expected = GasCostOf.Transaction + + 4 * GasCostOf.VeryLow // three PUSH1 0x00 + PUSH20 target + + GasCostOf.ColdAccountAccess // cold EXTCODECOPY access + + ExtraWarmAccess; // EIP-8038 extra access + AssertGas(result, expected); + } +} + +/// +/// EIP-8038 raises the transaction access-list entry costs to match the cold-access costs they pre-warm. +/// +public class Eip8038IntrinsicGasTests +{ + private static IReleaseSpec Spec(bool eip8038Enabled) => + new OverridableReleaseSpec(Cancun.Instance) { IsEip8038Enabled = eip8038Enabled }; + + [TestCase(false, 21000 + GasCostOf.AccessAccountListEntry, TestName = "address entry, EIP-8038 off")] + [TestCase(true, 21000 + Eip8038Constants.AccessListAddressCost, TestName = "address entry, EIP-8038 on")] + public void Access_list_address_entry_cost(bool eip8038Enabled, long expectedStandard) + { + AccessList accessList = new AccessList.Builder().AddAddress(TestItem.AddressC).Build(); + Transaction tx = Build.A.Transaction.SignedAndResolved().WithAccessList(accessList).TestObject; + + EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, Spec(eip8038Enabled)); + + Assert.That(gas.Standard, Is.EqualTo(expectedStandard)); + } + + [TestCase(false, 21000 + GasCostOf.AccessAccountListEntry + GasCostOf.AccessStorageListEntry, TestName = "address + key, EIP-8038 off")] + [TestCase(true, 21000 + Eip8038Constants.AccessListAddressCost + Eip8038Constants.AccessListStorageKeyCost, TestName = "address + key, EIP-8038 on")] + public void Access_list_address_and_storage_key_cost(bool eip8038Enabled, long expectedStandard) + { + AccessList accessList = new AccessList.Builder() + .AddAddress(TestItem.AddressC) + .AddStorage((UInt256)1) + .Build(); + Transaction tx = Build.A.Transaction.SignedAndResolved().WithAccessList(accessList).TestObject; + + EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, Spec(eip8038Enabled)); + + Assert.That(gas.Standard, Is.EqualTo(expectedStandard)); + } +} diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 8aa6d1c5f55b..6bb3a21b07fb 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -235,9 +235,12 @@ public static bool ConsumeAccountAccessGas(ref EthereumGasPolicy gas, } // EIP-2780 prices a cold touch of a code-less account cheaper than one with code. + // EIP-8038 otherwise reprices the cold account-access cost. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long ColdAccountAccessCost(IReleaseSpec spec, bool hasCode) => - spec.IsEip2780Enabled && !hasCode ? GasCostOf.ColdAccountAccessNoCodeEip2780 : GasCostOf.ColdAccountAccess; + spec.IsEip2780Enabled && !hasCode ? GasCostOf.ColdAccountAccessNoCodeEip2780 + : spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess + : GasCostOf.ColdAccountAccess; public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, ref readonly StackAccessTracker accessTracker, @@ -254,7 +257,7 @@ public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, } if (accessTracker.WarmUp(in storageCell)) - return UpdateGas(ref gas, GasCostOf.ColdSLoad); + return UpdateGas(ref gas, spec.IsEip8038Enabled ? Eip8038Constants.ColdStorageAccess : GasCostOf.ColdSLoad); if (storageAccessType == StorageAccessType.SLOAD) return UpdateGas(ref gas, GasCostOf.WarmStateRead); return true; diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs index 69841ed45035..4574bb88be09 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs @@ -179,8 +179,11 @@ public static long AccessListCost(Transaction transaction, IReleaseSpec spec, lo } (int addressesCount, int storageKeysCount) = accessList.Count; - return addressesCount * GasCostOf.AccessAccountListEntry - + storageKeysCount * GasCostOf.AccessStorageListEntry + // EIP-8038 realigns access-list entry costs with the cold-access costs they pre-warm. + long addressCost = spec.IsEip8038Enabled ? Eip8038Constants.AccessListAddressCost : GasCostOf.AccessAccountListEntry; + long storageKeyCost = spec.IsEip8038Enabled ? Eip8038Constants.AccessListStorageKeyCost : GasCostOf.AccessStorageListEntry; + return addressesCount * addressCost + + storageKeysCount * storageKeyCost + spec.GasCosts.TotalCostFloorPerToken * floorTokensInAccessList; [DoesNotReturn, StackTraceHidden] diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs index 217c17aaa691..e3780702b6e6 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs @@ -172,6 +172,10 @@ public static EvmExceptionType InstructionExtCodeCopy( hasCode: !spec.IsEip2780Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; + // EIP-8038 charges an extra warm access for the second DB read EXTCODECOPY performs. + if (spec.IsEip8038Enabled && !TGasPolicy.UpdateGas(ref gas, Eip8038Constants.WarmAccess)) + goto OutOfGas; + if (!result.IsZero) { // Update memory cost if the destination region requires expansion. @@ -247,6 +251,10 @@ public static EvmExceptionType InstructionExtCodeSize( hasCode: !spec.IsEip2780Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; + // EIP-8038 charges an extra warm access for the second DB read EXTCODESIZE performs. + if (spec.IsEip8038Enabled && !TGasPolicy.UpdateGas(ref gas, Eip8038Constants.WarmAccess)) + goto OutOfGas; + vm.WorldState.AddAccountRead(address); // Attempt a peephole optimization when tracing is not active and code is available. diff --git a/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs b/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs index 7628cba8051e..f3f338c5d6f0 100644 --- a/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs @@ -99,6 +99,7 @@ public class OverridableReleaseSpec(IReleaseSpec spec) : IReleaseSpec public long Eip2935RingBufferSize { get; set; } = spec.Eip2935RingBufferSize; public bool IsEip6780Enabled { get; set; } = spec.IsEip6780Enabled; public bool IsEip8246Enabled { get; set; } = spec.IsEip8246Enabled; + public bool IsEip8038Enabled { get; set; } = spec.IsEip8038Enabled; public bool IsEip4788Enabled { get; set; } = spec.IsEip4788Enabled; public bool IsEip4844FeeCollectorEnabled { get; set; } = spec.IsEip4844FeeCollectorEnabled; public Address? Eip4788ContractAddress { get; set; } = spec.Eip4788ContractAddress; diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs index 6143e411c81b..a6383db880f9 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs @@ -188,6 +188,7 @@ public class ChainParameters public ulong? Eip7708TransitionTimestamp { get; set; } public ulong? Eip8024TransitionTimestamp { get; set; } public ulong? Eip8246TransitionTimestamp { get; set; } + public ulong? Eip8038TransitionTimestamp { get; set; } public ulong? Eip7843TransitionTimestamp { get; set; } public ulong? Eip7954TransitionTimestamp { get; set; } public ulong? Eip2780TransitionTimestamp { get; set; } diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs index e837d0ac5b11..762ee8af3746 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs @@ -327,6 +327,7 @@ protected virtual ReleaseSpec CreateReleaseSpec(ChainSpec chainSpec, long releas releaseSpec.IsEip7918Enabled = (chainSpec.Parameters.Eip7918TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; releaseSpec.IsEip8024Enabled = (chainSpec.Parameters.Eip8024TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; releaseSpec.IsEip8246Enabled = (chainSpec.Parameters.Eip8246TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; + releaseSpec.IsEip8038Enabled = (chainSpec.Parameters.Eip8038TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; bool eip1559FeeCollector = releaseSpec.IsEip1559Enabled && (chainSpec.Parameters.Eip1559FeeCollectorTransition ?? long.MaxValue) <= releaseStartBlock; bool eip4844FeeCollector = releaseSpec.IsEip4844Enabled && (chainSpec.Parameters.Eip4844FeeCollectorTransitionTimestamp ?? long.MaxValue) <= releaseStartTimestamp; diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs index 29d1f341c8b9..c118bb8c96d0 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs @@ -217,6 +217,7 @@ bool GetForInnerPathExistence(KeyValuePair o) => Eip8024TransitionTimestamp = chainSpecJson.Params.Eip8024TransitionTimestamp, Eip8246TransitionTimestamp = chainSpecJson.Params.Eip8246TransitionTimestamp, + Eip8038TransitionTimestamp = chainSpecJson.Params.Eip8038TransitionTimestamp, Eip7843TransitionTimestamp = chainSpecJson.Params.Eip7843TransitionTimestamp, Eip7954TransitionTimestamp = chainSpecJson.Params.Eip7954TransitionTimestamp, Eip2780TransitionTimestamp = chainSpecJson.Params.Eip2780TransitionTimestamp, diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs index 920062a605ce..ada37020d8ab 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs @@ -195,6 +195,7 @@ public class ChainSpecParamsJson : IHasNamedForks public ulong? Eip7708TransitionTimestamp { get; set; } public ulong? Eip8024TransitionTimestamp { get; set; } public ulong? Eip8246TransitionTimestamp { get; set; } + public ulong? Eip8038TransitionTimestamp { get; set; } public ulong? Eip7843TransitionTimestamp { get; set; } public ulong? Eip7954TransitionTimestamp { get; set; } public ulong? Eip2780TransitionTimestamp { get; set; } diff --git a/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs b/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs index e7d4f9d1d35a..d7aed0e56867 100644 --- a/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs @@ -94,6 +94,7 @@ public class ReleaseSpec : IReleaseSpec public bool IsEip5656Enabled { get; set; } public bool IsEip6780Enabled { get; set; } public bool IsEip8246Enabled { get; set; } + public bool IsEip8038Enabled { get; set; } public bool IsEip4788Enabled { get; set; } public bool IsEip7702Enabled { get; set; } public bool IsEip7823Enabled { get; set; } From 649f2b71a38dbd7c096d30c7571831e4d5d203b6 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:17:33 +0100 Subject: [PATCH 10/39] test: update pyspec fixtures to glamsterdam-devnet v6.0.0 Co-Authored-By: Claude Opus 4.8 --- src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Constants.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Constants.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Constants.cs index 58f388e9418b..4d4e1fc06132 100644 --- a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Constants.cs +++ b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Constants.cs @@ -6,6 +6,6 @@ namespace Ethereum.Blockchain.Pyspec.Test; public class Constants { public const string ARCHIVE_URL_TEMPLATE = "https://github.com/ethereum/execution-specs/releases/download/{0}/{1}"; - public const string DEFAULT_ARCHIVE_VERSION = "tests-bal@v7.2.0"; - public const string DEFAULT_ARCHIVE_NAME = "fixtures_bal.tar.gz"; + public const string DEFAULT_ARCHIVE_VERSION = "tests-glamsterdam-devnet@v6.0.0"; + public const string DEFAULT_ARCHIVE_NAME = "fixtures_glamsterdam-devnet.tar.gz"; } From 1a40187fbd0c077558387e938df8a35faf203b76 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:07:24 +0100 Subject: [PATCH 11/39] feat: EIP-7954 increase max contract size to 64 KiB (devnet-6) 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 --- src/Nethermind/Nethermind.Core/CodeSizeConstants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Core/CodeSizeConstants.cs b/src/Nethermind/Nethermind.Core/CodeSizeConstants.cs index bcd2406faf92..8d5a4a06e91d 100644 --- a/src/Nethermind/Nethermind.Core/CodeSizeConstants.cs +++ b/src/Nethermind/Nethermind.Core/CodeSizeConstants.cs @@ -6,5 +6,5 @@ namespace Nethermind.Core; public static class CodeSizeConstants { public const int MaxCodeSizeEip170 = 24_576; // 24KiB - public const int MaxCodeSizeEip7954 = 32_768; // 32KiB + public const int MaxCodeSizeEip7954 = 65_536; // 64KiB } From 8efe06a2b4ea17658ec18ce637ec478c403a9329 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:08:49 +0100 Subject: [PATCH 12/39] feat: schedule EIP-2780 and EIP-8246 into Amsterdam (devnet-6) 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 --- src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs index 424b474ba532..a65b2cacc7a7 100644 --- a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs +++ b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs @@ -11,6 +11,7 @@ public class Amsterdam() : NamedReleaseSpec(BPO2.Instance) public override void Apply(NamedReleaseSpec spec) { spec.Name = "Amsterdam"; + spec.IsEip2780Enabled = true; spec.IsEip7976Enabled = true; spec.IsEip7981Enabled = true; spec.IsEip7708Enabled = true; @@ -21,9 +22,7 @@ public override void Apply(NamedReleaseSpec spec) spec.MaxCodeSize = CodeSizeConstants.MaxCodeSizeEip7954; spec.IsEip8024Enabled = true; spec.IsEip8037Enabled = true; - // EIP-8246 is implemented but stays off here: it is still a Draft and not part of the - // EEST `for_amsterdam` fixtures, so enabling it would diverge from conformance tests. - // It can be activated via the Eip8246Transition chainspec parameter when scheduled. + spec.IsEip8246Enabled = true; spec.EngineApiNewPayloadVersion = EngineApiVersions.NewPayload.V5; spec.EngineApiGetPayloadVersion = EngineApiVersions.GetPayload.V6; spec.EngineApiForkchoiceVersion = EngineApiVersions.Fcu.V4; From de79428f9d36798d766085a8204e22e8ff453ec1 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:19:54 +0100 Subject: [PATCH 13/39] feat: EIP-8038 state-access reprice with final values (devnet-6) 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 --- .../Nethermind.Core/Eip8038Constants.cs | 34 +++++++++---------- .../Nethermind.Specs/Forks/25_Amsterdam.cs | 1 + 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/Nethermind/Nethermind.Core/Eip8038Constants.cs b/src/Nethermind/Nethermind.Core/Eip8038Constants.cs index c85d78caf55f..933dbd31e501 100644 --- a/src/Nethermind/Nethermind.Core/Eip8038Constants.cs +++ b/src/Nethermind/Nethermind.Core/Eip8038Constants.cs @@ -8,29 +8,27 @@ namespace Nethermind.Core; /// (regular + state) gas model. /// /// -/// The EIP is a Draft: the final repriced values are still TBD. The base parameters below are -/// therefore placeholders equal to the current (pre-8038) costs, so that enabling the EIP is a no-op -/// for the base constants until the final figures land. The derived parameters are expressed via the -/// EIP's derivation formulas so they recompute automatically once the base values are finalized. +/// Scheduled in Amsterdam by glamsterdam-devnet-6 with the final repriced values below. The derived +/// parameters are expressed via the EIP's derivation formulas so they recompute from the base values. /// public static class Eip8038Constants { - // Base parameters (placeholders == current values; final values TBD). + // Base parameters (final values per EIP-8038, glamsterdam-devnet-6). - /// Cold account-touch cost (EIP-2929 COLD_ACCOUNT_ACCESS). - public const long ColdAccountAccess = GasCostOf.ColdAccountAccess; // 2600 + /// Cold account-touch cost (COLD_ACCOUNT_ACCESS). + public const long ColdAccountAccess = 3000; // was 2600 (EIP-2929) - /// Warm state-access cost (EIP-2929 WARM_ACCESS). - public const long WarmAccess = GasCostOf.WarmStateRead; // 100 + /// Warm state-access cost (WARM_ACCESS). + public const long WarmAccess = GasCostOf.WarmStateRead; // 100 (unchanged) - /// Cold storage-slot access cost (EIP-2929 COLD_STORAGE_ACCESS). - public const long ColdStorageAccess = GasCostOf.ColdSLoad; // 2100 + /// Cold storage-slot access cost (COLD_STORAGE_ACCESS). + public const long ColdStorageAccess = 3000; // was 2100 (EIP-2929) /// The account-write component of value-bearing *CALLs (CALL_VALUE - CALL_STIPEND). - public const long AccountWrite = GasCostOf.CallValue - GasCostOf.CallStipend; // 6700 + public const long AccountWrite = 8000; - /// The regular-gas write component of SSTORE (GAS_STORAGE_UPDATE - COLD_STORAGE_ACCESS - WARM_ACCESS). - public const long StorageWrite = GasCostOf.SReset - ColdStorageAccess - WarmAccess; // 2800 + /// The regular-gas write component of SSTORE (STORAGE_WRITE). + public const long StorageWrite = 10000; /// Stipend forwarded with a value-bearing call (unchanged from EIP-2929). public const long CallStipend = GasCostOf.CallStipend; // 2300 @@ -38,17 +36,17 @@ public static class Eip8038Constants // Derived parameters (EIP-8038 derivation formulas; recompute from the base values above). /// CALL_VALUE = ACCOUNT_WRITE + CALL_STIPEND. - public const long CallValue = AccountWrite + CallStipend; // 9000 + public const long CallValue = AccountWrite + CallStipend; // 10300 /// CREATE_ACCESS = ACCOUNT_WRITE + COLD_STORAGE_ACCESS, charged in regular gas by CREATE/CREATE2. public const long CreateAccess = AccountWrite + ColdStorageAccess; /// Access-list address entry cost, redefined to COLD_ACCOUNT_ACCESS. - public const long AccessListAddressCost = ColdAccountAccess; // 2600 (was 2400) + public const long AccessListAddressCost = ColdAccountAccess; // 3000 (was 2400) /// Access-list storage-key entry cost, redefined to COLD_STORAGE_ACCESS. - public const long AccessListStorageKeyCost = ColdStorageAccess; // 2100 (was 1900) + public const long AccessListStorageKeyCost = ColdStorageAccess; // 3000 (was 1900) /// STORAGE_CLEAR_REFUND = (STORAGE_WRITE + COLD_STORAGE_ACCESS) * 4800 / 5000. - public const long StorageClearRefund = (StorageWrite + ColdStorageAccess) * 4800 / 5000; + public const long StorageClearRefund = (StorageWrite + ColdStorageAccess) * 4800 / 5000; // 12480 } diff --git a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs index a65b2cacc7a7..c014b7073f7d 100644 --- a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs +++ b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs @@ -22,6 +22,7 @@ public override void Apply(NamedReleaseSpec spec) spec.MaxCodeSize = CodeSizeConstants.MaxCodeSizeEip7954; spec.IsEip8024Enabled = true; spec.IsEip8037Enabled = true; + spec.IsEip8038Enabled = true; spec.IsEip8246Enabled = true; spec.EngineApiNewPayloadVersion = EngineApiVersions.NewPayload.V5; spec.EngineApiGetPayloadVersion = EngineApiVersions.GetPayload.V6; From 159aebff4210abafe2ff48e06c45f0400ffbb049 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:36:50 +0100 Subject: [PATCH 14/39] feat: align EIP-2780 intrinsic gas to devnet-6 spec 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 --- src/Nethermind/Nethermind.Core/GasCostOf.cs | 3 +- .../GasPolicy/EthereumGasPolicy.cs | 80 +++++-------------- 2 files changed, 23 insertions(+), 60 deletions(-) diff --git a/src/Nethermind/Nethermind.Core/GasCostOf.cs b/src/Nethermind/Nethermind.Core/GasCostOf.cs index 4e82ca45de26..9fb62fb286be 100644 --- a/src/Nethermind/Nethermind.Core/GasCostOf.cs +++ b/src/Nethermind/Nethermind.Core/GasCostOf.cs @@ -93,7 +93,8 @@ public static class GasCostOf public const long MinModExpEip7883 = 500; // eip-7883 // eip-2780: reduce intrinsic transaction gas and reprice state-touching primitives. - public const long TransactionEip2780 = 4500; // TX_BASE_COST = 3000 ECRECOVER + 1000 STATE_UPDATE + 500 sender cold no-code + public const long TransactionEip2780 = 12000; // TX_BASE_COST: ECDSA recovery + sender account access + sender account write + public const long TxValueCostEip2780 = 4244; // recipient balance write for a value-bearing transfer (non-create) public const long StateUpdateEip2780 = 1000; // one account-leaf write (nonce/balance coalesced) public const long ColdAccountAccessNoCodeEip2780 = 500; // cold touch of an account known to have no code public const long TransferLogEip2780 = 1756; // eip-7708 LOG3 transfer event: 375 + 3*375 + 32*8 diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index bafc87a46a4f..95ec6fb17a91 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -236,12 +236,11 @@ public static bool ConsumeAccountAccessGas(ref EthereumGasPolicy gas, }; } - // EIP-2780 prices a cold touch of a code-less account cheaper than one with code. - // EIP-8038 otherwise reprices the cold account-access cost. + // EIP-8038 reprices the (flat) cold account-access cost. devnet-6 dropped the earlier + // EIP-2780 two-tier no-code discount, so the touch is independent of whether the account has code. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long ColdAccountAccessCost(IReleaseSpec spec, bool hasCode) => - spec.IsEip2780Enabled && !hasCode ? GasCostOf.ColdAccountAccessNoCodeEip2780 - : spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess + spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, @@ -529,7 +528,9 @@ public static EthereumGasPolicy CreateAvailableFromIntrinsic(long gasLimit, in E [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long CreateCost(Transaction tx, IReleaseSpec spec) => tx.IsContractCreation && spec.IsEip2Enabled - ? (spec.IsEip8037Enabled ? GasCostOf.CreateRegular : GasCostOf.TxCreate) + ? (spec.IsEip8038Enabled ? Eip8038Constants.CreateAccess + : spec.IsEip8037Enabled ? GasCostOf.CreateRegular + : GasCostOf.TxCreate) : 0; [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -541,72 +542,33 @@ private static long DataCost(Transaction tx, IReleaseSpec spec, long tokensInCal spec.GetBaseDataCost(tx) + tokensInCallData * GasCostOf.TxDataZero; /// - /// EIP-2780 charges on top of TX_BASE_COST: the EIP-7708 transfer log, the new-account surcharge, - /// and the recipient cold/warm touch plus its value-transfer STATE_UPDATE. + /// EIP-2780 recipient charge on top of TX_BASE_COST. For a non-self call: a flat + /// COLD_ACCOUNT_ACCESS touch, plus (for a value transfer) the EIP-7708 transfer log and + /// TX_VALUE_COST recipient balance write. For a CREATE: only the transfer log when value is sent + /// (the CREATE_ACCESS regular cost and NEW_ACCOUNT state cost are added by + /// /). /// /// - /// The recipient touch overrides EIP-2929's "all tx addresses are warm" rule. It is priced here - /// (where pre-state is available) rather than mid-execution; the recipient remains pre-warmed, so - /// no opcode re-charges it. Requires for the state-dependent parts. + /// Mirrors EELS calculate_intrinsic_cost: the recipient touch is a flat cold charge independent of + /// the recipient's existence, code, or access-list membership (so no lookup), + /// overriding EIP-2929's "all tx addresses are warm" rule. The recipient stays pre-warmed for execution. /// private static long Eip2780ExtraGas(Transaction tx, IReleaseSpec spec, IReadOnlyStateProvider? worldState) { if (!spec.IsEip2780Enabled) return 0; - bool isCreate = tx.IsContractCreation; - Address? to = tx.To; bool hasValue = !tx.Value.IsZero; - bool senderIsRecipient = !isCreate && tx.SenderAddress == to; - long cost = 0; - // EIP-7708 transfer log on the top-level value transfer; CREATE endows a distinct address. - if (hasValue && (isCreate || !senderIsRecipient)) - cost += GasCostOf.TransferLogEip2780; + if (tx.IsContractCreation) + return hasValue ? GasCostOf.TransferLogEip2780 : 0; - if (isCreate || to is null || worldState is null) return cost; + // Self-transfers coalesce into the sender leaf write already priced into TX_BASE_COST. + if (tx.SenderAddress == tx.To) return 0; - bool isPrecompile = spec.IsPrecompile(to); - bool recipientDead = worldState.IsDeadAccount(to); - - // New-account surcharge: value transfer to a nonexistent, non-precompile recipient. - if (hasValue && !isPrecompile && recipientDead) - cost += GasCostOf.NewAccount; - - // Self-transfers coalesce into the sender leaf write already priced into TX_BASE_COST; - // precompiles are warm at tx start and charged zero. - if (!senderIsRecipient && !isPrecompile) - { - cost += RecipientTouchCost(spec, worldState, to, AccessListAddresses(tx)); - // The new-account surcharge already covers the recipient leaf write. - if (hasValue && !recipientDead) - cost += GasCostOf.StateUpdateEip2780; - } + long cost = spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; + if (hasValue) + cost += GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780; return cost; } - - private static long RecipientTouchCost(IReleaseSpec spec, IReadOnlyStateProvider worldState, Address to, IReadOnlySet
? accessList) - { - long cost = accessList?.Contains(to) == true - ? GasCostOf.WarmStateRead - : worldState.IsContract(to) ? GasCostOf.ColdAccountAccess : GasCostOf.ColdAccountAccessNoCodeEip2780; - - // EIP-7702: a delegated recipient also touches its delegation target (always carries code). - // The EVM warms (does not gas-charge) this target for the top-level frame, so this is the sole charge. - if (spec.IsEip7702Enabled && ICodeInfoRepository.TryGetDelegatedAddress(worldState.GetCode(to).AsSpan(), out Address? target)) - cost += accessList?.Contains(target) == true ? GasCostOf.WarmStateRead : GasCostOf.ColdAccountAccess; - - return cost; - } - - private static IReadOnlySet
? AccessListAddresses(Transaction tx) - { - if (tx.AccessList is null) return null; - HashSet
set = []; - foreach ((Address address, _) in tx.AccessList) - { - set.Add(address); - } - return set; - } } From 116c5fcd0f8b165d5e50b89c54e251467f329fa8 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:58:01 +0100 Subject: [PATCH 15/39] feat: implement EIP-8282 builder execution requests glamsterdam-devnet-6 adds two builder-request predeploys that are system-called at the end of every block, after the EIP-7002 withdrawal and EIP-7251 consolidation requests: - builder deposit (type 0x03) at 0x0000884d2A...008282 - builder exit (type 0x04) at 0x000014574A...008282 Their per-block system calls read storage that must appear in the EIP-7928 block access list, so without them every Amsterdam block's BAL hash mismatches. Mirrors the existing 7002/7251 request handling: IsEip8282Enabled wired through the spec + chainspec, ExecutionRequestType BuilderDeposit/BuilderExit, two SystemCall reads in the requests processor (flat-encoded in request-type order so the requests hash is correct), and both predeploys registered as BAL system contracts. Co-Authored-By: Claude Opus 4.8 --- .../ExecutionRequestsProcessor.cs | 35 +++++++++++++++++++ .../BlockAccessListManager.Validation.cs | 4 ++- .../Nethermind.Core/Eip8282Constants.cs | 16 +++++++++ .../ExecutionRequest/ExecutionRequest.cs | 4 ++- .../Messages/BlockErrorMessages.cs | 12 +++++++ .../Nethermind.Core/Specs/IReleaseSpec.cs | 5 +++ .../Specs/IReleaseSpecExtensions.cs | 3 +- .../Specs/ReleaseSpecDecorator.cs | 1 + .../OverridableReleaseSpec.cs | 1 + .../ChainSpecStyle/ChainParameters.cs | 1 + .../ChainSpecBasedSpecProvider.cs | 1 + .../ChainSpecStyle/ChainSpecLoader.cs | 1 + .../Json/ChainSpecParamsJson.cs | 1 + .../Nethermind.Specs/Forks/25_Amsterdam.cs | 1 + .../Nethermind.Specs/ReleaseSpec.cs | 1 + 15 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 src/Nethermind/Nethermind.Core/Eip8282Constants.cs diff --git a/src/Nethermind/Nethermind.Consensus/ExecutionRequests/ExecutionRequestsProcessor.cs b/src/Nethermind/Nethermind.Consensus/ExecutionRequests/ExecutionRequestsProcessor.cs index b1373b958925..30b48ee03df8 100644 --- a/src/Nethermind/Nethermind.Consensus/ExecutionRequests/ExecutionRequestsProcessor.cs +++ b/src/Nethermind/Nethermind.Consensus/ExecutionRequests/ExecutionRequestsProcessor.cs @@ -48,11 +48,33 @@ public class ExecutionRequestsProcessor : IExecutionRequestsProcessor GasPrice = UInt256.Zero, }; + private readonly SystemCall _builderDepositTransaction = new() + { + Value = UInt256.Zero, + Data = Array.Empty(), + To = Eip8282Constants.BuilderDepositRequestPredeployAddress, + SenderAddress = Address.SystemUser, + GasLimit = GasLimit, + GasPrice = UInt256.Zero, + }; + + private readonly SystemCall _builderExitTransaction = new() + { + Value = UInt256.Zero, + Data = Array.Empty(), + To = Eip8282Constants.BuilderExitRequestPredeployAddress, + SenderAddress = Address.SystemUser, + GasLimit = GasLimit, + GasPrice = UInt256.Zero, + }; + public ExecutionRequestsProcessor(ITransactionProcessor transactionProcessor) { _transactionProcessor = transactionProcessor; _withdrawalTransaction.Hash = _withdrawalTransaction.CalculateHash(); _consolidationTransaction.Hash = _consolidationTransaction.CalculateHash(); + _builderDepositTransaction.Hash = _builderDepositTransaction.CalculateHash(); + _builderExitTransaction.Hash = _builderExitTransaction.CalculateHash(); } public void ProcessExecutionRequests(Block block, IWorldState state, TxReceipt[] receipts, IReleaseSpec spec) @@ -79,6 +101,19 @@ public void ProcessExecutionRequests(Block block, IWorldState state, TxReceipt[] BlockErrorMessages.ConsolidationsContractEmpty, BlockErrorMessages.ConsolidationsContractFailed); } + // EIP-8282: builder deposit and builder exit requests, dequeued after the + // withdrawal/consolidation requests so the flat encoding stays in request-type order. + if (spec.BuilderRequestsEnabled) + { + ReadRequests(block, state, Eip8282Constants.BuilderDepositRequestPredeployAddress, ref requests, _builderDepositTransaction, + ExecutionRequestType.BuilderDepositRequest, + BlockErrorMessages.BuilderDepositsContractEmpty, BlockErrorMessages.BuilderDepositsContractFailed); + + ReadRequests(block, state, Eip8282Constants.BuilderExitRequestPredeployAddress, ref requests, _builderExitTransaction, + ExecutionRequestType.BuilderExitRequest, + BlockErrorMessages.BuilderExitsContractEmpty, BlockErrorMessages.BuilderExitsContractFailed); + } + block.ExecutionRequests = [.. requests]; block.Header.RequestsHash = ExecutionRequestExtensions.CalculateHashFromFlatEncodedRequests(block.ExecutionRequests); diff --git a/src/Nethermind/Nethermind.Consensus/Processing/BlockAccessListManager.Validation.cs b/src/Nethermind/Nethermind.Consensus/Processing/BlockAccessListManager.Validation.cs index 574fbc043284..b6876ec5a5db 100644 --- a/src/Nethermind/Nethermind.Consensus/Processing/BlockAccessListManager.Validation.cs +++ b/src/Nethermind/Nethermind.Consensus/Processing/BlockAccessListManager.Validation.cs @@ -325,7 +325,9 @@ private static bool IsSystemUserReadAt0(AccountChangesAtIndex ac, uint index) private static bool IsSystemContract(Address address) => address == Eip7002Constants.WithdrawalRequestPredeployAddress - || address == Eip7251Constants.ConsolidationRequestPredeployAddress; + || address == Eip7251Constants.ConsolidationRequestPredeployAddress + || address == Eip8282Constants.BuilderDepositRequestPredeployAddress + || address == Eip8282Constants.BuilderExitRequestPredeployAddress; private static bool IsToleratedGeneratedOnlyAccount(Address address, uint index, bool hasNoChangesAtIndex, bool hasChargeableReads) => hasNoChangesAtIndex diff --git a/src/Nethermind/Nethermind.Core/Eip8282Constants.cs b/src/Nethermind/Nethermind.Core/Eip8282Constants.cs new file mode 100644 index 000000000000..491eb06807b6 --- /dev/null +++ b/src/Nethermind/Nethermind.Core/Eip8282Constants.cs @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Core; + +/// +/// EIP-8282 builder execution requests: the two predeploys that are system-called at the end of every +/// block (alongside the EIP-7002 withdrawal and EIP-7251 consolidation request predeploys) to dequeue +/// builder deposit and builder exit requests. +/// +public static class Eip8282Constants +{ + public static readonly Address BuilderDepositRequestPredeployAddress = new("0x0000884d2AA32eAa155F59A2f24eFa73D9008282"); + + public static readonly Address BuilderExitRequestPredeployAddress = new("0x000014574A74c805590AFF9499fc7A690f008282"); +} diff --git a/src/Nethermind/Nethermind.Core/ExecutionRequest/ExecutionRequest.cs b/src/Nethermind/Nethermind.Core/ExecutionRequest/ExecutionRequest.cs index 38c002f5c5ad..cfde52f5d21e 100644 --- a/src/Nethermind/Nethermind.Core/ExecutionRequest/ExecutionRequest.cs +++ b/src/Nethermind/Nethermind.Core/ExecutionRequest/ExecutionRequest.cs @@ -10,7 +10,9 @@ public enum ExecutionRequestType : byte { Deposit = 0, WithdrawalRequest = 1, - ConsolidationRequest = 2 + ConsolidationRequest = 2, + BuilderDepositRequest = 3, // eip-8282 + BuilderExitRequest = 4 // eip-8282 } public class ExecutionRequest diff --git a/src/Nethermind/Nethermind.Core/Messages/BlockErrorMessages.cs b/src/Nethermind/Nethermind.Core/Messages/BlockErrorMessages.cs index 02aa6a1fa34d..4db69b8188f4 100644 --- a/src/Nethermind/Nethermind.Core/Messages/BlockErrorMessages.cs +++ b/src/Nethermind/Nethermind.Core/Messages/BlockErrorMessages.cs @@ -144,6 +144,18 @@ public static string InvalidRequestsHash(Hash256? expected, Hash256? actual) => public const string ConsolidationsContractFailed = "ConsolidationsFailed: Contract execution failed."; + public const string BuilderDepositsContractEmpty = + "BuilderDepositsEmpty: Contract is not deployed."; + + public const string BuilderDepositsContractFailed = + "BuilderDepositsFailed: Contract execution failed."; + + public const string BuilderExitsContractEmpty = + "BuilderExitsEmpty: Contract is not deployed."; + + public const string BuilderExitsContractFailed = + "BuilderExitsFailed: Contract execution failed."; + public static string InvalidDepositEventLayout(string error) => $"DepositsInvalid: Invalid deposit event layout: {error}"; diff --git a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs index 5770d4490881..fa64e41cef1d 100644 --- a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs @@ -321,6 +321,11 @@ public interface IReleaseSpec : IEip1559Spec, IReceiptSpec /// bool IsEip8246Enabled { get; } + /// + /// EIP-8282: builder execution requests (builder deposit + builder exit predeploys). + /// + bool IsEip8282Enabled { get; } + /// /// EIP-8038: State-access gas cost update /// diff --git a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpecExtensions.cs b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpecExtensions.cs index cec0e4159ec1..35db54a958bf 100644 --- a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpecExtensions.cs +++ b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpecExtensions.cs @@ -16,6 +16,7 @@ public static class IReleaseSpecExtensions public bool DepositsEnabled => spec.IsEip6110Enabled; public bool WithdrawalRequestsEnabled => spec.IsEip7002Enabled; public bool ConsolidationRequestsEnabled => spec.IsEip7251Enabled; + public bool BuilderRequestsEnabled => spec.IsEip8282Enabled; // STATE related public bool ClearEmptyAccountWhenTouched => spec.IsEip158Enabled; // VM @@ -58,7 +59,7 @@ public static class IReleaseSpecExtensions public bool MCopyIncluded => spec.IsEip5656Enabled; public bool BlobBaseFeeEnabled => spec.IsEip4844Enabled; public bool IsAuthorizationListEnabled => spec.IsEip7702Enabled; - public bool RequestsEnabled => spec.ConsolidationRequestsEnabled || spec.WithdrawalRequestsEnabled || spec.DepositsEnabled; + public bool RequestsEnabled => spec.ConsolidationRequestsEnabled || spec.WithdrawalRequestsEnabled || spec.DepositsEnabled || spec.BuilderRequestsEnabled; /// /// Determines whether the specified address is a precompiled contract for this release specification. /// diff --git a/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs b/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs index 7675955cd2ff..fec91d19156c 100644 --- a/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs +++ b/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs @@ -82,6 +82,7 @@ public class ReleaseSpecDecorator(IReleaseSpec spec) : IReleaseSpec public virtual bool IsEip6780Enabled => spec.IsEip6780Enabled; public virtual bool IsEip8246Enabled => spec.IsEip8246Enabled; public virtual bool IsEip8038Enabled => spec.IsEip8038Enabled; + public virtual bool IsEip8282Enabled => spec.IsEip8282Enabled; public virtual bool IsEip7702Enabled => spec.IsEip7702Enabled; public virtual bool IsEip7823Enabled => spec.IsEip7823Enabled; public virtual bool IsEip7825Enabled => spec.IsEip7825Enabled; diff --git a/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs b/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs index 1726348fec3c..5efc05964d02 100644 --- a/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs @@ -96,6 +96,7 @@ public class OverridableReleaseSpec(IReleaseSpec spec) : IReleaseSpec public bool IsEip6780Enabled { get; set; } = spec.IsEip6780Enabled; public bool IsEip8246Enabled { get; set; } = spec.IsEip8246Enabled; public bool IsEip8038Enabled { get; set; } = spec.IsEip8038Enabled; + public bool IsEip8282Enabled { get; set; } = spec.IsEip8282Enabled; public bool IsEip4788Enabled { get; set; } = spec.IsEip4788Enabled; public bool IsEip4844FeeCollectorEnabled { get; set; } = spec.IsEip4844FeeCollectorEnabled; public Address? Eip4788ContractAddress { get; set; } = spec.Eip4788ContractAddress; diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs index 553a440ebb90..145705384762 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs @@ -186,6 +186,7 @@ public class ChainParameters public ulong? Eip8024TransitionTimestamp { get; set; } public ulong? Eip8246TransitionTimestamp { get; set; } public ulong? Eip8038TransitionTimestamp { get; set; } + public ulong? Eip8282TransitionTimestamp { get; set; } public ulong? Eip7843TransitionTimestamp { get; set; } public ulong? Eip7954TransitionTimestamp { get; set; } public ulong? Eip2780TransitionTimestamp { get; set; } diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs index eb5214f2b4ae..f43cead9b306 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs @@ -325,6 +325,7 @@ protected virtual ReleaseSpec CreateReleaseSpec(ChainSpec chainSpec, long releas releaseSpec.IsEip8024Enabled = (chainSpec.Parameters.Eip8024TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; releaseSpec.IsEip8246Enabled = (chainSpec.Parameters.Eip8246TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; releaseSpec.IsEip8038Enabled = (chainSpec.Parameters.Eip8038TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; + releaseSpec.IsEip8282Enabled = (chainSpec.Parameters.Eip8282TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; bool eip1559FeeCollector = releaseSpec.IsEip1559Enabled && (chainSpec.Parameters.Eip1559FeeCollectorTransition ?? long.MaxValue) <= releaseStartBlock; bool eip4844FeeCollector = releaseSpec.IsEip4844Enabled && (chainSpec.Parameters.Eip4844FeeCollectorTransitionTimestamp ?? long.MaxValue) <= releaseStartTimestamp; diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs index 1280a5dc01dd..6a2c00ace2a7 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs @@ -215,6 +215,7 @@ bool GetForInnerPathExistence(KeyValuePair o) => Eip8024TransitionTimestamp = chainSpecJson.Params.Eip8024TransitionTimestamp, Eip8246TransitionTimestamp = chainSpecJson.Params.Eip8246TransitionTimestamp, Eip8038TransitionTimestamp = chainSpecJson.Params.Eip8038TransitionTimestamp, + Eip8282TransitionTimestamp = chainSpecJson.Params.Eip8282TransitionTimestamp, Eip7843TransitionTimestamp = chainSpecJson.Params.Eip7843TransitionTimestamp, Eip7954TransitionTimestamp = chainSpecJson.Params.Eip7954TransitionTimestamp, Eip2780TransitionTimestamp = chainSpecJson.Params.Eip2780TransitionTimestamp, diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs index 85e67c40d62f..3b744dbaacbd 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs @@ -193,6 +193,7 @@ public class ChainSpecParamsJson : IHasNamedForks public ulong? Eip8024TransitionTimestamp { get; set; } public ulong? Eip8246TransitionTimestamp { get; set; } public ulong? Eip8038TransitionTimestamp { get; set; } + public ulong? Eip8282TransitionTimestamp { get; set; } public ulong? Eip7843TransitionTimestamp { get; set; } public ulong? Eip7954TransitionTimestamp { get; set; } public ulong? Eip2780TransitionTimestamp { get; set; } diff --git a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs index c014b7073f7d..79e1c672c447 100644 --- a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs +++ b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs @@ -24,6 +24,7 @@ public override void Apply(NamedReleaseSpec spec) spec.IsEip8037Enabled = true; spec.IsEip8038Enabled = true; spec.IsEip8246Enabled = true; + spec.IsEip8282Enabled = true; spec.EngineApiNewPayloadVersion = EngineApiVersions.NewPayload.V5; spec.EngineApiGetPayloadVersion = EngineApiVersions.GetPayload.V6; spec.EngineApiForkchoiceVersion = EngineApiVersions.Fcu.V4; diff --git a/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs b/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs index 8500a1068ac5..1a6356b6dd6a 100644 --- a/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs @@ -91,6 +91,7 @@ public class ReleaseSpec : IReleaseSpec public bool IsEip6780Enabled { get; set; } public bool IsEip8246Enabled { get; set; } public bool IsEip8038Enabled { get; set; } + public bool IsEip8282Enabled { get; set; } public bool IsEip4788Enabled { get; set; } public bool IsEip7702Enabled { get; set; } public bool IsEip7823Enabled { get; set; } From d78e652953a267a2a318ec4926727eb2e2e375bc Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:10:43 +0100 Subject: [PATCH 16/39] feat: EIP-8038 SSTORE write cost (STORAGE_WRITE) 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 --- .../Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 95ec6fb17a91..6bf4e89967a6 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -302,12 +302,16 @@ public static bool ConsumeStorageWrite(ref EthereumGa where TEip8037 : struct, IFlag where TIsSlotCreation : struct, IFlag { - if (!TIsSlotCreation.IsActive) return UpdateGas(ref gas, spec.GasCosts.SStoreResetCost); + // EIP-8038 reprices the SSTORE write component (charged on the first change to a slot, + // for both fresh slots and resets) to a flat STORAGE_WRITE. + long regularWriteCost = spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : GasCostOf.SSetRegular; + if (!TIsSlotCreation.IsActive) + return UpdateGas(ref gas, spec.IsEip8038Enabled ? Eip8038Constants.StorageWrite : spec.GasCosts.SStoreResetCost); return TEip8037.IsActive switch { // EIP-8037: charge the regular component first so an OOG halt does not // spill state gas into gas_left and then restore it to the parent frame. - true => TryConsumeStateAndRegularGas(ref gas, GetStorageSetStateCost(in gas), GasCostOf.SSetRegular), + true => TryConsumeStateAndRegularGas(ref gas, GetStorageSetStateCost(in gas), regularWriteCost), false => UpdateGas(ref gas, GasCostOf.SSet), }; } From ac177ae86e47666617adf716dc760ad8f6b14a4d Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:24:39 +0100 Subject: [PATCH 17/39] feat: EIP-8038 SSTORE access folds in warm cost; drop net-metered charge 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 --- src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs | 5 ++++- src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs b/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs index 124848462b48..8a74053bf829 100644 --- a/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs +++ b/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs @@ -57,8 +57,11 @@ public SpecGasCosts(IReleaseSpec spec) ? GasCostOf.SReset - GasCostOf.ColdSLoad : GasCostOf.SReset; + // EIP-8038 folds the warm-access charge into the SSTORE access cost itself (see + // ConsumeStorageAccessGas), so no separate net-metered charge is added on top. long netMeteredSStoreCost = NetMeteredSStoreCost = - hotCold ? GasCostOf.WarmStateRead + spec.IsEip8038Enabled ? GasCostOf.Free + : hotCold ? GasCostOf.WarmStateRead : netIstanbul ? GasCostOf.SStoreNetMeteredEip2200 : netConstantinople ? GasCostOf.SStoreNetMeteredEip1283 : GasCostOf.Free; diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 6bf4e89967a6..791a1e270d44 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -259,7 +259,9 @@ public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, if (accessTracker.WarmUp(in storageCell)) return UpdateGas(ref gas, spec.IsEip8038Enabled ? Eip8038Constants.ColdStorageAccess : GasCostOf.ColdSLoad); - if (storageAccessType == StorageAccessType.SLOAD) + // EIP-8038 charges the warm-access cost on SSTORE too (the net-metered charge is dropped); + // pre-8038, a warm SSTORE access is free here and the warm cost comes from net metering. + if (storageAccessType == StorageAccessType.SLOAD || spec.IsEip8038Enabled) return UpdateGas(ref gas, GasCostOf.WarmStateRead); return true; } From dc56e32302e2500e5416ed8810950d419e528a66 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:55:59 +0100 Subject: [PATCH 18/39] feat: EIP-8038 SSTORE source-based refunds 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 --- src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs | 8 +++++--- .../Instructions/EvmInstructions.Storage.cs | 10 ++++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs b/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs index 8a74053bf829..9b8950d7d653 100644 --- a/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs +++ b/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs @@ -111,9 +111,11 @@ public SpecGasCosts(IReleaseSpec spec) ? GasCostOf.TotalCostFloorPerTokenEip7623 : GasCostOf.Free; - SClearRefund = spec.IsEip3529Enabled - ? RefundOf.SClearAfterEip3529 - : RefundOf.SClearBeforeEip3529; + SClearRefund = spec.IsEip8038Enabled + ? Eip8038Constants.StorageClearRefund // 12480 + : spec.IsEip3529Enabled + ? RefundOf.SClearAfterEip3529 + : RefundOf.SClearBeforeEip3529; DestroyRefund = spec.IsEip3529Enabled ? RefundOf.DestroyAfterEip3529 diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Storage.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Storage.cs index 2d34bf5a34f6..96d7e7a0a590 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Storage.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Storage.cs @@ -560,12 +560,18 @@ internal static EvmExceptionType InstructionSStoreMetered Date: Mon, 22 Jun 2026 15:26:37 +0100 Subject: [PATCH 19/39] feat: EIP-8038 EIP-7702 per-authorization regular cost 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 --- src/Nethermind/Nethermind.Core/Eip8038Constants.cs | 7 +++++++ src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Core/Eip8038Constants.cs b/src/Nethermind/Nethermind.Core/Eip8038Constants.cs index 933dbd31e501..e0172aa96a93 100644 --- a/src/Nethermind/Nethermind.Core/Eip8038Constants.cs +++ b/src/Nethermind/Nethermind.Core/Eip8038Constants.cs @@ -49,4 +49,11 @@ public static class Eip8038Constants /// STORAGE_CLEAR_REFUND = (STORAGE_WRITE + COLD_STORAGE_ACCESS) * 4800 / 5000. public const long StorageClearRefund = (StorageWrite + ColdStorageAccess) * 4800 / 5000; // 12480 + + /// + /// EIP-7702 per-authorization regular gas: ACCOUNT_WRITE + REGULAR_PER_AUTH_BASE_COST, where + /// the latter is the auth-tuple calldata floor (101 bytes × 16), an ECRECOVER (3000), a cold account + /// touch, and two warm accesses. + /// + public const long PerAuthBaseRegular = AccountWrite + (101 * 16 + 3000 + ColdAccountAccess + 2 * WarmAccess); // 15816 } diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs index 4574bb88be09..c31d7fbb4fbe 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs @@ -205,9 +205,11 @@ public static (long RegularCost, long StateCost) AuthorizationListCost(Transacti } long authCount = authList.Length; + // EIP-8038 reprices the per-authorization regular cost (ACCOUNT_WRITE + auth-base). + long perAuthRegular = spec.IsEip8038Enabled ? Eip8038Constants.PerAuthBaseRegular : GasCostOf.PerAuthBaseRegular; return spec.IsEip8037Enabled ? ( - authCount * GasCostOf.PerAuthBaseRegular, + authCount * perAuthRegular, authCount * (GasCostOf.NewAccountState + GasCostOf.PerAuthBaseState) ) : (authCount * GasCostOf.NewAccount, 0); From a13003261fbff8297d819a5b7bf654f4ab3bf7c4 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:32:30 +0100 Subject: [PATCH 20/39] feat: EIP-2780 intrinsic charges delegated-recipient target touch 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 --- .../GasPolicy/EthereumGasPolicy.cs | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 791a1e270d44..2cb25dc1ca0d 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -556,8 +556,9 @@ private static long DataCost(Transaction tx, IReleaseSpec spec, long tokensInCal /// /// /// Mirrors EELS calculate_intrinsic_cost: the recipient touch is a flat cold charge independent of - /// the recipient's existence, code, or access-list membership (so no lookup), - /// overriding EIP-2929's "all tx addresses are warm" rule. The recipient stays pre-warmed for execution. + /// the recipient's existence or code, overriding EIP-2929's "all tx addresses are warm" rule; the recipient + /// stays pre-warmed for execution. A delegated recipient (EIP-7702) additionally touches its delegation + /// target — priced here from since the top-level frame only warms it. /// private static long Eip2780ExtraGas(Transaction tx, IReleaseSpec spec, IReadOnlyStateProvider? worldState) { @@ -571,10 +572,29 @@ private static long Eip2780ExtraGas(Transaction tx, IReleaseSpec spec, IReadOnly // Self-transfers coalesce into the sender leaf write already priced into TX_BASE_COST. if (tx.SenderAddress == tx.To) return 0; - long cost = spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; + long coldAccess = spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; + long cost = coldAccess; if (hasValue) cost += GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780; + // EIP-7702: calling a delegated recipient also touches its delegation target. The EVM warms + // (does not gas-charge) that target for the top-level frame, so it is priced here instead. + if (spec.IsEip7702Enabled && worldState is not null && tx.To is not null + && ICodeInfoRepository.TryGetDelegatedAddress(worldState.GetCode(tx.To).AsSpan(), out Address? target)) + { + cost += TxAccessListContains(tx, target) ? GasCostOf.WarmStateRead : coldAccess; + } + return cost; } + + private static bool TxAccessListContains(Transaction tx, Address address) + { + if (tx.AccessList is null) return false; + foreach ((Address entry, _) in tx.AccessList) + { + if (entry == address) return true; + } + return false; + } } From 79289a223399875f714d8f1a0a49466353cb347b Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:13:55 +0100 Subject: [PATCH 21/39] feat: EIP-2780/8037 top-frame NEW_ACCOUNT charge for new-account transfers 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 --- .../TransactionProcessor.cs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index 32562c9a054b..c65b6ebfc9a9 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -388,16 +388,30 @@ private TransactionResult ExecuteSimpleTransfer( bool senderIsRecipient = tx.SenderAddress == recipient; bool isTracingActions = tracer.IsTracingActions; + // EIP-2780/EIP-8037 top-frame charge: a value transfer that materialises a new + // (dead, non-precompile) recipient pays NEW_ACCOUNT state gas, evaluated against + // pre-transfer state. If the (state) gas cannot be covered the frame is out of gas: + // no value moves and the sender forfeits all gas. + bool newAccountOutOfGas = false; + if (spec.IsEip2780Enabled && hasValueTransfer && !senderIsRecipient + && !spec.IsPrecompile(recipient) && WorldState.IsDeadAccount(recipient)) + { + newAccountOutOfGas = !TGasPolicy.ConsumeStateGas(ref gasAvailable, TGasPolicy.GetNewAccountStateCost(in gasAvailable)); + // Out of gas: consume the whole budget; the failed frame moves no value. + if (newAccountOutOfGas) + TGasPolicy.Consume(ref gasAvailable, TGasPolicy.GetRemainingGas(in gasAvailable)); + } + // Self-send: sender account is already touched/warmed by gas charging and any // +/- value balance ops would cancel to a net no-op, so skip both state writes. - if (!senderIsRecipient) + if (!senderIsRecipient && !newAccountOutOfGas) { if (hasValueTransfer) PayValue(tx, spec, opts); WorldState.AddToBalanceAndCreateIfNotExists(recipient, in hasValueTransfer ? ref value : ref UInt256.Zero, spec); } JournalCollection? logs = null; - if (spec.IsEip7708Enabled && hasValueTransfer && !senderIsRecipient) + if (spec.IsEip7708Enabled && hasValueTransfer && !senderIsRecipient && !newAccountOutOfGas) { LogEntry transferLog = TransferLog.CreateTransfer(tx.SenderAddress!, recipient, in value); logs = [transferLog]; @@ -429,7 +443,7 @@ private TransactionResult ExecuteSimpleTransfer( long postIntrinsicStateReservoir = TGasPolicy.GetStateReservoir(in gasAvailable); GasConsumed spentGas = Refund(tx, header, spec, opts, in substate, in gasAvailable, in opcodeGasPrice, codeInsertRefunds: 0, in floorGas, in standardGas, postIntrinsicStateReservoir); - const int statusCode = StatusCode.Success; + int statusCode = newAccountOutOfGas ? StatusCode.Failure : StatusCode.Success; if (tracer.IsTracingAccess) { From cd1610bf46edadede88f28bf0541a8afa3ec033e Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 16:48:42 +0100 Subject: [PATCH 22/39] feat: EIP-8038 CALL value/new-account reprice 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 --- .../GasPolicy/EthereumGasPolicy.cs | 9 +++++++-- .../Nethermind.Evm/GasPolicy/IGasPolicy.cs | 2 +- .../Instructions/EvmInstructions.Call.cs | 18 +++++++++++------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 2cb25dc1ca0d..d7e3c57bbb6d 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -422,10 +422,15 @@ public static long ApplyCodeInsertRefunds(ref EthereumGasPolicy gas, int codeIns public static bool ConsumeCallValueTransfer(ref EthereumGasPolicy gas) => UpdateGas(ref gas, GasCostOf.CallValue); - // EIP-2780 value-moving call cost: subsumes the legacy CallValue + NewAccount charges. + // EIP-2780 value-moving call cost. Under EIP-8038 a value-bearing call charges a flat CALL_VALUE + // (the new-account surcharge moves to a separate NEW_ACCOUNT state charge); the earlier draft used + // a three-tier charge keyed on self-call and recipient existence that subsumed the surcharge. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool ConsumeCallValueTransferEip2780(ref EthereumGasPolicy gas, bool isSelfCall, bool recipientEmpty) + public static bool ConsumeCallValueTransferEip2780(ref EthereumGasPolicy gas, bool isSelfCall, bool recipientEmpty, IReleaseSpec spec) { + if (spec.IsEip8038Enabled) + return UpdateGas(ref gas, Eip8038Constants.CallValue); + long cost = isSelfCall ? GasCostOf.CallValueSelfEip2780 : recipientEmpty ? GasCostOf.CallValueNewAccountEip2780 : GasCostOf.CallValueExistingEip2780; diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs index c31d7fbb4fbe..a9c133ba441a 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs @@ -134,7 +134,7 @@ static virtual long ApplyCodeInsertRefunds(ref TSelf gas, int codeInsertRefunds, static abstract bool ConsumeCallValueTransfer(ref TSelf gas); // EIP-2780 three-tier value-moving call cost replacing the legacy CallValue + NewAccount charges. - static abstract bool ConsumeCallValueTransferEip2780(ref TSelf gas, bool isSelfCall, bool recipientEmpty); + static abstract bool ConsumeCallValueTransferEip2780(ref TSelf gas, bool isSelfCall, bool recipientEmpty, IReleaseSpec spec); static abstract bool ConsumeNewAccountCreation(ref TSelf gas) where TEip8037 : struct, IFlag; static abstract bool ConsumeLogEmission(ref TSelf gas, long topicCount, long dataSize); static abstract TSelf Max(in TSelf a, in TSelf b); diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs index 18a31f275e31..368a88e7c89f 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs @@ -147,7 +147,7 @@ public static EvmExceptionType InstructionCall !state.AccountExists(target), - true => hasValueTransfer && state.IsDeadAccount(target), - }); + // EIP-8038 charges a value transfer to a dead recipient the NEW_ACCOUNT state cost (separate + // from the flat CALL_VALUE above). The earlier EIP-2780 draft folded creation into the + // value-transfer tier, so it charges nothing extra here. + bool chargesNewAccount = spec.IsEip8038Enabled + ? hasValueTransfer && state.IsDeadAccount(target) + : !spec.IsEip2780Enabled && (spec.ClearEmptyAccountWhenTouched switch + { + false => !state.AccountExists(target), + true => hasValueTransfer && state.IsDeadAccount(target), + }); bool newAccountOutOfGas = chargesNewAccount && !TGasPolicy.ConsumeNewAccountCreation(ref gas); From 611e7fb89f45c51f7ebd90e1b8148eac3554fcfc Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:10:24 +0100 Subject: [PATCH 23/39] feat: EIP-2780/8037 top-frame delegation-target touch in execution 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 --- .../TransactionProcessing/TransactionProcessor.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index c65b6ebfc9a9..960c37a3b901 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -299,6 +299,17 @@ private TransactionResult ExecuteEvmTransaction( if (!(result = BuildExecutionEnvironment(tx, spec, _codeInfoRepository, accessTracker, preloadedCodeInfo, preloadedDelegationAddress, out ExecutionEnvironment e))) return result; using ExecutionEnvironment env = e; + // EIP-2780/EIP-8037 top-frame charge: a top-level call whose recipient is an EIP-7702 + // delegation also touches the delegation target with a (flat) cold account access. The + // target is already pre-warmed for the frame, so this is the sole charge for it. If the + // gas cannot be covered the frame is exhausted and the EVM halts out of gas. + if (spec.IsEip2780Enabled && !tx.IsContractCreation && preloadedDelegationAddress is not null) + { + long delegationCold = spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; + if (!TGasPolicy.UpdateGas(ref gasAvailable, delegationCold)) + TGasPolicy.Consume(ref gasAvailable, TGasPolicy.GetRemainingGas(in gasAvailable)); + } + int statusCode = !tracer.IsTracingInstructions ? ExecuteEvmCall(tx, header, spec, tracer, opts, delegationRefunds, intrinsicGas, accessTracker, gasAvailable, env, out TransactionSubstate substate, out GasConsumed spentGas) : ExecuteEvmCall(tx, header, spec, tracer, opts, delegationRefunds, intrinsicGas, accessTracker, gasAvailable, env, out substate, out spentGas); From c3a2aab95dfad7ac26b780ceff38326650d4fe9d Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:24:20 +0100 Subject: [PATCH 24/39] fix: EIP-7778/8037 block gas excludes the calldata floor 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 --- .../GasPolicy/Eip8037BlockGasInclusionCheck.cs | 9 +++++---- .../TransactionProcessing/TransactionProcessor.cs | 3 +-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/Eip8037BlockGasInclusionCheck.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/Eip8037BlockGasInclusionCheck.cs index 3412bd2fc0ba..07aaafc0c6b0 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/Eip8037BlockGasInclusionCheck.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/Eip8037BlockGasInclusionCheck.cs @@ -45,11 +45,12 @@ public static long CalculateBlockRegularGas( long initialRegularGas, long remainingRegularGas, long stateGasSpill, - long stateGasSpillReclassified, - long floorGas) + long stateGasSpillReclassified) { + // EIP-7778/EIP-8037: the block's regular-gas dimension is the pre-refund regular gas actually + // consumed. The EIP-7623/7976 calldata floor is a minimum charge on the sender (tx_gas_used / + // receipts) only and must NOT inflate the block gasUsed. long executionRegularGasUsed = initialRegularGas - remainingRegularGas - stateGasSpill + stateGasSpillReclassified; - long blockRegularGas = intrinsicRegularGas + executionRegularGasUsed; - return Math.Max(blockRegularGas, floorGas); + return intrinsicRegularGas + executionRegularGasUsed; } } diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index 960c37a3b901..e51f770b2112 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -1723,8 +1723,7 @@ private static long Calculate8037BlockRegularGas( initialRegularGas, remainingRegularGas, stateGasSpill, - stateGasSpillReclassified, - floorGas); + stateGasSpillReclassified); } protected virtual void PayRefund(Transaction tx, UInt256 refundAmount, IReleaseSpec spec) From 20bfdb9dda6e554c98b1aa78dbb11d5cceffddb6 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Tue, 23 Jun 2026 00:56:24 +0100 Subject: [PATCH 25/39] fix: EIP-8038 EIP-7702 existing-authority regular ACCOUNT_WRITE refund 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 --- .../GasPolicy/EthereumGasPolicy.cs | 27 +++++++++---------- .../TransactionProcessor.cs | 5 +++- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index d7e3c57bbb6d..42fca5104698 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -400,23 +400,22 @@ public static void ResetForHalt(ref EthereumGasPolicy gas, long initialStateRese } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static long GetCodeInsertRegularRefund(int codeInsertRefunds, IReleaseSpec spec) => - spec.IsEip8037Enabled || codeInsertRefunds <= 0 - ? 0 - : (GasCostOf.NewAccount - GasCostOf.PerAuthBaseCost) * codeInsertRefunds; + public static long GetCodeInsertRegularRefund(int codeInsertRefunds, IReleaseSpec spec) + { + if (codeInsertRefunds <= 0) return 0; + // EIP-8038: per existing-authority EIP-7702 refund, the worst-case ACCOUNT_WRITE charged in the + // intrinsic is returned to the regular-gas refund counter (the NEW_ACCOUNT/AUTH_BASE state refunds + // are applied separately in Apply8037DelegationRefunds). + if (spec.IsEip8038Enabled) return Eip8038Constants.AccountWrite * codeInsertRefunds; + if (spec.IsEip8037Enabled) return 0; + return (GasCostOf.NewAccount - GasCostOf.PerAuthBaseCost) * codeInsertRefunds; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static long ApplyCodeInsertRefunds(ref EthereumGasPolicy gas, int codeInsertRefunds, IReleaseSpec spec, long stateGasFloor) - { - if (codeInsertRefunds > 0 && spec.IsEip8037Enabled) - { - long stateGasRefund = checked(GetNewAccountStateCost(in gas) * codeInsertRefunds); - long refundFloor = Math.Max(0, stateGasFloor - stateGasRefund); - RefundStateGas(ref gas, stateGasRefund, refundFloor, trackSpillRefund: false); - } - - return GetCodeInsertRegularRefund(codeInsertRefunds, spec); - } + // Under EIP-8037 the per-authorization state refund is applied pre-execution in + // Apply8037DelegationRefunds; only the regular refund is surfaced for the refund counter here. + => GetCodeInsertRegularRefund(codeInsertRefunds, spec); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ConsumeCallValueTransfer(ref EthereumGasPolicy gas) diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index e51f770b2112..b45412edbbda 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -650,7 +650,10 @@ private TransactionResult Apply8037DelegationRefunds( long refundFloor = Math.Max(0, stateGasFloor - stateGasRefund); TGasPolicy.RefundStateGas(ref gasAvailable, stateGasRefund, refundFloor, trackSpillRefund: false); - delegationRefunds = 0; + // delegationRefunds (existing-authority count) is intentionally NOT zeroed: it flows on to + // Refund as codeInsertRefunds to surface the regular ACCOUNT_WRITE refund. The state refund + // applied just above is the only state-dimension refund, so ApplyCodeInsertRefunds must not + // re-apply it. delegationAuthBaseRefunds = 0; } From 812bb97aa07a82525db7f20a72806f4e5c7c3606 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Tue, 23 Jun 2026 10:49:33 +0100 Subject: [PATCH 26/39] feat: EIP-8038 SELFDESTRUCT beneficiary ACCOUNT_WRITE charge 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 --- .../Instructions/EvmInstructions.ControlFlow.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs index e9196fd7fdd4..1585d4e7ca58 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs @@ -250,7 +250,17 @@ public static EvmExceptionType InstructionSelfDestruct !inheritorAccountExists && spec.UseShanghaiDDosProtection, }; - bool outOfGas = chargesNewAccount && !(TGasPolicy.ConsumeNewAccountCreation(ref gas)); + bool outOfGas = false; + if (chargesNewAccount) + { + // EIP-8038: sending a positive balance to an empty beneficiary costs ACCOUNT_WRITE regular + // gas in addition to the NEW_ACCOUNT state gas. Charge regular first so a regular-gas OOG does + // not spill state gas. + if (spec.IsEip8038Enabled) + outOfGas = !TGasPolicy.UpdateGas(ref gas, Eip8038Constants.AccountWrite); + if (!outOfGas) + outOfGas = !TGasPolicy.ConsumeNewAccountCreation(ref gas); + } if (outOfGas) goto OutOfGas; From 27b5809ded9c3cd139634e00d1ec0223ded6c532 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Tue, 23 Jun 2026 10:49:33 +0100 Subject: [PATCH 27/39] feat: EIP-8038 SELFDESTRUCT beneficiary ACCOUNT_WRITE charge 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 --- .../Instructions/EvmInstructions.ControlFlow.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs index e9196fd7fdd4..1585d4e7ca58 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs @@ -250,7 +250,17 @@ public static EvmExceptionType InstructionSelfDestruct !inheritorAccountExists && spec.UseShanghaiDDosProtection, }; - bool outOfGas = chargesNewAccount && !(TGasPolicy.ConsumeNewAccountCreation(ref gas)); + bool outOfGas = false; + if (chargesNewAccount) + { + // EIP-8038: sending a positive balance to an empty beneficiary costs ACCOUNT_WRITE regular + // gas in addition to the NEW_ACCOUNT state gas. Charge regular first so a regular-gas OOG does + // not spill state gas. + if (spec.IsEip8038Enabled) + outOfGas = !TGasPolicy.UpdateGas(ref gas, Eip8038Constants.AccountWrite); + if (!outOfGas) + outOfGas = !TGasPolicy.ConsumeNewAccountCreation(ref gas); + } if (outOfGas) goto OutOfGas; From 811b0310dcbc322474416b30eeaab848d5d83027 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:01:10 +0100 Subject: [PATCH 28/39] feat: EIP-8038 CREATE/CREATE2 opcode account cost (CREATE_ACCESS) 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 --- .../Nethermind.Evm/Instructions/EvmInstructions.Create.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs index 948995bae579..4ff65e86d0c9 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs @@ -123,7 +123,12 @@ public static EvmExceptionType InstructionCreate Date: Tue, 23 Jun 2026 14:31:41 +0100 Subject: [PATCH 29/39] fix: EIP-8038 refund full auth state/regular gas for invalid authorizations 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 --- .../TransactionProcessing/TransactionProcessor.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index b45412edbbda..babbdc04790b 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -732,6 +732,13 @@ private int ProcessDelegations(Transaction tx, IReleaseSpec spec, in StackAccess if (authorizationResult != AuthorizationTupleResult.Valid) { if (Logger.IsDebug) Logger.Debug($"Delegation {authTuple} is invalid with error: {error}"); + // EIP-8038: an invalid authorization touches no state, so the worst-case intrinsic + // charge (NEW_ACCOUNT + AUTH_BASE state and ACCOUNT_WRITE regular) is fully refunded. + if (spec.IsEip8037Enabled) + { + refunds++; + authBaseRefunds++; + } } else { From 4fd85c2bd5ab16460940762525b45e1d82490f17 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:51:59 +0100 Subject: [PATCH 30/39] fix: EIP-2780 top-frame delegation touch uses post-authorization state 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 --- .../TransactionProcessing/TransactionProcessor.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index babbdc04790b..d1c85eeca3dd 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -303,7 +303,11 @@ private TransactionResult ExecuteEvmTransaction( // delegation also touches the delegation target with a (flat) cold account access. The // target is already pre-warmed for the frame, so this is the sole charge for it. If the // gas cannot be covered the frame is exhausted and the EVM halts out of gas. - if (spec.IsEip2780Enabled && !tx.IsContractCreation && preloadedDelegationAddress is not null) + // The delegation is read from post-authorization state, so a delegation installed by this + // same transaction's authorization list is included (and one cleared by it is excluded). + bool recipientIsDelegated = spec.IsEip7702Enabled && tx.To is not null + && _codeInfoRepository.TryGetDelegation(tx.To, spec, out _); + if (spec.IsEip2780Enabled && !tx.IsContractCreation && recipientIsDelegated) { long delegationCold = spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; if (!TGasPolicy.UpdateGas(ref gasAvailable, delegationCold)) From 357a5be4d5ac1db3796f0009ee12bd8fd015b0a2 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Tue, 23 Jun 2026 17:29:38 +0100 Subject: [PATCH 31/39] test: pin EIP-8038 constants to devnet-6 repriced values Co-Authored-By: Claude Opus 4.8 --- .../Eip8038ConstantsTests.cs | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs b/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs index 8ae66c23c393..eab65c91d80d 100644 --- a/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs +++ b/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs @@ -7,14 +7,14 @@ namespace Nethermind.Core.Test; /// -/// Pins the EIP-8038 gas parameters to the spec's derivation formulas. The base values are -/// placeholders equal to the current (pre-8038) costs while the EIP is a Draft; these tests -/// guard the relationships so the derived values stay correct when the final figures land. +/// Pins the EIP-8038 gas parameters to the final repriced values scheduled in Amsterdam by +/// glamsterdam-devnet-6, and guards the derivation relationships so the derived values stay +/// consistent with the base parameters. /// public class Eip8038ConstantsTests { [Test] - public void Base_parameters_match_their_current_placeholder_values() + public void Base_parameters_match_the_devnet6_repriced_values() { long coldAccountAccess = Eip8038Constants.ColdAccountAccess; long warmAccess = Eip8038Constants.WarmAccess; @@ -25,21 +25,24 @@ public void Base_parameters_match_their_current_placeholder_values() Assert.Multiple(() => { - Assert.That(coldAccountAccess, Is.EqualTo(2600)); + Assert.That(coldAccountAccess, Is.EqualTo(3000)); Assert.That(warmAccess, Is.EqualTo(100)); - Assert.That(coldStorageAccess, Is.EqualTo(2100)); - Assert.That(accountWrite, Is.EqualTo(6700)); - Assert.That(storageWrite, Is.EqualTo(2800)); + Assert.That(coldStorageAccess, Is.EqualTo(3000)); + Assert.That(accountWrite, Is.EqualTo(8000)); + Assert.That(storageWrite, Is.EqualTo(10000)); Assert.That(callStipend, Is.EqualTo(2300)); }); } [Test] - public void Account_write_is_call_value_minus_stipend() - { - long accountWrite = Eip8038Constants.AccountWrite; - Assert.That(accountWrite, Is.EqualTo(GasCostOf.CallValue - GasCostOf.CallStipend)); - } + public void Derived_parameters_match_the_devnet6_repriced_values() => + Assert.Multiple(() => + { + Assert.That(Eip8038Constants.CallValue, Is.EqualTo(10300)); + Assert.That(Eip8038Constants.CreateAccess, Is.EqualTo(11000)); + Assert.That(Eip8038Constants.StorageClearRefund, Is.EqualTo(12480)); + Assert.That(Eip8038Constants.PerAuthBaseRegular, Is.EqualTo(15816)); + }); [Test] public void Call_value_is_account_write_plus_stipend() From 2281bc07587902a4fa0fb34f523a98b0063b9127 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Tue, 23 Jun 2026 18:21:16 +0100 Subject: [PATCH 32/39] fix: segregate standalone EIP-2780 gas model from EIP-8038 reprice Restore the standalone EIP-2780 two-tier cold-account model and intrinsic NEW_ACCOUNT / delegation-target pricing for the non-EIP-8038 path, and gate the EIP-8037 execution-time NEW_ACCOUNT and delegation-target charges on IsEip8037Enabled. Previously these execution charges fired on IsEip2780Enabled, double-charging the standalone path (which already prices them intrinsically). Amsterdam is unaffected (EIP-8037 always active). Co-Authored-By: Claude Opus 4.8 --- .../Nethermind.Evm.Test/Eip2780Tests.cs | 2 +- .../Eip8037BlockGasInclusionCheckTests.cs | 26 ++--- .../GasPolicy/EthereumGasPolicy.cs | 96 +++++++++++++++---- .../TransactionProcessor.cs | 27 +++--- 4 files changed, 104 insertions(+), 47 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs index f4bdf7af6d97..010653476103 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs @@ -29,7 +29,7 @@ public class Eip2780Tests private static long ChargeCallValue(bool isSelfCall, bool recipientEmpty) { EthereumGasPolicy gas = EthereumGasPolicy.FromLong(1_000_000); - Assert.That(EthereumGasPolicy.ConsumeCallValueTransferEip2780(ref gas, isSelfCall, recipientEmpty), Is.True); + Assert.That(EthereumGasPolicy.ConsumeCallValueTransferEip2780(ref gas, isSelfCall, recipientEmpty, Eip2780Spec), Is.True); return 1_000_000 - EthereumGasPolicy.GetRemainingGas(in gas); } diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip8037BlockGasInclusionCheckTests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip8037BlockGasInclusionCheckTests.cs index 23074d114ed8..9bd575ce4895 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Eip8037BlockGasInclusionCheckTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Eip8037BlockGasInclusionCheckTests.cs @@ -184,7 +184,6 @@ public void Calculate_block_regular_gas_keeps_valid_transcripts_non_negative() long stateGasSpill = random.Next(0, (int)Math.Min(spentRegular, int.MaxValue)); long stateGasSpillReclassified = random.Next(0, (int)Math.Min(stateGasSpill, int.MaxValue)); long remainingRegular = initialRegular - spentRegular; - long floorGas = random.Next(21_000, 200_000); long executionRegularGasUsed = initialRegular - remainingRegular - stateGasSpill + stateGasSpillReclassified; long blockRegularGas = Eip8037BlockGasInclusionCheck.CalculateBlockRegularGas( @@ -192,26 +191,27 @@ public void Calculate_block_regular_gas_keeps_valid_transcripts_non_negative() initialRegular, remainingRegular, stateGasSpill, - stateGasSpillReclassified, - floorGas); + stateGasSpillReclassified); Assert.That(executionRegularGasUsed, Is.GreaterThanOrEqualTo(0L)); - Assert.That(blockRegularGas, Is.EqualTo(Math.Max(intrinsicRegular + executionRegularGasUsed, floorGas))); + Assert.That(blockRegularGas, Is.EqualTo(intrinsicRegular + executionRegularGasUsed)); } } - [TestCase(300L, 100L, TestName = "Calculate_block_regular_gas_floor_clamps_low_regular_gas")] - [TestCase(0L, 0L, TestName = "Calculate_block_regular_gas_allows_negative_execution_intermediate")] - public void Calculate_block_regular_gas_clamps_to_floor(long initialRegular, long remainingRegular) + [Test] + public void Calculate_block_regular_gas_ignores_calldata_floor() { + // Regression: the EIP-7623/7976 calldata floor is a minimum charge on the sender + // (tx_gas_used / receipts) only and must NOT inflate the block's regular-gas dimension. + // Here the actual regular gas consumed (21_000) is far below any plausible floor, yet the + // block regular gas must report the consumed amount, not a floor-clamped value. long blockRegularGas = Eip8037BlockGasInclusionCheck.CalculateBlockRegularGas( intrinsicRegularGas: 21_000, - initialRegularGas: initialRegular, - remainingRegularGas: remainingRegular, - stateGasSpill: 200, - stateGasSpillReclassified: 0, - floorGas: 53_000); + initialRegularGas: 0, + remainingRegularGas: 0, + stateGasSpill: 0, + stateGasSpillReclassified: 0); - Assert.That(blockRegularGas, Is.EqualTo(53_000)); + Assert.That(blockRegularGas, Is.EqualTo(21_000)); } } diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 42fca5104698..43961fc843cd 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -241,6 +241,7 @@ public static bool ConsumeAccountAccessGas(ref EthereumGasPolicy gas, [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long ColdAccountAccessCost(IReleaseSpec spec, bool hasCode) => spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess + : spec.IsEip2780Enabled && !hasCode ? GasCostOf.ColdAccountAccessNoCodeEip2780 : GasCostOf.ColdAccountAccess; public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, @@ -552,22 +553,30 @@ private static long DataCost(Transaction tx, IReleaseSpec spec, long tokensInCal spec.GetBaseDataCost(tx) + tokensInCallData * GasCostOf.TxDataZero; /// - /// EIP-2780 recipient charge on top of TX_BASE_COST. For a non-self call: a flat - /// COLD_ACCOUNT_ACCESS touch, plus (for a value transfer) the EIP-7708 transfer log and - /// TX_VALUE_COST recipient balance write. For a CREATE: only the transfer log when value is sent - /// (the CREATE_ACCESS regular cost and NEW_ACCOUNT state cost are added by - /// /). + /// EIP-2780 recipient charge on top of TX_BASE_COST. Dispatches to the EIP-8038 (glamsterdam-devnet-6) + /// flat model when EIP-8038 is active, otherwise to the standalone EIP-2780 two-tier model. /// /// - /// Mirrors EELS calculate_intrinsic_cost: the recipient touch is a flat cold charge independent of - /// the recipient's existence or code, overriding EIP-2929's "all tx addresses are warm" rule; the recipient - /// stays pre-warmed for execution. A delegated recipient (EIP-7702) additionally touches its delegation - /// target — priced here from since the top-level frame only warms it. + /// Both models mirror their respective EELS calculate_intrinsic_cost: the recipient touch overrides + /// EIP-2929's "all tx addresses are warm" rule and the recipient stays pre-warmed for execution. See + /// and for the specifics. /// private static long Eip2780ExtraGas(Transaction tx, IReleaseSpec spec, IReadOnlyStateProvider? worldState) { if (!spec.IsEip2780Enabled) return 0; + return spec.IsEip8038Enabled + ? Eip8038IntrinsicRecipientGas(tx, spec, worldState) + : Eip2780StandaloneExtraGas(tx, spec, worldState); + } + // EIP-8038 (glamsterdam-devnet-6): the recipient touch is a flat cold charge independent of the + // recipient's existence or code, and a value transfer adds the EIP-7708 transfer log plus a fixed + // value-move cost. The new-account surcharge moves to a separate NEW_ACCOUNT state charge, and the + // EIP-7702 delegation-target touch is charged at execution time (against post-authorization state) + // rather than here; neither is priced in this method. worldState-independent because the EELS + // intrinsic cost does not consult state. + private static long Eip8038IntrinsicRecipientGas(Transaction tx, IReleaseSpec spec, IReadOnlyStateProvider? worldState) + { bool hasValue = !tx.Value.IsZero; if (tx.IsContractCreation) @@ -576,29 +585,74 @@ private static long Eip2780ExtraGas(Transaction tx, IReleaseSpec spec, IReadOnly // Self-transfers coalesce into the sender leaf write already priced into TX_BASE_COST. if (tx.SenderAddress == tx.To) return 0; - long coldAccess = spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; - long cost = coldAccess; + long cost = Eip8038Constants.ColdAccountAccess; if (hasValue) cost += GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780; - // EIP-7702: calling a delegated recipient also touches its delegation target. The EVM warms - // (does not gas-charge) that target for the top-level frame, so it is priced here instead. - if (spec.IsEip7702Enabled && worldState is not null && tx.To is not null - && ICodeInfoRepository.TryGetDelegatedAddress(worldState.GetCode(tx.To).AsSpan(), out Address? target)) + return cost; + } + + // Standalone EIP-2780 (pre-EIP-8038) intrinsic recipient cost: a two-tier cold touch keyed on + // whether the recipient carries code, plus a NEW_ACCOUNT surcharge and a STATE_UPDATE leaf write + // for value transfers, mirroring the EIP-2780 reference table. Requires + // to classify the recipient; superseded by under EIP-8038. + private static long Eip2780StandaloneExtraGas(Transaction tx, IReleaseSpec spec, IReadOnlyStateProvider? worldState) + { + bool isCreate = tx.IsContractCreation; + Address? to = tx.To; + bool hasValue = !tx.Value.IsZero; + bool senderIsRecipient = !isCreate && tx.SenderAddress == to; + + long cost = 0; + // EIP-7708 transfer log on the top-level value transfer; CREATE endows a distinct address. + if (hasValue && (isCreate || !senderIsRecipient)) + cost += GasCostOf.TransferLogEip2780; + + if (isCreate || to is null || worldState is null) return cost; + + bool isPrecompile = spec.IsPrecompile(to); + bool recipientDead = worldState.IsDeadAccount(to); + + // New-account surcharge: value transfer to a nonexistent, non-precompile recipient. + if (hasValue && !isPrecompile && recipientDead) + cost += GasCostOf.NewAccount; + + // Self-transfers coalesce into the sender leaf write already priced into TX_BASE_COST; + // precompiles are warm at tx start and charged zero. + if (!senderIsRecipient && !isPrecompile) { - cost += TxAccessListContains(tx, target) ? GasCostOf.WarmStateRead : coldAccess; + cost += RecipientTouchCost(spec, worldState, to, AccessListAddresses(tx)); + // The new-account surcharge already covers the recipient leaf write. + if (hasValue && !recipientDead) + cost += GasCostOf.StateUpdateEip2780; } return cost; } - private static bool TxAccessListContains(Transaction tx, Address address) + private static long RecipientTouchCost(IReleaseSpec spec, IReadOnlyStateProvider worldState, Address to, IReadOnlySet
? accessList) { - if (tx.AccessList is null) return false; - foreach ((Address entry, _) in tx.AccessList) + long cost = accessList?.Contains(to) == true + ? GasCostOf.WarmStateRead + : worldState.IsContract(to) ? GasCostOf.ColdAccountAccess : GasCostOf.ColdAccountAccessNoCodeEip2780; + + // EIP-7702: a delegated recipient also touches its delegation target (always carries code). + // The EVM warms (does not gas-charge) this target for the top-level frame, so this is the sole charge. + if (spec.IsEip7702Enabled && ICodeInfoRepository.TryGetDelegatedAddress(worldState.GetCode(to).AsSpan(), out Address? target)) + cost += accessList?.Contains(target) == true ? GasCostOf.WarmStateRead : GasCostOf.ColdAccountAccess; + + return cost; + } + + private static IReadOnlySet
? AccessListAddresses(Transaction tx) + { + if (tx.AccessList is null) return null; + HashSet
set = []; + foreach ((Address address, _) in tx.AccessList) { - if (entry == address) return true; + set.Add(address); } - return false; + return set; } + } diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index d1c85eeca3dd..411bfd94a3cd 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -299,15 +299,17 @@ private TransactionResult ExecuteEvmTransaction( if (!(result = BuildExecutionEnvironment(tx, spec, _codeInfoRepository, accessTracker, preloadedCodeInfo, preloadedDelegationAddress, out ExecutionEnvironment e))) return result; using ExecutionEnvironment env = e; - // EIP-2780/EIP-8037 top-frame charge: a top-level call whose recipient is an EIP-7702 - // delegation also touches the delegation target with a (flat) cold account access. The - // target is already pre-warmed for the frame, so this is the sole charge for it. If the - // gas cannot be covered the frame is exhausted and the EVM halts out of gas. - // The delegation is read from post-authorization state, so a delegation installed by this - // same transaction's authorization list is included (and one cleared by it is excluded). + // EIP-8037 top-frame charge: a top-level call whose recipient is an EIP-7702 delegation + // also touches the delegation target with a (flat) cold account access. The target is + // already pre-warmed for the frame, so this is the sole charge for it. If the gas cannot + // be covered the frame is exhausted and the EVM halts out of gas. The delegation is read + // from post-authorization state, so a delegation installed by this same transaction's + // authorization list is included (and one cleared by it is excluded). Gated on EIP-8037 + // (the 2-D state-gas model); the standalone EIP-2780 model instead prices the delegation + // target touch in the intrinsic cost, so charging here would double-charge. bool recipientIsDelegated = spec.IsEip7702Enabled && tx.To is not null && _codeInfoRepository.TryGetDelegation(tx.To, spec, out _); - if (spec.IsEip2780Enabled && !tx.IsContractCreation && recipientIsDelegated) + if (spec.IsEip8037Enabled && !tx.IsContractCreation && recipientIsDelegated) { long delegationCold = spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; if (!TGasPolicy.UpdateGas(ref gasAvailable, delegationCold)) @@ -403,12 +405,13 @@ private TransactionResult ExecuteSimpleTransfer( bool senderIsRecipient = tx.SenderAddress == recipient; bool isTracingActions = tracer.IsTracingActions; - // EIP-2780/EIP-8037 top-frame charge: a value transfer that materialises a new - // (dead, non-precompile) recipient pays NEW_ACCOUNT state gas, evaluated against - // pre-transfer state. If the (state) gas cannot be covered the frame is out of gas: - // no value moves and the sender forfeits all gas. + // EIP-8037 top-frame charge: a value transfer that materialises a new (dead, non-precompile) + // recipient pays NEW_ACCOUNT state gas, evaluated against pre-transfer state. If the (state) + // gas cannot be covered the frame is out of gas: no value moves and the sender forfeits all + // gas. Gated on EIP-8037 (the 2-D state-gas model); the standalone EIP-2780 model instead + // prices NEW_ACCOUNT in the intrinsic cost, so charging here would double-charge. bool newAccountOutOfGas = false; - if (spec.IsEip2780Enabled && hasValueTransfer && !senderIsRecipient + if (spec.IsEip8037Enabled && hasValueTransfer && !senderIsRecipient && !spec.IsPrecompile(recipient) && WorldState.IsDeadAccount(recipient)) { newAccountOutOfGas = !TGasPolicy.ConsumeStateGas(ref gasAvailable, TGasPolicy.GetNewAccountStateCost(in gasAvailable)); From 300de2bc638f12c3540f6e080ff7f52771b895f3 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Tue, 23 Jun 2026 23:51:56 +0100 Subject: [PATCH 33/39] fix: EIP-8037 refund NEW_ACCOUNT on successful CREATE to a pre-existing account When CREATE/CREATE2 deploys to an address that already exists (e.g. a pre-funded account), EELS refunds the up-front NEW_ACCOUNT state gas on success, since code is added to an existing leaf rather than materialising a new account. Mirrors EELS amsterdam generic_create target_alive refund. Co-Authored-By: Claude Opus 4.8 --- src/Nethermind/Nethermind.Evm/VirtualMachine.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs index 3dff3674a4de..47d9e501211e 100644 --- a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs +++ b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs @@ -289,6 +289,14 @@ public virtual TransactionSubstate ExecuteTransaction( if (isCreate) { IncorporateChildStateGasRefunds(previousState); + // EIP-8037: the NEW_ACCOUNT state gas charged up-front at the CREATE/CREATE2 + // opcode is refunded on successful deployment when the target account already + // existed (e.g. a pre-funded address), since the code is added to an existing + // account leaf rather than materialising a new one. + if (previousState.IsCreateOnPreExistingAccount) + { + CreditStateGasRefund(ref _currentState.Gas, TGasPolicy.GetCreateStateCost(in _currentState.Gas)); + } } } } From 431d3ea57fd8173f2e839f304b97ab9232ad0ef9 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:27:04 +0100 Subject: [PATCH 34/39] fix: EIP-8037 refund NEW_ACCOUNT on successful top-level CREATE tx to a pre-existing account A CREATE transaction charges NEW_ACCOUNT state gas in the intrinsic cost. EELS process_transaction refunds it when the tx errors or the created target was already alive (created_target_alive). Nethermind already refunds on revert/halt; this adds the success-with-pre-existing-target case, mirroring EELS. Co-Authored-By: Claude Opus 4.8 --- .../TransactionProcessing/TransactionProcessor.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index 411bfd94a3cd..249e1d8def12 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -1251,6 +1251,11 @@ private int ExecuteEvmCall( Snapshot snapshot = WorldState.TakeSnapshot(); long floorGasLong = TGasPolicy.GetRemainingGas(gas.FloorGas); + // EIP-8037: capture whether the create target already existed (was alive) before deployment. + // On a successful create to a pre-existing account, the up-front NEW_ACCOUNT state gas charged + // in the intrinsic cost is refunded since no new account leaf is materialised. + bool createdTargetAlive = tx.IsContractCreation && !WorldState.IsDeadAccount(env.ExecutingAccount); + PayValue(tx, spec, opts); if (env.CodeInfo is not null) @@ -1359,7 +1364,7 @@ private int ExecuteEvmCall( } } - gasConsumed = Refund(tx, header, spec, opts, in substate, gasAvailable, VirtualMachine.TxExecutionContext.GasPrice, delegationRefunds, gas.FloorGas, gas.Standard, postIntrinsicStateReservoir); + gasConsumed = Refund(tx, header, spec, opts, in substate, gasAvailable, VirtualMachine.TxExecutionContext.GasPrice, delegationRefunds, gas.FloorGas, gas.Standard, postIntrinsicStateReservoir, createdTargetAlive); goto Complete; FailContractCreate: if (Logger.IsTrace) Logger.Trace("Restoring state from before transaction"); @@ -1601,11 +1606,14 @@ protected void TraceLogInvalidTx(Transaction transaction, string reason) } protected virtual GasConsumed Refund(Transaction tx, BlockHeader header, IReleaseSpec spec, ExecutionOptions opts, - in TransactionSubstate substate, in TGasPolicy unspentGas, in UInt256 gasPrice, int codeInsertRefunds, in TGasPolicy floorGas, in TGasPolicy intrinsicGasStandard, long postIntrinsicStateReservoir) + in TransactionSubstate substate, in TGasPolicy unspentGas, in UInt256 gasPrice, int codeInsertRefunds, in TGasPolicy floorGas, in TGasPolicy intrinsicGasStandard, long postIntrinsicStateReservoir, bool createdTargetAlive = false) { TGasPolicy gasAfterExecution = unspentGas; long stateGasFloor = TGasPolicy.GetStateReservoir(in intrinsicGasStandard); - if (substate.ShouldRevert && spec.IsEip8037Enabled) + // EIP-8037: refund the top-level create's up-front NEW_ACCOUNT state gas on a reverted create + // (state is rolled back) or on a successful create whose target already existed (was alive). + // Exceptional halts (substate.IsError) refund it separately via CompleteEip8037Halt below. + if (spec.IsEip8037Enabled && (substate.ShouldRevert || (!substate.IsError && createdTargetAlive))) { long refundedTopLevelCreateStateGas = CalculateTopLevelCreateIntrinsicStateRefund(tx, in intrinsicGasStandard); if (refundedTopLevelCreateStateGas > 0) From 90cb4f751ef1ef8943fa42e69d13d25d706ae72f Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:42:19 +0100 Subject: [PATCH 35/39] fix: EIP-8037 refund NEW_ACCOUNT when value CALL cannot proceed A value transfer to a new account charges NEW_ACCOUNT state gas up-front. When the call cannot proceed (call depth exceeded or insufficient caller balance) no account is created, so EELS refunds it (generic_call / call insufficient-balance path). Mirror that. No-op pre-EIP-8037. Co-Authored-By: Claude Opus 4.8 --- .../Nethermind.Evm/Instructions/EvmInstructions.Call.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs index 368a88e7c89f..a429f7c1a0d7 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs @@ -251,6 +251,11 @@ delegated is not null && // Refund the remaining gas to the caller. TGasPolicy.UpdateGasUp(ref gas, gasLimitUl); + // EIP-8037: a value transfer to a new account charges NEW_ACCOUNT state gas up-front; when the + // call cannot proceed (call depth exceeded or caller balance too low) no account is created, so + // refund it. No-op pre-EIP-8037 (CreditStateGasRefund self-gates), matching legacy semantics. + if (chargesNewAccount) + vm.CreditStateGasRefund(ref gas, TGasPolicy.GetNewAccountStateCost(in gas), trackSpillRefund: false); if (TTracingInst.IsActive) { vm.TxTracer.ReportGasUpdateForVmTrace(gasLimitUl, TGasPolicy.GetRemainingGas(in gas)); From 09afb9d72f272e57fbafa1cac478df1bd24deacd Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:21:20 +0100 Subject: [PATCH 36/39] test: update intrinsic/EIP-8037/EIP-8038 unit tests to devnet-6 gas model Update stale placeholder expected values to the devnet-6 Amsterdam reprice (TX_BASE=12000, CREATE_ACCESS=11000, COLD_ACCOUNT_ACCESS=3000, repriced access list, PerAuthBaseRegular=15816, recipient cold+value intrinsic charge). Rewrite the code-insert refund test to assert the EIP-8038 regular-gas (ACCOUNT_WRITE) refund semantics instead of the obsolete state-gas credit. Co-Authored-By: Claude Opus 4.8 --- .../Nethermind.Evm.Test/Eip8037Tests.cs | 28 +++++++++++-------- .../Nethermind.Evm.Test/Eip8038Tests.cs | 5 ++-- .../IntrinsicGasCalculatorTests.cs | 15 +++++++--- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip8037Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip8037Tests.cs index 79d3437c745a..5de6064825f5 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Eip8037Tests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Eip8037Tests.cs @@ -183,11 +183,14 @@ public void Amsterdam_access_list_floor_pricing_is_added_to_regular_and_floor_in IntrinsicGas splitIntrinsicGas = EthereumGasPolicy.CalculateIntrinsicGas(tx, Amsterdam.Instance); EthereumIntrinsicGas intrinsicGas = IntrinsicGasCalculator.Calculate(tx, Amsterdam.Instance); - long accessListBaseCost = GasCostOf.AccessAccountListEntry + 3 * GasCostOf.AccessStorageListEntry; + // Amsterdam (EIP-2780 + EIP-8038): TX_BASE=12000; access-list entries repriced to COLD_ACCOUNT_ACCESS / + // COLD_STORAGE_ACCESS; the value-bearing recipient touch adds COLD_ACCOUNT_ACCESS + TRANSFER_LOG + TX_VALUE. + long recipientRegular = Eip8038Constants.ColdAccountAccess + GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780; + long accessListBaseCost = Eip8038Constants.AccessListAddressCost + 3 * Eip8038Constants.AccessListStorageKeyCost; long accessListFloorTokens = (20L + 3 * 32L) * Amsterdam.Instance.GasCosts.TxDataNonZeroMultiplier; long accessListFloorCost = accessListFloorTokens * Amsterdam.Instance.GasCosts.TotalCostFloorPerToken; - long expectedRegular = GasCostOf.Transaction + accessListBaseCost + accessListFloorCost; - long expectedFloorGas = GasCostOf.Transaction + accessListFloorCost; + long expectedRegular = GasCostOf.TransactionEip2780 + recipientRegular + accessListBaseCost + accessListFloorCost; + long expectedFloorGas = GasCostOf.TransactionEip2780 + accessListFloorCost; Assert.That(splitIntrinsicGas.Standard.Value, Is.EqualTo(expectedRegular)); Assert.That(splitIntrinsicGas.Standard.StateReservoir, Is.Zero); @@ -270,22 +273,23 @@ public void State_refund_is_clamped_to_intrinsic_state_floor() } [Test] - public void Code_insert_state_refund_is_available_to_later_state_gas() + public void Code_insert_refund_credits_regular_gas_not_state_under_eip8038() { - const long intrinsicAuthState = GasCostOf.NewAccountState + GasCostOf.PerAuthBaseState; + // EIP-8038: the per-authorization code-insert (EIP-7702 existing-authority) refund returns the + // worst-case ACCOUNT_WRITE to the regular-gas refund counter and leaves the state-gas dimension + // untouched (the NEW_ACCOUNT / AUTH_BASE state refunds are applied separately, pre-execution). EthereumGasPolicy gas = new() { - Value = 2 * GasCostOf.SSetState - GasCostOf.NewAccountState, - StateGasUsed = intrinsicAuthState, + Value = 0, + StateReservoir = 0, + StateGasUsed = GasCostOf.PerAuthBaseState, }; - long regularRefund = EthereumGasPolicy.ApplyCodeInsertRefunds(ref gas, 1, Amsterdam.Instance, intrinsicAuthState); - Assert.That(EthereumGasPolicy.ConsumeStateGas(ref gas, GasCostOf.SSetState), Is.True); - Assert.That(EthereumGasPolicy.ConsumeStateGas(ref gas, GasCostOf.SSetState), Is.True); + long regularRefund = EthereumGasPolicy.ApplyCodeInsertRefunds(ref gas, 1, Amsterdam.Instance, stateGasFloor: 0); - Assert.That(regularRefund, Is.Zero); + Assert.That(regularRefund, Is.EqualTo(Eip8038Constants.AccountWrite)); Assert.That((gas.Value, gas.StateReservoir, gas.StateGasUsed, gas.StateGasSpill), - Is.EqualTo((0L, 0L, GasCostOf.PerAuthBaseState + 2 * GasCostOf.SSetState, 2 * GasCostOf.SSetState - GasCostOf.NewAccountState))); + Is.EqualTo((0L, 0L, GasCostOf.PerAuthBaseState, 0L))); } [Test] diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip8038Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip8038Tests.cs index b77338c00107..182c3a3b88b1 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Eip8038Tests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Eip8038Tests.cs @@ -39,6 +39,7 @@ public class Eip8038Tests(bool eip8038Enabled) : VirtualMachineTestsBase private static readonly Address Target = TestItem.AddressC; private long ExtraWarmAccess => eip8038Enabled ? Eip8038Constants.WarmAccess : 0; + private long ColdAccountAccess => eip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; [SetUp] public override void Setup() @@ -72,7 +73,7 @@ public void ExtCodeSize_charges_extra_warm_access() Assert.That(result.StatusCode, Is.EqualTo(StatusCode.Success)); long expected = GasCostOf.Transaction + GasCostOf.VeryLow // PUSH20 target - + GasCostOf.ColdAccountAccess // cold EXTCODESIZE access + + ColdAccountAccess // cold EXTCODESIZE access (EIP-8038 repriced when enabled) + ExtraWarmAccess // EIP-8038 extra access + GasCostOf.Base; // POP AssertGas(result, expected); @@ -96,7 +97,7 @@ public void ExtCodeCopy_charges_extra_warm_access() Assert.That(result.StatusCode, Is.EqualTo(StatusCode.Success)); long expected = GasCostOf.Transaction + 4 * GasCostOf.VeryLow // three PUSH1 0x00 + PUSH20 target - + GasCostOf.ColdAccountAccess // cold EXTCODECOPY access + + ColdAccountAccess // cold EXTCODECOPY access (EIP-8038 repriced when enabled) + ExtraWarmAccess; // EIP-8038 extra access AssertGas(result, expected); } diff --git a/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs b/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs index e31ba2ba44cd..cf16fc32c823 100644 --- a/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs @@ -235,7 +235,10 @@ public void Eip8037_policy_intrinsic_gas_splits_authorization_cost() .TestObject; IntrinsicGas intrinsicGas = EthereumGasPolicy.CalculateIntrinsicGas(tx, Amsterdam.Instance); - Assert.That(intrinsicGas.Standard.Value, Is.EqualTo(GasCostOf.Transaction + GasCostOf.PerAuthBaseRegular)); + // Amsterdam (EIP-2780 + EIP-8038): TX_BASE=12000; the value-bearing recipient touch adds + // COLD_ACCOUNT_ACCESS + TRANSFER_LOG + TX_VALUE; the authorization adds ACCOUNT_WRITE + base. + long recipientRegular = Eip8038Constants.ColdAccountAccess + GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780; + Assert.That(intrinsicGas.Standard.Value, Is.EqualTo(GasCostOf.TransactionEip2780 + recipientRegular + Eip8038Constants.PerAuthBaseRegular)); Assert.That(intrinsicGas.Standard.StateReservoir, Is.EqualTo(GasCostOf.NewAccountState + GasCostOf.PerAuthBaseState)); } @@ -247,7 +250,9 @@ public void Eip8037_nongeneric_intrinsic_gas_includes_state_gas_for_create() .TestObject; EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, Amsterdam.Instance); - long expectedRegular = GasCostOf.Transaction + GasCostOf.CreateRegular; + // Amsterdam (EIP-2780 + EIP-8038): TX_BASE=12000, create regular = CREATE_ACCESS (+ TRANSFER_LOG + // for the value endowment), create state = NEW_ACCOUNT. + long expectedRegular = GasCostOf.TransactionEip2780 + Eip8038Constants.CreateAccess + GasCostOf.TransferLogEip2780; long expectedState = GasCostOf.CreateState; Assert.That(gas.Standard, Is.EqualTo(expectedRegular + expectedState)); Assert.That(gas.MinimalGas, Is.EqualTo(Math.Max(gas.Standard, gas.FloorGas))); @@ -261,7 +266,9 @@ public void Eip8037_nongeneric_intrinsic_gas_includes_state_gas_for_setcode() .TestObject; EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, Amsterdam.Instance); - long expectedRegular = GasCostOf.Transaction + GasCostOf.PerAuthBaseRegular; + // Amsterdam (EIP-2780 + EIP-8038): TX_BASE=12000; value-bearing recipient touch + authorization. + long recipientRegular = Eip8038Constants.ColdAccountAccess + GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780; + long expectedRegular = GasCostOf.TransactionEip2780 + recipientRegular + Eip8038Constants.PerAuthBaseRegular; long expectedState = GasCostOf.NewAccountState + GasCostOf.PerAuthBaseState; Assert.That(gas.Standard, Is.EqualTo(expectedRegular + expectedState)); } @@ -275,7 +282,7 @@ public void Eip8037_nongeneric_minimal_gas_is_at_least_regular_plus_state() .TestObject; EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, Amsterdam.Instance); - long regularPlusState = GasCostOf.Transaction + GasCostOf.CreateRegular + GasCostOf.CreateState; + long regularPlusState = GasCostOf.TransactionEip2780 + Eip8038Constants.CreateAccess + GasCostOf.TransferLogEip2780 + GasCostOf.CreateState; Assert.That(gas.MinimalGas, Is.GreaterThanOrEqualTo(regularPlusState)); } From 6240dd67b6198f1735cc6d06c547b09a11a233dc Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:29:12 +0100 Subject: [PATCH 37/39] fix: EIP-8037 refund NEW_ACCOUNT when a value CALL frame reverts or halts A value transfer to a dead recipient charges NEW_ACCOUNT state gas up-front at the *CALL. EELS generic_call refunds it on child error (the account is not created when the frame reverts/halts). Add a NewAccountCharged frame flag and refund it on the revert (VirtualMachine main loop), exceptional-halt (HandleException) and precompile-failure (HandleFailure) frame-pop paths, mirroring the create refund. Co-Authored-By: Claude Opus 4.8 --- .../Instructions/EvmInstructions.Call.cs | 10 +++++++--- .../Nethermind.Evm/VirtualMachine.cs | 20 +++++++++++++++++++ src/Nethermind/Nethermind.Evm/VmState.cs | 14 ++++++++++++- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs index a429f7c1a0d7..4d5c38d6cd7f 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs @@ -289,7 +289,7 @@ delegated is not null && return EvmExceptionType.None; } - return CreateFullCallFrame(vm, ref gas, in dataOffset, dataLength, outputOffset, outputLength, codeInfo, target, caller, codeSource, env, in callValue, gasLimitUl); + return CreateFullCallFrame(vm, ref gas, in dataOffset, dataLength, outputOffset, outputLength, codeInfo, target, caller, codeSource, env, in callValue, gasLimitUl, chargesNewAccount); [MethodImpl(MethodImplOptions.NoInlining)] static EvmExceptionType CreateFullCallFrame( @@ -305,7 +305,8 @@ static EvmExceptionType CreateFullCallFrame( Address codeSource, ExecutionEnvironment env, in UInt256 callValue, - long gasLimitUl) + long gasLimitUl, + bool newAccountCharged) { IWorldState state = vm.WorldState; // Take a snapshot of the state for potential rollback. @@ -343,7 +344,10 @@ static EvmExceptionType CreateFullCallFrame( isCreateOnPreExistingAccount: false, env: callEnv, stateForAccessLists: in vm.VmState.AccessTracker, - snapshot: in snapshot); + snapshot: in snapshot, + // EIP-8037/EIP-8038: a value transfer to a dead recipient charged NEW_ACCOUNT state gas up-front; + // refunded on this frame's failure path (revert/halt) since the account is then not created. + newAccountCharged: newAccountCharged); return EvmExceptionType.None; } diff --git a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs index 47d9e501211e..e9cf2ce43213 100644 --- a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs +++ b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs @@ -310,6 +310,12 @@ public virtual TransactionSubstate ExecuteTransaction( { CreditStateGasRefund(ref _currentState.Gas, TGasPolicy.GetCreateStateCost(in _currentState.Gas)); } + else if (previousState.NewAccountCharged) + { + // EIP-8037: the reverted *CALL did not create its (dead) recipient, so refund + // the NEW_ACCOUNT state gas the parent charged up-front for the value transfer. + CreditStateGasRefund(ref _currentState.Gas, TGasPolicy.GetNewAccountStateCost(in _currentState.Gas)); + } // Revert state changes for the previous call frame when a revert condition is signaled. HandleRevert(previousState, callResult, ref previousCallOutput); } @@ -599,6 +605,9 @@ protected TransactionSubstate HandleFailure(Exception failure, str _previousCallResult = StatusCode.FailureBytes; bool failedCreate = _currentState.ExecutionType.IsAnyCreate(); + // Captured before the pop: the failed *CALL did not create its (dead) recipient, so the parent + // refunds the NEW_ACCOUNT state gas it charged up-front for the value transfer (EIP-8037). + bool childNewAccountCharged = _currentState.NewAccountCharged; // Reset output destination and return data. _previousCallOutputDestination = UInt256.Zero; @@ -610,6 +619,10 @@ protected TransactionSubstate HandleFailure(Exception failure, str { CreditStateGasRefund(ref _currentState.Gas, TGasPolicy.GetCreateStateCost(in _currentState.Gas), trackSpillRefund: false); } + else if (childNewAccountCharged) + { + CreditStateGasRefund(ref _currentState.Gas, TGasPolicy.GetNewAccountStateCost(in _currentState.Gas), trackSpillRefund: false); + } shouldExit = false; return default; @@ -781,6 +794,9 @@ protected TransactionSubstate HandleException(scoped in CallResult callResult, s _previousCallResult = StatusCode.FailureBytes; bool failedCreate = _currentState.ExecutionType.IsAnyCreate(); + // Captured before the pop: the halted *CALL did not create its (dead) recipient, so the parent + // refunds the NEW_ACCOUNT state gas it charged up-front for the value transfer (EIP-8037). + bool childNewAccountCharged = _currentState.NewAccountCharged; // Reset output destination and clear return data. _previousCallOutputDestination = UInt256.Zero; @@ -792,6 +808,10 @@ protected TransactionSubstate HandleException(scoped in CallResult callResult, s { CreditStateGasRefund(ref _currentState.Gas, TGasPolicy.GetCreateStateCost(in _currentState.Gas), trackSpillRefund: false); } + else if (childNewAccountCharged) + { + CreditStateGasRefund(ref _currentState.Gas, TGasPolicy.GetNewAccountStateCost(in _currentState.Gas), trackSpillRefund: false); + } // Return null to indicate that the failure was handled and execution should continue in the parent frame. shouldExit = false; diff --git a/src/Nethermind/Nethermind.Evm/VmState.cs b/src/Nethermind/Nethermind.Evm/VmState.cs index 8e3f4ba45573..c329fe545bf9 100644 --- a/src/Nethermind/Nethermind.Evm/VmState.cs +++ b/src/Nethermind/Nethermind.Evm/VmState.cs @@ -43,6 +43,13 @@ public class VmState : IDisposable public bool IsContinuation { get; set; } // TODO: move to CallEnv public bool IsCreateOnPreExistingAccount { get; private set; } // TODO: move to CallEnv + /// + /// EIP-8037/EIP-8038: the parent *CALL charged NEW_ACCOUNT state gas up-front for a value + /// transfer materialising this (previously dead) recipient. If this frame errors or reverts the + /// account is not created, so the parent refunds that state gas on the frame's failure path. + /// + public bool NewAccountCharged { get; private set; } // TODO: move to CallEnv + private bool _isDisposed = true; private EvmPooledMemory _memory; @@ -69,6 +76,7 @@ public static VmState RentTopLevel( isTopLevel: true, isStatic: false, isCreateOnPreExistingAccount: false, + newAccountCharged: false, env: env, stateForAccessLists: accessedItems, snapshot: snapshot); @@ -88,7 +96,8 @@ public static VmState RentFrame( ExecutionEnvironment env, in StackAccessTracker stateForAccessLists, in Snapshot snapshot, - bool isTopLevel = false) + bool isTopLevel = false, + bool newAccountCharged = false) { VmState state = Rent(); state.Initialize( @@ -99,6 +108,7 @@ public static VmState RentFrame( isTopLevel: isTopLevel, isStatic: isStatic, isCreateOnPreExistingAccount: isCreateOnPreExistingAccount, + newAccountCharged: newAccountCharged, env: env, stateForAccessLists: stateForAccessLists, snapshot: snapshot); @@ -117,6 +127,7 @@ private void Initialize( bool isTopLevel, bool isStatic, bool isCreateOnPreExistingAccount, + bool newAccountCharged, ExecutionEnvironment env, in StackAccessTracker stateForAccessLists, in Snapshot snapshot) @@ -146,6 +157,7 @@ private void Initialize( IsStatic = isStatic; IsContinuation = false; IsCreateOnPreExistingAccount = isCreateOnPreExistingAccount; + NewAccountCharged = newAccountCharged; if (!_isDisposed) { From 682818bc4ee6bac5e0833e4e1590149e6120f426 Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 24 Jun 2026 13:32:50 +0100 Subject: [PATCH 38/39] fix: EIP-7928 avoid spurious BAL read from IsContract in account-access gas The hasCode argument to ConsumeAccountAccessGas evaluated WorldState.IsContract(addr) to select the EIP-2780 two-tier cold cost, but IsContract records a BAL account read. Under EIP-8038 the cold cost is flat (hasCode unused), so this read is spurious and wrongly lands the target in the block access list when the opcode runs out of gas before actually accessing it. Short-circuit IsContract when EIP-8038 is active. Co-Authored-By: Claude Opus 4.8 --- .../Nethermind.Evm/Instructions/EvmInstructions.Call.cs | 2 +- .../Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs | 4 ++-- .../Instructions/EvmInstructions.ControlFlow.cs | 2 +- .../Instructions/EvmInstructions.Environment.cs | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs index 4d5c38d6cd7f..4633f2427944 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs @@ -162,7 +162,7 @@ public static EvmExceptionType InstructionCall( // Charge gas for account access (considering hot/cold storage costs). if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address, - hasCode: !spec.IsEip2780Enabled || vm.WorldState.IsContract(address))) + hasCode: !spec.IsEip2780Enabled || spec.IsEip8038Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; // EIP-8038 charges an extra warm access for the second DB read EXTCODECOPY performs. @@ -248,7 +248,7 @@ public static EvmExceptionType InstructionExtCodeSize( // Charge gas for accessing the account's state. if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address, - hasCode: !spec.IsEip2780Enabled || vm.WorldState.IsContract(address))) + hasCode: !spec.IsEip2780Enabled || spec.IsEip8038Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; // EIP-8038 charges an extra warm access for the second DB read EXTCODESIZE performs. diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs index 1585d4e7ca58..8bf938864cfb 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs @@ -225,7 +225,7 @@ public static EvmExceptionType InstructionSelfDestruct(Virt // Charge gas for account access. If insufficient gas remains, abort. if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address, - hasCode: !spec.IsEip2780Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; + hasCode: !spec.IsEip2780Enabled || spec.IsEip8038Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; UInt256 result = vm.WorldState.GetBalance(address); return stack.PushUInt256(in result); @@ -612,7 +612,7 @@ public static EvmExceptionType InstructionExtCodeHash( if (address is null) goto StackUnderflow; // Check if enough gas for account access and charge accordingly. if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address, - hasCode: !spec.IsEip2780Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; + hasCode: !spec.IsEip2780Enabled || spec.IsEip8038Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; IWorldState state = vm.WorldState; // For dead accounts, the specification requires pushing zero. From fed5aa68e9efb442db4e7e74621d9ac4a174a20f Mon Sep 17 00:00:00 2001 From: Marc Harvey-Hill <10379486+Marchhill@users.noreply.github.com> Date: Wed, 24 Jun 2026 14:15:58 +0100 Subject: [PATCH 39/39] fix: EIP-8037 charge top-level NEW_ACCOUNT for value transfer on the EVM path The top-frame NEW_ACCOUNT state-gas charge for a value transfer materialising a new (dead, non-precompile) recipient existed only in the simple-transfer fast path. EELS charges it for every non-create top-level frame, so transactions carrying code, calldata, or an authorization list (which take the EVM path) were missing it, leaving their block state-gas understated. Mirror the charge in ExecuteEvmTransaction. Co-Authored-By: Claude Opus 4.8 --- .../TransactionProcessing/TransactionProcessor.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index 249e1d8def12..4642104dd72f 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -299,6 +299,18 @@ private TransactionResult ExecuteEvmTransaction( if (!(result = BuildExecutionEnvironment(tx, spec, _codeInfoRepository, accessTracker, preloadedCodeInfo, preloadedDelegationAddress, out ExecutionEnvironment e))) return result; using ExecutionEnvironment env = e; + // EIP-8037 top-frame charge: a value transfer materialising a new (dead, non-precompile) + // recipient pays NEW_ACCOUNT state gas, evaluated against pre-transfer state. This mirrors the + // ExecuteSimpleTransfer charge for the EVM path (transactions carrying code, calldata, or an + // authorization list, which bypass the simple-transfer fast path). + if (spec.IsEip8037Enabled && !tx.IsContractCreation && !tx.ValueRef.IsZero + && tx.To is not null && tx.SenderAddress != tx.To + && !spec.IsPrecompile(tx.To) && WorldState.IsDeadAccount(tx.To)) + { + if (!TGasPolicy.ConsumeStateGas(ref gasAvailable, TGasPolicy.GetNewAccountStateCost(in gasAvailable))) + TGasPolicy.Consume(ref gasAvailable, TGasPolicy.GetRemainingGas(in gasAvailable)); + } + // EIP-8037 top-frame charge: a top-level call whose recipient is an EIP-7702 delegation // also touches the delegation target with a (flat) cold account access. The target is // already pre-warmed for the frame, so this is the sole charge for it. If the gas cannot