diff --git a/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs b/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs index 8ae66c23c39..eab65c91d80 100644 --- a/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs +++ b/src/Nethermind/Nethermind.Core.Test/Eip8038ConstantsTests.cs @@ -7,14 +7,14 @@ namespace Nethermind.Core.Test; /// -/// Pins the EIP-8038 gas parameters to the spec's derivation formulas. The base values are -/// placeholders equal to the current (pre-8038) costs while the EIP is a Draft; these tests -/// guard the relationships so the derived values stay correct when the final figures land. +/// Pins the EIP-8038 gas parameters to the final repriced values scheduled in Amsterdam by +/// glamsterdam-devnet-6, and guards the derivation relationships so the derived values stay +/// consistent with the base parameters. /// public class Eip8038ConstantsTests { [Test] - public void Base_parameters_match_their_current_placeholder_values() + public void Base_parameters_match_the_devnet6_repriced_values() { long coldAccountAccess = Eip8038Constants.ColdAccountAccess; long warmAccess = Eip8038Constants.WarmAccess; @@ -25,21 +25,24 @@ public void Base_parameters_match_their_current_placeholder_values() Assert.Multiple(() => { - Assert.That(coldAccountAccess, Is.EqualTo(2600)); + Assert.That(coldAccountAccess, Is.EqualTo(3000)); Assert.That(warmAccess, Is.EqualTo(100)); - Assert.That(coldStorageAccess, Is.EqualTo(2100)); - Assert.That(accountWrite, Is.EqualTo(6700)); - Assert.That(storageWrite, Is.EqualTo(2800)); + Assert.That(coldStorageAccess, Is.EqualTo(3000)); + Assert.That(accountWrite, Is.EqualTo(8000)); + Assert.That(storageWrite, Is.EqualTo(10000)); Assert.That(callStipend, Is.EqualTo(2300)); }); } [Test] - public void Account_write_is_call_value_minus_stipend() - { - long accountWrite = Eip8038Constants.AccountWrite; - Assert.That(accountWrite, Is.EqualTo(GasCostOf.CallValue - GasCostOf.CallStipend)); - } + public void Derived_parameters_match_the_devnet6_repriced_values() => + Assert.Multiple(() => + { + Assert.That(Eip8038Constants.CallValue, Is.EqualTo(10300)); + Assert.That(Eip8038Constants.CreateAccess, Is.EqualTo(11000)); + Assert.That(Eip8038Constants.StorageClearRefund, Is.EqualTo(12480)); + Assert.That(Eip8038Constants.PerAuthBaseRegular, Is.EqualTo(15816)); + }); [Test] public void Call_value_is_account_write_plus_stipend() diff --git a/src/Nethermind/Nethermind.Core/CodeSizeConstants.cs b/src/Nethermind/Nethermind.Core/CodeSizeConstants.cs index bcd2406faf9..8d5a4a06e91 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 index c85d78caf55..e0172aa96a9 100644 --- a/src/Nethermind/Nethermind.Core/Eip8038Constants.cs +++ b/src/Nethermind/Nethermind.Core/Eip8038Constants.cs @@ -8,29 +8,27 @@ namespace Nethermind.Core; /// (regular + state) gas model. /// /// -/// The EIP is a Draft: the final repriced values are still TBD. The base parameters below are -/// therefore placeholders equal to the current (pre-8038) costs, so that enabling the EIP is a no-op -/// for the base constants until the final figures land. The derived parameters are expressed via the -/// EIP's derivation formulas so they recompute automatically once the base values are finalized. +/// Scheduled in Amsterdam by glamsterdam-devnet-6 with the final repriced values below. The derived +/// parameters are expressed via the EIP's derivation formulas so they recompute from the base values. /// public static class Eip8038Constants { - // Base parameters (placeholders == current values; final values TBD). + // Base parameters (final values per EIP-8038, glamsterdam-devnet-6). - /// Cold account-touch cost (EIP-2929 COLD_ACCOUNT_ACCESS). - public const long ColdAccountAccess = GasCostOf.ColdAccountAccess; // 2600 + /// Cold account-touch cost (COLD_ACCOUNT_ACCESS). + public const long ColdAccountAccess = 3000; // was 2600 (EIP-2929) - /// Warm state-access cost (EIP-2929 WARM_ACCESS). - public const long WarmAccess = GasCostOf.WarmStateRead; // 100 + /// Warm state-access cost (WARM_ACCESS). + public const long WarmAccess = GasCostOf.WarmStateRead; // 100 (unchanged) - /// Cold storage-slot access cost (EIP-2929 COLD_STORAGE_ACCESS). - public const long ColdStorageAccess = GasCostOf.ColdSLoad; // 2100 + /// Cold storage-slot access cost (COLD_STORAGE_ACCESS). + public const long ColdStorageAccess = 3000; // was 2100 (EIP-2929) /// The account-write component of value-bearing *CALLs (CALL_VALUE - CALL_STIPEND). - public const long AccountWrite = GasCostOf.CallValue - GasCostOf.CallStipend; // 6700 + public const long AccountWrite = 8000; - /// The regular-gas write component of SSTORE (GAS_STORAGE_UPDATE - COLD_STORAGE_ACCESS - WARM_ACCESS). - public const long StorageWrite = GasCostOf.SReset - ColdStorageAccess - WarmAccess; // 2800 + /// The regular-gas write component of SSTORE (STORAGE_WRITE). + public const long StorageWrite = 10000; /// Stipend forwarded with a value-bearing call (unchanged from EIP-2929). public const long CallStipend = GasCostOf.CallStipend; // 2300 @@ -38,17 +36,24 @@ public static class Eip8038Constants // Derived parameters (EIP-8038 derivation formulas; recompute from the base values above). /// CALL_VALUE = ACCOUNT_WRITE + CALL_STIPEND. - public const long CallValue = AccountWrite + CallStipend; // 9000 + public const long CallValue = AccountWrite + CallStipend; // 10300 /// CREATE_ACCESS = ACCOUNT_WRITE + COLD_STORAGE_ACCESS, charged in regular gas by CREATE/CREATE2. public const long CreateAccess = AccountWrite + ColdStorageAccess; /// Access-list address entry cost, redefined to COLD_ACCOUNT_ACCESS. - public const long AccessListAddressCost = ColdAccountAccess; // 2600 (was 2400) + public const long AccessListAddressCost = ColdAccountAccess; // 3000 (was 2400) /// Access-list storage-key entry cost, redefined to COLD_STORAGE_ACCESS. - public const long AccessListStorageKeyCost = ColdStorageAccess; // 2100 (was 1900) + public const long AccessListStorageKeyCost = ColdStorageAccess; // 3000 (was 1900) /// STORAGE_CLEAR_REFUND = (STORAGE_WRITE + COLD_STORAGE_ACCESS) * 4800 / 5000. - public const long StorageClearRefund = (StorageWrite + ColdStorageAccess) * 4800 / 5000; + public const long StorageClearRefund = (StorageWrite + ColdStorageAccess) * 4800 / 5000; // 12480 + + /// + /// 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/GasCostOf.cs b/src/Nethermind/Nethermind.Core/GasCostOf.cs index 4e82ca45de2..9fb62fb286b 100644 --- a/src/Nethermind/Nethermind.Core/GasCostOf.cs +++ b/src/Nethermind/Nethermind.Core/GasCostOf.cs @@ -93,7 +93,8 @@ public static class GasCostOf public const long MinModExpEip7883 = 500; // eip-7883 // eip-2780: reduce intrinsic transaction gas and reprice state-touching primitives. - public const long TransactionEip2780 = 4500; // TX_BASE_COST = 3000 ECRECOVER + 1000 STATE_UPDATE + 500 sender cold no-code + public const long TransactionEip2780 = 12000; // TX_BASE_COST: ECDSA recovery + sender account access + sender account write + public const long TxValueCostEip2780 = 4244; // recipient balance write for a value-bearing transfer (non-create) public const long StateUpdateEip2780 = 1000; // one account-leaf write (nonce/balance coalesced) public const long ColdAccountAccessNoCodeEip2780 = 500; // cold touch of an account known to have no code public const long TransferLogEip2780 = 1756; // eip-7708 LOG3 transfer event: 375 + 3*375 + 32*8 diff --git a/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs b/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs index 124848462b4..9b8950d7d65 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 index f4bdf7af6d9..01065347610 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Eip2780Tests.cs @@ -29,7 +29,7 @@ public class Eip2780Tests private static long ChargeCallValue(bool isSelfCall, bool recipientEmpty) { EthereumGasPolicy gas = EthereumGasPolicy.FromLong(1_000_000); - Assert.That(EthereumGasPolicy.ConsumeCallValueTransferEip2780(ref gas, isSelfCall, recipientEmpty), Is.True); + Assert.That(EthereumGasPolicy.ConsumeCallValueTransferEip2780(ref gas, isSelfCall, recipientEmpty, Eip2780Spec), Is.True); return 1_000_000 - EthereumGasPolicy.GetRemainingGas(in gas); } diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip8037BlockGasInclusionCheckTests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip8037BlockGasInclusionCheckTests.cs index 23074d114ed..9bd575ce489 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 79d3437c745..5de6064825f 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Eip8037Tests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Eip8037Tests.cs @@ -183,11 +183,14 @@ public void Amsterdam_access_list_floor_pricing_is_added_to_regular_and_floor_in IntrinsicGas splitIntrinsicGas = EthereumGasPolicy.CalculateIntrinsicGas(tx, Amsterdam.Instance); EthereumIntrinsicGas intrinsicGas = IntrinsicGasCalculator.Calculate(tx, Amsterdam.Instance); - long accessListBaseCost = GasCostOf.AccessAccountListEntry + 3 * GasCostOf.AccessStorageListEntry; + // Amsterdam (EIP-2780 + EIP-8038): TX_BASE=12000; access-list entries repriced to COLD_ACCOUNT_ACCESS / + // COLD_STORAGE_ACCESS; the value-bearing recipient touch adds COLD_ACCOUNT_ACCESS + TRANSFER_LOG + TX_VALUE. + long recipientRegular = Eip8038Constants.ColdAccountAccess + GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780; + long accessListBaseCost = Eip8038Constants.AccessListAddressCost + 3 * Eip8038Constants.AccessListStorageKeyCost; long accessListFloorTokens = (20L + 3 * 32L) * Amsterdam.Instance.GasCosts.TxDataNonZeroMultiplier; long accessListFloorCost = accessListFloorTokens * Amsterdam.Instance.GasCosts.TotalCostFloorPerToken; - long expectedRegular = GasCostOf.Transaction + accessListBaseCost + accessListFloorCost; - long expectedFloorGas = GasCostOf.Transaction + accessListFloorCost; + long expectedRegular = GasCostOf.TransactionEip2780 + recipientRegular + accessListBaseCost + accessListFloorCost; + long expectedFloorGas = GasCostOf.TransactionEip2780 + accessListFloorCost; Assert.That(splitIntrinsicGas.Standard.Value, Is.EqualTo(expectedRegular)); Assert.That(splitIntrinsicGas.Standard.StateReservoir, Is.Zero); @@ -270,22 +273,23 @@ public void State_refund_is_clamped_to_intrinsic_state_floor() } [Test] - public void Code_insert_state_refund_is_available_to_later_state_gas() + public void Code_insert_refund_credits_regular_gas_not_state_under_eip8038() { - const long intrinsicAuthState = GasCostOf.NewAccountState + GasCostOf.PerAuthBaseState; + // EIP-8038: the per-authorization code-insert (EIP-7702 existing-authority) refund returns the + // worst-case ACCOUNT_WRITE to the regular-gas refund counter and leaves the state-gas dimension + // untouched (the NEW_ACCOUNT / AUTH_BASE state refunds are applied separately, pre-execution). EthereumGasPolicy gas = new() { - Value = 2 * GasCostOf.SSetState - GasCostOf.NewAccountState, - StateGasUsed = intrinsicAuthState, + Value = 0, + StateReservoir = 0, + StateGasUsed = GasCostOf.PerAuthBaseState, }; - long regularRefund = EthereumGasPolicy.ApplyCodeInsertRefunds(ref gas, 1, Amsterdam.Instance, intrinsicAuthState); - Assert.That(EthereumGasPolicy.ConsumeStateGas(ref gas, GasCostOf.SSetState), Is.True); - Assert.That(EthereumGasPolicy.ConsumeStateGas(ref gas, GasCostOf.SSetState), Is.True); + long regularRefund = EthereumGasPolicy.ApplyCodeInsertRefunds(ref gas, 1, Amsterdam.Instance, stateGasFloor: 0); - Assert.That(regularRefund, Is.Zero); + Assert.That(regularRefund, Is.EqualTo(Eip8038Constants.AccountWrite)); Assert.That((gas.Value, gas.StateReservoir, gas.StateGasUsed, gas.StateGasSpill), - Is.EqualTo((0L, 0L, GasCostOf.PerAuthBaseState + 2 * GasCostOf.SSetState, 2 * GasCostOf.SSetState - GasCostOf.NewAccountState))); + Is.EqualTo((0L, 0L, GasCostOf.PerAuthBaseState, 0L))); } [Test] diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip8038Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip8038Tests.cs index b77338c0010..182c3a3b88b 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Eip8038Tests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Eip8038Tests.cs @@ -39,6 +39,7 @@ public class Eip8038Tests(bool eip8038Enabled) : VirtualMachineTestsBase private static readonly Address Target = TestItem.AddressC; private long ExtraWarmAccess => eip8038Enabled ? Eip8038Constants.WarmAccess : 0; + private long ColdAccountAccess => eip8038Enabled ? Eip8038Constants.ColdAccountAccess : GasCostOf.ColdAccountAccess; [SetUp] public override void Setup() @@ -72,7 +73,7 @@ public void ExtCodeSize_charges_extra_warm_access() Assert.That(result.StatusCode, Is.EqualTo(StatusCode.Success)); long expected = GasCostOf.Transaction + GasCostOf.VeryLow // PUSH20 target - + GasCostOf.ColdAccountAccess // cold EXTCODESIZE access + + ColdAccountAccess // cold EXTCODESIZE access (EIP-8038 repriced when enabled) + ExtraWarmAccess // EIP-8038 extra access + GasCostOf.Base; // POP AssertGas(result, expected); @@ -96,7 +97,7 @@ public void ExtCodeCopy_charges_extra_warm_access() Assert.That(result.StatusCode, Is.EqualTo(StatusCode.Success)); long expected = GasCostOf.Transaction + 4 * GasCostOf.VeryLow // three PUSH1 0x00 + PUSH20 target - + GasCostOf.ColdAccountAccess // cold EXTCODECOPY access + + ColdAccountAccess // cold EXTCODECOPY access (EIP-8038 repriced when enabled) + ExtraWarmAccess; // EIP-8038 extra access AssertGas(result, expected); } diff --git a/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs b/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs index e31ba2ba44c..cf16fc32c82 100644 --- a/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs @@ -235,7 +235,10 @@ public void Eip8037_policy_intrinsic_gas_splits_authorization_cost() .TestObject; IntrinsicGas intrinsicGas = EthereumGasPolicy.CalculateIntrinsicGas(tx, Amsterdam.Instance); - Assert.That(intrinsicGas.Standard.Value, Is.EqualTo(GasCostOf.Transaction + GasCostOf.PerAuthBaseRegular)); + // Amsterdam (EIP-2780 + EIP-8038): TX_BASE=12000; the value-bearing recipient touch adds + // COLD_ACCOUNT_ACCESS + TRANSFER_LOG + TX_VALUE; the authorization adds ACCOUNT_WRITE + base. + long recipientRegular = Eip8038Constants.ColdAccountAccess + GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780; + Assert.That(intrinsicGas.Standard.Value, Is.EqualTo(GasCostOf.TransactionEip2780 + recipientRegular + Eip8038Constants.PerAuthBaseRegular)); Assert.That(intrinsicGas.Standard.StateReservoir, Is.EqualTo(GasCostOf.NewAccountState + GasCostOf.PerAuthBaseState)); } @@ -247,7 +250,9 @@ public void Eip8037_nongeneric_intrinsic_gas_includes_state_gas_for_create() .TestObject; EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, Amsterdam.Instance); - long expectedRegular = GasCostOf.Transaction + GasCostOf.CreateRegular; + // Amsterdam (EIP-2780 + EIP-8038): TX_BASE=12000, create regular = CREATE_ACCESS (+ TRANSFER_LOG + // for the value endowment), create state = NEW_ACCOUNT. + long expectedRegular = GasCostOf.TransactionEip2780 + Eip8038Constants.CreateAccess + GasCostOf.TransferLogEip2780; long expectedState = GasCostOf.CreateState; Assert.That(gas.Standard, Is.EqualTo(expectedRegular + expectedState)); Assert.That(gas.MinimalGas, Is.EqualTo(Math.Max(gas.Standard, gas.FloorGas))); @@ -261,7 +266,9 @@ public void Eip8037_nongeneric_intrinsic_gas_includes_state_gas_for_setcode() .TestObject; EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, Amsterdam.Instance); - long expectedRegular = GasCostOf.Transaction + GasCostOf.PerAuthBaseRegular; + // Amsterdam (EIP-2780 + EIP-8038): TX_BASE=12000; value-bearing recipient touch + authorization. + long recipientRegular = Eip8038Constants.ColdAccountAccess + GasCostOf.TransferLogEip2780 + GasCostOf.TxValueCostEip2780; + long expectedRegular = GasCostOf.TransactionEip2780 + recipientRegular + Eip8038Constants.PerAuthBaseRegular; long expectedState = GasCostOf.NewAccountState + GasCostOf.PerAuthBaseState; Assert.That(gas.Standard, Is.EqualTo(expectedRegular + expectedState)); } @@ -275,7 +282,7 @@ public void Eip8037_nongeneric_minimal_gas_is_at_least_regular_plus_state() .TestObject; EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, Amsterdam.Instance); - long regularPlusState = GasCostOf.Transaction + GasCostOf.CreateRegular + GasCostOf.CreateState; + long regularPlusState = GasCostOf.TransactionEip2780 + Eip8038Constants.CreateAccess + GasCostOf.TransferLogEip2780 + GasCostOf.CreateState; Assert.That(gas.MinimalGas, Is.GreaterThanOrEqualTo(regularPlusState)); } diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/Eip8037BlockGasInclusionCheck.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/Eip8037BlockGasInclusionCheck.cs index 3412bd2fc0b..07aaafc0c6b 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 bafc87a46a4..43961fc843c 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -236,12 +236,12 @@ public static bool ConsumeAccountAccessGas(ref EthereumGasPolicy gas, }; } - // EIP-2780 prices a cold touch of a code-less account cheaper than one with code. - // EIP-8038 otherwise reprices the cold account-access cost. + // EIP-8038 reprices the (flat) cold account-access cost. devnet-6 dropped the earlier + // EIP-2780 two-tier no-code discount, so the touch is independent of whether the account has code. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long ColdAccountAccessCost(IReleaseSpec spec, bool hasCode) => - spec.IsEip2780Enabled && !hasCode ? GasCostOf.ColdAccountAccessNoCodeEip2780 - : spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess + spec.IsEip8038Enabled ? Eip8038Constants.ColdAccountAccess + : spec.IsEip2780Enabled && !hasCode ? GasCostOf.ColdAccountAccessNoCodeEip2780 : GasCostOf.ColdAccountAccess; public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, @@ -260,7 +260,9 @@ public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, if (accessTracker.WarmUp(in storageCell)) return UpdateGas(ref gas, spec.IsEip8038Enabled ? Eip8038Constants.ColdStorageAccess : GasCostOf.ColdSLoad); - if (storageAccessType == StorageAccessType.SLOAD) + // EIP-8038 charges the warm-access cost on SSTORE too (the net-metered charge is dropped); + // pre-8038, a warm SSTORE access is free here and the warm cost comes from net metering. + if (storageAccessType == StorageAccessType.SLOAD || spec.IsEip8038Enabled) return UpdateGas(ref gas, GasCostOf.WarmStateRead); return true; } @@ -303,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), }; } @@ -395,32 +401,36 @@ 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: subsumes the legacy CallValue + NewAccount charges. + // EIP-2780 value-moving call cost. Under EIP-8038 a value-bearing call charges a flat CALL_VALUE + // (the new-account surcharge moves to a separate NEW_ACCOUNT state charge); the earlier draft used + // a three-tier charge keyed on self-call and recipient existence that subsumed the surcharge. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool ConsumeCallValueTransferEip2780(ref EthereumGasPolicy gas, bool isSelfCall, bool recipientEmpty) + public static bool ConsumeCallValueTransferEip2780(ref EthereumGasPolicy gas, bool isSelfCall, bool recipientEmpty, IReleaseSpec spec) { + if (spec.IsEip8038Enabled) + return UpdateGas(ref gas, Eip8038Constants.CallValue); + long cost = isSelfCall ? GasCostOf.CallValueSelfEip2780 : recipientEmpty ? GasCostOf.CallValueNewAccountEip2780 : GasCostOf.CallValueExistingEip2780; @@ -529,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)] @@ -541,18 +553,51 @@ private static long DataCost(Transaction tx, IReleaseSpec spec, long tokensInCal spec.GetBaseDataCost(tx) + tokensInCallData * GasCostOf.TxDataZero; /// - /// EIP-2780 charges on top of TX_BASE_COST: the EIP-7708 transfer log, the new-account surcharge, - /// and the recipient cold/warm touch plus its value-transfer STATE_UPDATE. + /// EIP-2780 recipient charge on top of TX_BASE_COST. Dispatches to the EIP-8038 (glamsterdam-devnet-6) + /// flat model when EIP-8038 is active, otherwise to the standalone EIP-2780 two-tier model. /// /// - /// The recipient touch overrides EIP-2929's "all tx addresses are warm" rule. It is priced here - /// (where pre-state is available) rather than mid-execution; the recipient remains pre-warmed, so - /// no opcode re-charges it. Requires for the state-dependent parts. + /// 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; @@ -609,4 +654,5 @@ private static long RecipientTouchCost(IReleaseSpec spec, IReadOnlyStateProvider } return set; } + } diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs index 4574bb88be0..a9c133ba441 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs @@ -134,7 +134,7 @@ static virtual long ApplyCodeInsertRefunds(ref TSelf gas, int codeInsertRefunds, static abstract bool ConsumeCallValueTransfer(ref TSelf gas); // EIP-2780 three-tier value-moving call cost replacing the legacy CallValue + NewAccount charges. - static abstract bool ConsumeCallValueTransferEip2780(ref TSelf gas, bool isSelfCall, bool recipientEmpty); + static abstract bool ConsumeCallValueTransferEip2780(ref TSelf gas, bool isSelfCall, bool recipientEmpty, IReleaseSpec spec); static abstract bool ConsumeNewAccountCreation(ref TSelf gas) where TEip8037 : struct, IFlag; static abstract bool ConsumeLogEmission(ref TSelf gas, long topicCount, long dataSize); static abstract TSelf Max(in TSelf a, in TSelf b); @@ -205,9 +205,11 @@ public static (long RegularCost, long StateCost) AuthorizationListCost(Transacti } long authCount = authList.Length; + // EIP-8038 reprices the per-authorization regular cost (ACCOUNT_WRITE + auth-base). + long perAuthRegular = spec.IsEip8038Enabled ? Eip8038Constants.PerAuthBaseRegular : GasCostOf.PerAuthBaseRegular; return spec.IsEip8037Enabled ? ( - authCount * GasCostOf.PerAuthBaseRegular, + authCount * perAuthRegular, authCount * (GasCostOf.NewAccountState + GasCostOf.PerAuthBaseState) ) : (authCount * GasCostOf.NewAccount, 0); diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs index 18a31f275e3..4633f242794 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs @@ -147,7 +147,7 @@ public static EvmExceptionType InstructionCall !state.AccountExists(target), - true => hasValueTransfer && state.IsDeadAccount(target), - }); + // EIP-8038 charges a value transfer to a dead recipient the NEW_ACCOUNT state cost (separate + // from the flat CALL_VALUE above). The earlier EIP-2780 draft folded creation into the + // value-transfer tier, so it charges nothing extra here. + bool chargesNewAccount = spec.IsEip8038Enabled + ? hasValueTransfer && state.IsDeadAccount(target) + : !spec.IsEip2780Enabled && (spec.ClearEmptyAccountWhenTouched switch + { + false => !state.AccountExists(target), + true => hasValueTransfer && state.IsDeadAccount(target), + }); bool newAccountOutOfGas = chargesNewAccount && !TGasPolicy.ConsumeNewAccountCreation(ref gas); @@ -247,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)); @@ -280,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( @@ -296,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. @@ -334,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 e3780702b6e..6efcb4c6f11 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.CodeCopy.cs @@ -169,7 +169,7 @@ public static EvmExceptionType InstructionExtCodeCopy( // Charge gas for account access (considering hot/cold storage costs). if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address, - hasCode: !spec.IsEip2780Enabled || vm.WorldState.IsContract(address))) + hasCode: !spec.IsEip2780Enabled || spec.IsEip8038Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; // EIP-8038 charges an extra warm access for the second DB read EXTCODECOPY performs. @@ -248,7 +248,7 @@ public static EvmExceptionType InstructionExtCodeSize( // Charge gas for accessing the account's state. if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address, - hasCode: !spec.IsEip2780Enabled || vm.WorldState.IsContract(address))) + hasCode: !spec.IsEip2780Enabled || spec.IsEip8038Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; // EIP-8038 charges an extra warm access for the second DB read EXTCODESIZE performs. diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs index e9196fd7fdd..8bf938864cf 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs @@ -225,7 +225,7 @@ public static EvmExceptionType InstructionSelfDestruct !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; diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs index 948995bae57..4ff65e86d0c 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 // Charge gas for account access. If insufficient gas remains, abort. if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address, - hasCode: !spec.IsEip2780Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; + hasCode: !spec.IsEip2780Enabled || spec.IsEip8038Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; UInt256 result = vm.WorldState.GetBalance(address); return stack.PushUInt256(in result); @@ -612,7 +612,7 @@ public static EvmExceptionType InstructionExtCodeHash( if (address is null) goto StackUnderflow; // Check if enough gas for account access and charge accordingly. if (!TGasPolicy.ConsumeAccountAccessGas(ref gas, spec, in vm.VmState.AccessTracker, vm.TxTracer.IsTracingAccess, address, - hasCode: !spec.IsEip2780Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; + hasCode: !spec.IsEip2780Enabled || spec.IsEip8038Enabled || vm.WorldState.IsContract(address))) goto OutOfGas; IWorldState state = vm.WorldState; // For dead accounts, the specification requires pushing zero. diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Storage.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Storage.cs index 2d34bf5a34f..96d7e7a0a59 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(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); @@ -388,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]; @@ -429,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) { @@ -625,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; } @@ -704,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 { @@ -1209,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) @@ -1317,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"); @@ -1559,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) @@ -1698,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 3dff3674a4d..e9cf2ce4321 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 8e3f4ba4557..c329fe545bf 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/Forks/25_Amsterdam.cs b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs index 424b474ba53..c014b7073f7 100644 --- a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs +++ b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs @@ -11,6 +11,7 @@ public class Amsterdam() : NamedReleaseSpec(BPO2.Instance) public override void Apply(NamedReleaseSpec spec) { spec.Name = "Amsterdam"; + spec.IsEip2780Enabled = true; spec.IsEip7976Enabled = true; spec.IsEip7981Enabled = true; spec.IsEip7708Enabled = true; @@ -21,9 +22,8 @@ public override void Apply(NamedReleaseSpec spec) spec.MaxCodeSize = CodeSizeConstants.MaxCodeSizeEip7954; spec.IsEip8024Enabled = true; spec.IsEip8037Enabled = true; - // EIP-8246 is implemented but stays off here: it is still a Draft and not part of the - // EEST `for_amsterdam` fixtures, so enabling it would diverge from conformance tests. - // It can be activated via the Eip8246Transition chainspec parameter when scheduled. + spec.IsEip8038Enabled = true; + spec.IsEip8246Enabled = true; spec.EngineApiNewPayloadVersion = EngineApiVersions.NewPayload.V5; spec.EngineApiGetPayloadVersion = EngineApiVersions.GetPayload.V6; spec.EngineApiForkchoiceVersion = EngineApiVersions.Fcu.V4;