diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Constants.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Constants.cs index ac0628d85307..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.3.2"; - 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"; } diff --git a/src/Nethermind/Nethermind.Blockchain/Tracing/GasEstimator.cs b/src/Nethermind/Nethermind.Blockchain/Tracing/GasEstimator.cs index dfb9f7e35706..a1ae75b32eb1 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.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.Test/Eip8038ConstantsTests.cs b/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs new file mode 100644 index 000000000000..eab65c91d80d --- /dev/null +++ b/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs @@ -0,0 +1,95 @@ +// 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 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_the_devnet6_repriced_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(3000)); + Assert.That(warmAccess, Is.EqualTo(100)); + 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 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() + { + 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/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 } diff --git a/src/Nethermind/Nethermind.Core/Eip8038Constants.cs b/src/Nethermind/Nethermind.Core/Eip8038Constants.cs new file mode 100644 index 000000000000..e0172aa96a93 --- /dev/null +++ b/src/Nethermind/Nethermind.Core/Eip8038Constants.cs @@ -0,0 +1,59 @@ +// 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. +/// +/// +/// 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 (final values per EIP-8038, glamsterdam-devnet-6). + + /// Cold account-touch cost (COLD_ACCOUNT_ACCESS). + public const long ColdAccountAccess = 3000; // was 2600 (EIP-2929) + + /// Warm state-access cost (WARM_ACCESS). + public const long WarmAccess = GasCostOf.WarmStateRead; // 100 (unchanged) + + /// 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 = 8000; + + /// 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 + + // Derived parameters (EIP-8038 derivation formulas; recompute from the base values above). + + /// CALL_VALUE = ACCOUNT_WRITE + CALL_STIPEND. + 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; // 3000 (was 2400) + + /// Access-list storage-key entry cost, redefined to COLD_STORAGE_ACCESS. + 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; // 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.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/GasCostOf.cs b/src/Nethermind/Nethermind.Core/GasCostOf.cs index 6f370f7451ee..9fb62fb286be 100644 --- a/src/Nethermind/Nethermind.Core/GasCostOf.cs +++ b/src/Nethermind/Nethermind.Core/GasCostOf.cs @@ -92,5 +92,15 @@ 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 = 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 + // 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/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 de720176c83c..fa64e41cef1d 100644 --- a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs @@ -316,6 +316,21 @@ public interface IReleaseSpec : IEip1559Spec, IReceiptSpec /// bool IsEip6780Enabled { get; } + /// + /// EIP-8246: SELFDESTRUCT no longer burns ETH + /// + bool IsEip8246Enabled { get; } + + /// + /// EIP-8282: builder execution requests (builder deposit + builder exit predeploys). + /// + bool IsEip8282Enabled { get; } + + /// + /// EIP-8038: State-access gas cost update + /// + bool IsEip8038Enabled { get; } + /// /// EIP-8024: Backward-compatible SWAPN, DUPN, EXCHANGE /// @@ -463,6 +478,16 @@ 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. + /// + /// + /// 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; } + /// /// 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/IReleaseSpecExtensions.cs b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpecExtensions.cs index 80037bff8cc5..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 @@ -52,12 +53,13 @@ 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; 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 1a695e89f9bc..fec91d19156c 100644 --- a/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs +++ b/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs @@ -80,6 +80,9 @@ 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 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; @@ -116,6 +119,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.Core/Specs/SpecGasCosts.cs b/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs index 124848462b48..9b8950d7d653 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; @@ -108,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.Test/Eip2780Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs new file mode 100644 index 000000000000..010653476103 --- /dev/null +++ b/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs @@ -0,0 +1,104 @@ +// 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, Eip2780Spec), 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 }))); + + // 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); + + Address recipient = recipientIsNew ? TestItem.AddressF : TestItem.AddressB; + Transaction tx = Build.A.Transaction + .WithTo(recipient) + .WithValue(value) + .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/Eip2780VmTests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip2780VmTests.cs new file mode 100644 index 000000000000..471ab633ea01 --- /dev/null +++ b/src/Nethermind/Nethermind.Evm.Test/Eip2780VmTests.cs @@ -0,0 +1,102 @@ +// 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)); + } + + [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.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.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 new file mode 100644 index 000000000000..182c3a3b88b1 --- /dev/null +++ b/src/Nethermind/Nethermind.Evm.Test/Eip8038Tests.cs @@ -0,0 +1,140 @@ +// 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; + private long ColdAccountAccess => eip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; + + [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 + + ColdAccountAccess // cold EXTCODESIZE access (EIP-8038 repriced when enabled) + + 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 + + ColdAccountAccess // cold EXTCODECOPY access (EIP-8038 repriced when enabled) + + 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.Test/Eip8246Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip8246Tests.cs new file mode 100644 index 000000000000..0eb55ffaa9e6 --- /dev/null +++ b/src/Nethermind/Nethermind.Evm.Test/Eip8246Tests.cs @@ -0,0 +1,264 @@ +// 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 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, 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, + // 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; + + // 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; + // 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, 500_000) + .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, 500_000) // triggers selfdestruct-to-inheritor + .CallWithValue(contractA, 500_000, 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, 500_000) + .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, 500_000).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); + } + + [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) + .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.Test/IntrinsicGasCalculatorTests.cs b/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs index dc2b65d64693..cf16fc32c823 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 @@ -232,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)); } @@ -244,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))); @@ -258,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)); } @@ -272,8 +282,118 @@ 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)); } + + // 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; // 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 + 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.SelfTransfer, (UInt256)1, TxBaseEip2780) + .SetName("Eip2780_intrinsic_self_transfer_4500"); + yield return new TestCaseData(Recipient.EmptyZeroValue, (UInt256)0, TxBaseEip2780 + ColdNoCode) + .SetName("Eip2780_intrinsic_no_transfer_to_empty_5000"); + } + + [TestCaseSource(nameof(Eip2780IntrinsicCases))] + public void Eip2780_intrinsic_gas_is_calculated_properly(Recipient recipient, UInt256 value, long expectedStandard) + { + OverridableReleaseSpec spec = new(Prague.Instance) { IsEip2780Enabled = true, IsEip7708Enabled = 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); + state.IsContract(Arg.Any
()).Returns(recipient is Recipient.Contract); + + 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_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, 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; + 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() + { + OverridableReleaseSpec spec = new(Prague.Instance) { IsEip2780Enabled = true, IsEip7708Enabled = 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, IsEip7708Enabled = 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/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/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 8ee0dcc5ed8b..43961fc843cd 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -2,9 +2,12 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; using Nethermind.Core; +using Nethermind.Core.Eip2930; using Nethermind.Core.Specs; +using Nethermind.Evm.State; using Nethermind.Int256; namespace Nethermind.Evm.GasPolicy; @@ -213,7 +216,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 +230,20 @@ 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-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.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess + : spec.IsEip2780Enabled && !hasCode ? GasCostOf.ColdAccountAccessNoCodeEip2780 + : GasCostOf.ColdAccountAccess; + public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, ref readonly StackAccessTracker accessTracker, bool isTracingAccess, @@ -247,8 +259,10 @@ public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, } if (accessTracker.WarmUp(in storageCell)) - return UpdateGas(ref gas, GasCostOf.ColdSLoad); - if (storageAccessType == StorageAccessType.SLOAD) + return UpdateGas(ref gas, spec.IsEip8038Enabled ? Eip8038Constants.ColdStorageAccess : GasCostOf.ColdSLoad); + // 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; } @@ -291,12 +305,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), }; } @@ -383,28 +401,42 @@ 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) => UpdateGas(ref gas, GasCostOf.CallValue); + // 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, IReleaseSpec spec) + { + if (spec.IsEip8038Enabled) + return UpdateGas(ref gas, Eip8038Constants.CallValue); + + 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 +484,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 + + Eip2780ExtraGas(tx, spec, worldState); long floorCost = IGasPolicy.CalculateFloorCost(tx, spec, tokensInCallData, floorTokensInAccessList); long createStateCost = CreateStateCost(tx, spec); long totalStateCost = authStateCost + createStateCost; @@ -505,7 +539,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)] @@ -516,4 +552,107 @@ 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 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. + /// + /// + /// 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) + return hasValue ? GasCostOf.TransferLogEip2780 : 0; + + // Self-transfers coalesce into the sender leaf write already priced into TX_BASE_COST. + if (tx.SenderAddress == tx.To) return 0; + + long cost = Eip8038Constants.ColdAccountAccess; + if (hasValue) + cost += GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780; + + 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 += 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 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; + } + } diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs index 76ea2de0f9f1..a9c133ba441a 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, 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); 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); @@ -172,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] @@ -195,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); @@ -210,12 +222,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..4633f2427944 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), - }; + // 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); @@ -238,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)); @@ -271,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( @@ -287,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. @@ -325,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/Instructions/EvmInstructions.CodeCopy.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs index 5bc88f743229..6efcb4c6f115 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs @@ -168,7 +168,12 @@ 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 || spec.IsEip8038Enabled || 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) @@ -242,7 +247,12 @@ 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 || spec.IsEip8038Enabled || 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); diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs index ddc59e557940..8bf938864cfb 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 !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; @@ -262,9 +274,10 @@ public static EvmExceptionType InstructionSelfDestruct(executingAccount, inheritor, result); 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(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 || spec.IsEip8038Enabled || 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 || spec.IsEip8038Enabled || 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/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 /// 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 04600f9541c1..4642104dd72f 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -299,6 +299,35 @@ 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 + // 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.IsEip8037Enabled && !tx.IsContractCreation && recipientIsDelegated) + { + 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); @@ -313,6 +342,9 @@ private TransactionResult ExecuteEvmTransaction( if (destroyList is not null) { 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); @@ -320,26 +352,39 @@ 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); + if (tracingRefunds) tracer.ReportRefund(destroyRefund); } SafeArrayPool
.Shared.Return(buffer); } else if (count == 1) { - FinalizeDestroyedAccount(WorldState, in substate, destroyList.First); + FinalizeDestroyedAccount(WorldState, in substate, destroyList.First, removeSelfdestructBurn); + if (tracingRefunds) tracer.ReportRefund(destroyRefund); } } - 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) + // 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)); } 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); + } } } @@ -372,16 +417,31 @@ private TransactionResult ExecuteSimpleTransfer( bool senderIsRecipient = tx.SenderAddress == recipient; bool isTracingActions = tracer.IsTracingActions; + // 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.IsEip8037Enabled && 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]; @@ -413,7 +473,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) { @@ -609,7 +669,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; } @@ -688,6 +751,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 { @@ -966,7 +1036,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) { @@ -1193,6 +1263,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) @@ -1266,23 +1341,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); @@ -1294,7 +1376,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"); @@ -1536,11 +1618,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) @@ -1675,8 +1760,7 @@ private static long Calculate8037BlockRegularGas( initialRegularGas, remainingRegularGas, stateGasSpill, - stateGasSpillReclassified, - floorGas); + stateGasSpillReclassified); } protected virtual void PayRefund(Transaction tx, UInt256 refundAmount, IReleaseSpec spec) diff --git a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs index 3dff3674a4de..e9cf2ce43213 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)); + } } } } @@ -302,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); } @@ -591,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; @@ -602,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; @@ -773,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; @@ -784,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) { diff --git a/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs b/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs index 5dc00a3dfd49..5efc05964d02 100644 --- a/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs @@ -94,6 +94,9 @@ 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 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; @@ -124,6 +127,8 @@ 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; + // 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; } diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs index 6ca9383afe49..145705384762 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs @@ -184,6 +184,10 @@ 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? 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 419be4dd236a..f43cead9b306 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs @@ -323,6 +323,9 @@ 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; + 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; @@ -348,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 420c6be23c73..6a2c00ace2a7 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs @@ -213,8 +213,12 @@ bool GetForInnerPathExistence(KeyValuePair o) => Eip7708TransitionTimestamp = chainSpecJson.Params.Eip7708TransitionTimestamp, 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, }; 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 d74a7b28d2ae..3b744dbaacbd 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs @@ -191,8 +191,12 @@ 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? Eip8038TransitionTimestamp { get; set; } + public ulong? Eip8282TransitionTimestamp { 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/Forks/25_Amsterdam.cs b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs index c13d697b7afe..79e1c672c447 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,6 +22,9 @@ public override void Apply(NamedReleaseSpec spec) spec.MaxCodeSize = CodeSizeConstants.MaxCodeSizeEip7954; spec.IsEip8024Enabled = true; 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 36a58d8affe4..1a6356b6dd6a 100644 --- a/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs @@ -89,6 +89,9 @@ 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 IsEip8038Enabled { get; set; } + public bool IsEip8282Enabled { get; set; } public bool IsEip4788Enabled { get; set; } public bool IsEip7702Enabled { get; set; } public bool IsEip7823Enabled { get; set; } @@ -169,6 +172,7 @@ public virtual FrozenSet BuildPrecompilesCache() public bool IsEip7708Enabled { get; set; } public bool IsEip7954Enabled { get; set; } + public bool IsEip2780Enabled { get; set; } private ReleaseSpec? _systemSpec;