diff --git a/cspell.json b/cspell.json index 938c3a407fd..4271699cda9 100644 --- a/cspell.json +++ b/cspell.json @@ -183,6 +183,7 @@ "contentfiles", "corechart", "corinfo", + "CPSB", "cpufrequency", "crummey", "cryptosuite", @@ -544,6 +545,7 @@ "nodetype", "nofile", "nongcstatic", + "nongeneric", "noninteractive", "nonposdao", "nonstring", @@ -746,6 +748,7 @@ "squarify", "stackalloc", "srcset", + "sset", "ssse", "sstfiles", "sstore", diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Constants.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Constants.cs new file mode 100644 index 00000000000..862a75bd256 --- /dev/null +++ b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Constants.cs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Ethereum.Blockchain.Pyspec.Test.Amsterdam; + +public static class Constants +{ + public const string BalArchiveVersion = "bal@v5.4.0"; + public const string BalArchiveName = "fixtures_bal.tar.gz"; +} diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7708BlockChainTests.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7708BlockChainTests.cs new file mode 100644 index 00000000000..475c6acd778 --- /dev/null +++ b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7708BlockChainTests.cs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ethereum.Test.Base; +using NUnit.Framework; + +namespace Ethereum.Blockchain.Pyspec.Test; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class Eip7708BlockChainTests : BlockchainTestBase +{ + private const string Eip7708Wildcard = "eip7708_eth_transfer_logs"; + + [TestCaseSource(nameof(LoadTests))] + public async Task Test(BlockchainTest test) => await RunTest(test); + + private static IEnumerable LoadTests() + { + TestsSourceLoader loader = new(new LoadPyspecTestsStrategy + { + ArchiveVersion = Amsterdam.Constants.BalArchiveVersion, + ArchiveName = Amsterdam.Constants.BalArchiveName + }, "fixtures/blockchain_tests", Eip7708Wildcard); + + return loader.LoadTests().OfType(); + } +} diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7708StateTests.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7708StateTests.cs new file mode 100644 index 00000000000..3ce03f953d6 --- /dev/null +++ b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7708StateTests.cs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using Ethereum.Test.Base; +using FluentAssertions; +using NUnit.Framework; + +namespace Ethereum.Blockchain.Pyspec.Test; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class Eip7708StateTests : GeneralStateTestBase +{ + private const string Eip7708Wildcard = "eip7708_eth_transfer_logs"; + + [TestCaseSource(nameof(LoadTests))] + public void Test(GeneralStateTest test) => RunTest(test).Pass.Should().BeTrue(); + + private static IEnumerable LoadTests() + { + TestsSourceLoader loader = new(new LoadPyspecTestsStrategy + { + ArchiveVersion = Amsterdam.Constants.BalArchiveVersion, + ArchiveName = Amsterdam.Constants.BalArchiveName + }, "fixtures/state_tests", Eip7708Wildcard); + + return loader.LoadTests(); + } +} diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7778BlockChainTests.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7778BlockChainTests.cs new file mode 100644 index 00000000000..58f8df21525 --- /dev/null +++ b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7778BlockChainTests.cs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ethereum.Test.Base; +using NUnit.Framework; + +namespace Ethereum.Blockchain.Pyspec.Test; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class Eip7778BlockChainTests : BlockchainTestBase +{ + private const string Eip7778Wildcard = "eip7778_block_gas_accounting_without_refunds"; + + [TestCaseSource(nameof(LoadTests))] + public async Task Test(BlockchainTest test) => await RunTest(test); + + private static IEnumerable LoadTests() + { + TestsSourceLoader loader = new(new LoadPyspecTestsStrategy + { + ArchiveVersion = Amsterdam.Constants.BalArchiveVersion, + ArchiveName = Amsterdam.Constants.BalArchiveName + }, "fixtures/blockchain_tests", Eip7778Wildcard); + + return loader.LoadTests().OfType(); + } +} diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7843BlockChainTests.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7843BlockChainTests.cs new file mode 100644 index 00000000000..79f3711ea90 --- /dev/null +++ b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7843BlockChainTests.cs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ethereum.Test.Base; +using NUnit.Framework; + +namespace Ethereum.Blockchain.Pyspec.Test; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class Eip7843BlockChainTests : BlockchainTestBase +{ + private const string Eip7843Wildcard = "eip7843_slotnum"; + + [TestCaseSource(nameof(LoadTests))] + public async Task Test(BlockchainTest test) => await RunTest(test); + + private static IEnumerable LoadTests() + { + TestsSourceLoader loader = new(new LoadPyspecTestsStrategy + { + ArchiveVersion = Amsterdam.Constants.BalArchiveVersion, + ArchiveName = Amsterdam.Constants.BalArchiveName + }, "fixtures/blockchain_tests", Eip7843Wildcard); + + return loader.LoadTests().OfType(); + } +} diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7843StateTests.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7843StateTests.cs new file mode 100644 index 00000000000..347369c7b75 --- /dev/null +++ b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7843StateTests.cs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using Ethereum.Test.Base; +using FluentAssertions; +using NUnit.Framework; + +namespace Ethereum.Blockchain.Pyspec.Test; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class Eip7843StateTests : GeneralStateTestBase +{ + private const string Eip7843Wildcard = "eip7843_slotnum"; + + [TestCaseSource(nameof(LoadTests))] + public void Test(GeneralStateTest test) => RunTest(test).Pass.Should().BeTrue(); + + private static IEnumerable LoadTests() + { + TestsSourceLoader loader = new(new LoadPyspecTestsStrategy + { + ArchiveVersion = Amsterdam.Constants.BalArchiveVersion, + ArchiveName = Amsterdam.Constants.BalArchiveName + }, "fixtures/state_tests", Eip7843Wildcard); + + return loader.LoadTests(); + } +} diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7928BlockChainTests.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7928BlockChainTests.cs new file mode 100644 index 00000000000..6ca1d65d6f4 --- /dev/null +++ b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7928BlockChainTests.cs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ethereum.Test.Base; +using NUnit.Framework; + +namespace Ethereum.Blockchain.Pyspec.Test; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class Eip7928BlockChainTests : BlockchainTestBase +{ + private const string Eip7928Wildcard = "eip7928_block_level_access_lists"; + + [TestCaseSource(nameof(LoadTests))] + public async Task Test(BlockchainTest test) => await RunTest(test); + + private static IEnumerable LoadTests() + { + TestsSourceLoader loader = new(new LoadPyspecTestsStrategy + { + ArchiveVersion = Amsterdam.Constants.BalArchiveVersion, + ArchiveName = Amsterdam.Constants.BalArchiveName + }, "fixtures/blockchain_tests", Eip7928Wildcard); + + return loader.LoadTests().OfType(); + } +} diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7954BlockChainTests.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7954BlockChainTests.cs new file mode 100644 index 00000000000..cfff82448de --- /dev/null +++ b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7954BlockChainTests.cs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ethereum.Test.Base; +using NUnit.Framework; + +namespace Ethereum.Blockchain.Pyspec.Test; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class Eip7954BlockChainTests : BlockchainTestBase +{ + private const string Eip7954Wildcard = "eip7954_increase_max_contract_size"; + + [TestCaseSource(nameof(LoadTests))] + public async Task Test(BlockchainTest test) => await RunTest(test); + + private static IEnumerable LoadTests() + { + TestsSourceLoader loader = new(new LoadPyspecTestsStrategy + { + ArchiveVersion = Amsterdam.Constants.BalArchiveVersion, + ArchiveName = Amsterdam.Constants.BalArchiveName + }, "fixtures/blockchain_tests", Eip7954Wildcard); + + return loader.LoadTests().OfType(); + } +} diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7954StateTests.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7954StateTests.cs new file mode 100644 index 00000000000..cf6c6a43c91 --- /dev/null +++ b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip7954StateTests.cs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using Ethereum.Test.Base; +using FluentAssertions; +using NUnit.Framework; + +namespace Ethereum.Blockchain.Pyspec.Test; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class Eip7954StateTests : GeneralStateTestBase +{ + private const string Eip7954Wildcard = "eip7954_increase_max_contract_size"; + + [TestCaseSource(nameof(LoadTests))] + public void Test(GeneralStateTest test) => RunTest(test).Pass.Should().BeTrue(); + + private static IEnumerable LoadTests() + { + TestsSourceLoader loader = new(new LoadPyspecTestsStrategy + { + ArchiveVersion = Amsterdam.Constants.BalArchiveVersion, + ArchiveName = Amsterdam.Constants.BalArchiveName + }, "fixtures/state_tests", Eip7954Wildcard); + + return loader.LoadTests(); + } +} diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip8024BlockChainTests.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip8024BlockChainTests.cs new file mode 100644 index 00000000000..0df0170c618 --- /dev/null +++ b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip8024BlockChainTests.cs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ethereum.Test.Base; +using NUnit.Framework; + +namespace Ethereum.Blockchain.Pyspec.Test; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class Eip8024BlockChainTests : BlockchainTestBase +{ + private const string Eip8024Wildcard = "eip8024_dupn_swapn_exchange"; + + [TestCaseSource(nameof(LoadTests))] + public async Task Test(BlockchainTest test) => await RunTest(test); + + private static IEnumerable LoadTests() + { + TestsSourceLoader loader = new(new LoadPyspecTestsStrategy + { + ArchiveVersion = Amsterdam.Constants.BalArchiveVersion, + ArchiveName = Amsterdam.Constants.BalArchiveName + }, "fixtures/blockchain_tests", Eip8024Wildcard); + + return loader.LoadTests().OfType(); + } +} diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip8024StateTests.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip8024StateTests.cs new file mode 100644 index 00000000000..7f141098b03 --- /dev/null +++ b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip8024StateTests.cs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using Ethereum.Test.Base; +using FluentAssertions; +using NUnit.Framework; + +namespace Ethereum.Blockchain.Pyspec.Test; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class Eip8024StateTests : GeneralStateTestBase +{ + private const string Eip8024Wildcard = "eip8024_dupn_swapn_exchange"; + + [TestCaseSource(nameof(LoadTests))] + public void Test(GeneralStateTest test) => RunTest(test).Pass.Should().BeTrue(); + + private static IEnumerable LoadTests() + { + TestsSourceLoader loader = new(new LoadPyspecTestsStrategy + { + ArchiveVersion = Amsterdam.Constants.BalArchiveVersion, + ArchiveName = Amsterdam.Constants.BalArchiveName + }, "fixtures/state_tests", Eip8024Wildcard); + + return loader.LoadTests(); + } +} diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip8037BlockChainTests.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip8037BlockChainTests.cs new file mode 100644 index 00000000000..272879e83c8 --- /dev/null +++ b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip8037BlockChainTests.cs @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Ethereum.Test.Base; +using NUnit.Framework; + +namespace Ethereum.Blockchain.Pyspec.Test; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class Eip8037BlockChainTests : BlockchainTestBase +{ + private const string Eip8037Wildcard = "eip8037_state_creation_gas_cost_increase"; + + [TestCaseSource(nameof(LoadTests))] + public async Task Test(BlockchainTest test) => await RunTest(test); + + private static IEnumerable LoadTests() + { + TestsSourceLoader loader = new(new LoadPyspecTestsStrategy + { + ArchiveVersion = Amsterdam.Constants.BalArchiveVersion, + ArchiveName = Amsterdam.Constants.BalArchiveName + }, "fixtures/blockchain_tests", Eip8037Wildcard); + + return loader.LoadTests().OfType(); + } +} diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip8037StateTests.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip8037StateTests.cs new file mode 100644 index 00000000000..5360b9ca4cd --- /dev/null +++ b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Eip8037StateTests.cs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using Ethereum.Test.Base; +using FluentAssertions; +using NUnit.Framework; + +namespace Ethereum.Blockchain.Pyspec.Test; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class Eip8037StateTests : GeneralStateTestBase +{ + private const string Eip8037Wildcard = "eip8037_state_creation_gas_cost_increase"; + + [TestCaseSource(nameof(LoadTests))] + public void Test(GeneralStateTest test) => RunTest(test).Pass.Should().BeTrue(); + + private static IEnumerable LoadTests() + { + TestsSourceLoader loader = new(new LoadPyspecTestsStrategy + { + ArchiveVersion = Amsterdam.Constants.BalArchiveVersion, + ArchiveName = Amsterdam.Constants.BalArchiveName + }, "fixtures/state_tests", Eip8037Wildcard); + + return loader.LoadTests(); + } +} diff --git a/src/Nethermind/Ethereum.Test.Base/BlockchainTestBase.cs b/src/Nethermind/Ethereum.Test.Base/BlockchainTestBase.cs index 86e35cc68da..c322b1f1e25 100644 --- a/src/Nethermind/Ethereum.Test.Base/BlockchainTestBase.cs +++ b/src/Nethermind/Ethereum.Test.Base/BlockchainTestBase.cs @@ -86,8 +86,12 @@ protected async Task RunTest(BlockchainTest test, Stopwatch? bool isEngineTest = test.Blocks is null && test.EngineNewPayloads is not null; + // Post-merge pyspec blockchain_test_from_state_test fixtures expect genesis to be processed + // under the target fork rules when the fork requires it (e.g. EIP-7928 sets BlockAccessListHash). + bool genesisUsesTargetFork = test.Network.IsEip7928Enabled; + List<(ForkActivation Activation, IReleaseSpec Spec)> transitions = - isEngineTest ? + isEngineTest || genesisUsesTargetFork ? [((ForkActivation)0, test.Network)] : [((ForkActivation)0, test.GenesisSpec), ((ForkActivation)1, test.Network)]; // genesis block is always initialized with Frontier @@ -98,7 +102,6 @@ protected async Task RunTest(BlockchainTest test, Stopwatch? ISpecProvider specProvider = new CustomSpecProvider(test.ChainId, test.ChainId, transitions.ToArray()); - Assert.That(isEngineTest || test.ChainId == GnosisSpecProvider.Instance.ChainId || specProvider.GenesisSpec == Frontier.Instance, "Expected genesis spec to be Frontier for blockchain tests"); if (test.Network is Cancun || test.NetworkAfterTransition is Cancun) { diff --git a/src/Nethermind/Ethereum.Test.Base/GeneralStateTest.cs b/src/Nethermind/Ethereum.Test.Base/GeneralStateTest.cs index da8142a513f..228a14bb656 100644 --- a/src/Nethermind/Ethereum.Test.Base/GeneralStateTest.cs +++ b/src/Nethermind/Ethereum.Test.Base/GeneralStateTest.cs @@ -37,6 +37,8 @@ public class GeneralStateTest : EthereumTest public Hash256? CurrentBeaconRoot { get; set; } public Hash256? CurrentWithdrawalsRoot { get; set; } public ulong? CurrentExcessBlobGas { get; set; } + public ulong? CurrentSlotNumber { get; set; } + public Hash256? RequestsHash { get; set; } diff --git a/src/Nethermind/Ethereum.Test.Base/GeneralStateTestEnvJson.cs b/src/Nethermind/Ethereum.Test.Base/GeneralStateTestEnvJson.cs index 384b5465b19..4b7de2e7596 100644 --- a/src/Nethermind/Ethereum.Test.Base/GeneralStateTestEnvJson.cs +++ b/src/Nethermind/Ethereum.Test.Base/GeneralStateTestEnvJson.cs @@ -20,5 +20,6 @@ public class GeneralStateTestEnvJson public Hash256? CurrentBeaconRoot { get; set; } public Hash256? CurrentWithdrawalsRoot { get; set; } public ulong? CurrentExcessBlobGas { get; set; } + public ulong? SlotNumber { get; set; } } } diff --git a/src/Nethermind/Ethereum.Test.Base/GeneralTestBase.cs b/src/Nethermind/Ethereum.Test.Base/GeneralTestBase.cs index 0caeea0862e..5d6bcc4e321 100644 --- a/src/Nethermind/Ethereum.Test.Base/GeneralTestBase.cs +++ b/src/Nethermind/Ethereum.Test.Base/GeneralTestBase.cs @@ -132,8 +132,10 @@ protected EthereumTestResult RunTest(GeneralStateTest test, ITxTracer txTracer) WithdrawalsRoot = test.CurrentWithdrawalsRoot ?? (spec.WithdrawalsEnabled ? PatriciaTree.EmptyTreeHash : null), ParentBeaconBlockRoot = test.CurrentBeaconRoot, ExcessBlobGas = test.CurrentExcessBlobGas ?? (test.Fork is Cancun ? 0ul : null), + SlotNumber = test.CurrentSlotNumber, BlobGasUsed = BlobGasCalculator.CalculateBlobGas(test.Transaction), RequestsHash = test.RequestsHash ?? (spec.RequestsEnabled ? ExecutionRequestExtensions.EmptyRequestsHash : null), + BlockAccessListHash = spec.IsEip7928Enabled ? Keccak.OfAnEmptySequenceRlp : null, TxRoot = TxTrie.CalculateRoot(transactions), ReceiptsRoot = test.PostReceiptsRoot, }; diff --git a/src/Nethermind/Ethereum.Test.Base/JsonToEthereumTest.cs b/src/Nethermind/Ethereum.Test.Base/JsonToEthereumTest.cs index fe301f028e7..c36954080ca 100644 --- a/src/Nethermind/Ethereum.Test.Base/JsonToEthereumTest.cs +++ b/src/Nethermind/Ethereum.Test.Base/JsonToEthereumTest.cs @@ -280,6 +280,7 @@ public static IEnumerable Convert(string name, string category CurrentBeaconRoot = testJson.Env.CurrentBeaconRoot, CurrentWithdrawalsRoot = testJson.Env.CurrentWithdrawalsRoot, CurrentExcessBlobGas = testJson.Env.CurrentExcessBlobGas, + CurrentSlotNumber = testJson.Env.SlotNumber, PostReceiptsRoot = stateJson.Logs, PostHash = stateJson.Hash, Pre = testJson.Pre.ToDictionary(p => p.Key, p => p.Value), diff --git a/src/Nethermind/Nethermind.Blockchain.Test/BlockchainProcessorTests.cs b/src/Nethermind/Nethermind.Blockchain.Test/BlockchainProcessorTests.cs index 2c77b030bbb..e04182f9699 100644 --- a/src/Nethermind/Nethermind.Blockchain.Test/BlockchainProcessorTests.cs +++ b/src/Nethermind/Nethermind.Blockchain.Test/BlockchainProcessorTests.cs @@ -463,10 +463,13 @@ public ProcessingTestContext IsKeptOnBranch() public ProcessingTestContext IsDeletedAsInvalid() { _logger.Info($"Waiting for {block.ToString(Block.Format.Short)} to be deleted"); + // Drain any stale signal (no NewHeadBlock fires for invalid blocks, so this always times out). processingTestContext._resetEvent.WaitOne(IgnoreWait); Assert.That(processingTestContext._blockTree.Head!.Hash, Is.EqualTo(processingTestContext._headBefore), "head"); + // Poll until the block is actually deleted — the 200 ms drain above is not enough on slow CI. + Assert.That(() => processingTestContext._blockTree.FindBlock(block.Hash, BlockTreeLookupOptions.None), + Is.Null.After(ProcessingWait, 50), $"block {block.ToString(Block.Format.Short)} should be deleted as invalid"); _logger.Info($"Finished waiting for {block.ToString(Block.Format.Short)} to be deleted"); - Assert.That(processingTestContext._blockTree.FindBlock(block.Hash, BlockTreeLookupOptions.None), Is.Null); return processingTestContext; } } diff --git a/src/Nethermind/Nethermind.Blockchain/Tracing/BlockReceiptsTracer.cs b/src/Nethermind/Nethermind.Blockchain/Tracing/BlockReceiptsTracer.cs index 4d8a535d41e..308b23b20a0 100644 --- a/src/Nethermind/Nethermind.Blockchain/Tracing/BlockReceiptsTracer.cs +++ b/src/Nethermind/Nethermind.Blockchain/Tracing/BlockReceiptsTracer.cs @@ -81,9 +81,14 @@ protected TxReceipt BuildFailedReceipt(Address recipient, in GasConsumed gasSpen /// The cumulative post-refund gas for receipts protected long UpdateCumulativeGasTracking(in GasConsumed gasConsumed) { - // Track cumulative block gas for restore (pre-refund) - long cumulativeBlockGas = (_cumulativeBlockGasPerTx.Count > 0 ? _cumulativeBlockGasPerTx[^1] : 0) + gasConsumed.EffectiveBlockGas; - _cumulativeBlockGasPerTx.Add(cumulativeBlockGas); + // Track cumulative block gas for restore (regular + EIP-8037 state) + (long prevRegular, long prevState) = _cumulativeBlockGasPerTx.Count > 0 ? _cumulativeBlockGasPerTx[^1] : (0, 0); + long cumulativeBlockGas = prevRegular + gasConsumed.EffectiveBlockGas; + long cumulativeBlockStateGas = prevState + gasConsumed.BlockStateGas; + _cumulativeBlockGasPerTx.Add((cumulativeBlockGas, cumulativeBlockStateGas)); + + // EIP-8037: block gasUsed = max(sum_regular, sum_state). Override header accumulation. + Block.Header.GasUsed = Math.Max(cumulativeBlockGas, cumulativeBlockStateGas); // Track cumulative receipt gas (post-refund) _cumulativeReceiptGas += gasConsumed.SpentGas; @@ -222,7 +227,7 @@ public void ReportFees(UInt256 fees, UInt256 burntFees) private ITxTracer _currentTxTracer = NullTxTracer.Instance; protected int _currentIndex { get; private set; } private readonly List _txReceipts = new(); - private readonly List _cumulativeBlockGasPerTx = new(); // Track pre-refund block gas for restore + private readonly List<(long Regular, long State)> _cumulativeBlockGasPerTx = new(); // Track pre-refund block gas for restore (regular + EIP-8037 state) private long _cumulativeReceiptGas; // Track cumulative post-refund gas for receipts protected Transaction? CurrentTx; public ReadOnlySpan TxReceipts => CollectionsMarshal.AsSpan(_txReceipts); @@ -245,8 +250,9 @@ public void Restore(int snapshot) Debug.Assert(_txReceipts.Count == _cumulativeBlockGasPerTx.Count, "Receipt and gas tracking lists must remain synchronized after restore"); - // Restore block gas from tracking (pre-refund) - Block.Header.GasUsed = _cumulativeBlockGasPerTx.Count > 0 ? _cumulativeBlockGasPerTx[^1] : 0; + // Restore block gas from tracking: max(cumulative_regular, cumulative_state) for EIP-8037 + (long cumulativeRegular, long cumulativeState) = _cumulativeBlockGasPerTx.Count > 0 ? _cumulativeBlockGasPerTx[^1] : (0, 0); + Block.Header.GasUsed = Math.Max(cumulativeRegular, cumulativeState); // Restore receipt gas from remaining receipts (post-refund) _cumulativeReceiptGas = _txReceipts.Count > 0 ? _txReceipts[^1].GasUsedTotal : 0; diff --git a/src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.cs b/src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.cs index ddd91e5aa1e..93d2c31b021 100644 --- a/src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.cs +++ b/src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.cs @@ -103,6 +103,7 @@ protected virtual TxReceipt[] ProcessBlock( IReleaseSpec spec, CancellationToken token) { + BlockBody body = block.Body; BlockHeader header = block.Header; ReceiptsTracer.SetOtherTracer(blockTracer); @@ -114,7 +115,8 @@ protected virtual TxReceipt[] ProcessBlock( blockHashStore.ApplyBlockhashStateChanges(header, spec); _stateProvider.Commit(spec, commitRoots: false); - TxReceipt[] receipts = blockTransactionsExecutor.ProcessTransactions(block, options, ReceiptsTracer, token); + TxReceipt[] receipts; + receipts = blockTransactionsExecutor.ProcessTransactions(block, options, ReceiptsTracer, token); // Signal that transactions are done — subscribers can cancel background work (e.g. prewarmer) // to free the thread pool for blooms, receipts root, state root parallel work below diff --git a/src/Nethermind/Nethermind.Consensus/Stateless/WitnessGeneratingWorldState.cs b/src/Nethermind/Nethermind.Consensus/Stateless/WitnessGeneratingWorldState.cs index f6c842e0712..e367b967efc 100644 --- a/src/Nethermind/Nethermind.Consensus/Stateless/WitnessGeneratingWorldState.cs +++ b/src/Nethermind/Nethermind.Consensus/Stateless/WitnessGeneratingWorldState.cs @@ -230,34 +230,18 @@ public void AddToBalance(Address address, in UInt256 balanceChange, IReleaseSpec inner.AddToBalance(address, in balanceChange, spec, out oldBalance); } - public bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balanceChange, IReleaseSpec spec) - { - RecordEmptySlots(address); - return inner.AddToBalanceAndCreateIfNotExists(address, in balanceChange, spec); - } public bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) { RecordEmptySlots(address); return inner.AddToBalanceAndCreateIfNotExists(address, in balanceChange, spec, out oldBalance); } - public void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec) - { - RecordEmptySlots(address); - inner.SubtractFromBalance(address, in balanceChange, spec); - } - public void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) { RecordEmptySlots(address); inner.SubtractFromBalance(address, in balanceChange, spec, out oldBalance); } - public void IncrementNonce(Address address, UInt256 delta) - { - RecordEmptySlots(address); - inner.IncrementNonce(address, delta); - } public void IncrementNonce(Address address, UInt256 delta, out UInt256 oldNonce) { RecordEmptySlots(address); diff --git a/src/Nethermind/Nethermind.Consensus/Validators/HeaderValidator.cs b/src/Nethermind/Nethermind.Consensus/Validators/HeaderValidator.cs index f5d94cb3581..915a4590843 100644 --- a/src/Nethermind/Nethermind.Consensus/Validators/HeaderValidator.cs +++ b/src/Nethermind/Nethermind.Consensus/Validators/HeaderValidator.cs @@ -383,12 +383,10 @@ protected virtual bool ValidateBlobGasFields(BlockHeader header, BlockHeader par return true; } - protected virtual ulong? CalculateExcessBlobGas(BlockHeader parent, IReleaseSpec spec) - { - return BlobGasCalculator.CalculateExcessBlobGas(parent, spec); - } + protected virtual ulong? CalculateExcessBlobGas(BlockHeader parent, IReleaseSpec spec) => + BlobGasCalculator.CalculateExcessBlobGas(parent, spec); - private bool ValidateBlockAccessListHash(BlockHeader header, IReleaseSpec spec, ref string? error) + protected virtual bool ValidateBlockAccessListHash(BlockHeader header, IReleaseSpec spec, ref string? error) { if (spec.IsEip7928Enabled) { diff --git a/src/Nethermind/Nethermind.Core.Test/Modules/PseudoNethermindModule.cs b/src/Nethermind/Nethermind.Core.Test/Modules/PseudoNethermindModule.cs index 3381c6616c4..4294f43a58e 100644 --- a/src/Nethermind/Nethermind.Core.Test/Modules/PseudoNethermindModule.cs +++ b/src/Nethermind/Nethermind.Core.Test/Modules/PseudoNethermindModule.cs @@ -43,6 +43,7 @@ public class PseudoNethermindModule(ChainSpec spec, IConfigProvider configProvid protected override void Load(ContainerBuilder builder) { IInitConfig initConfig = configProvider.GetConfig(); + initConfig.AutoDump = DumpOptions.None; if (TestUseFlat) { ISyncConfig syncConfig = configProvider.GetConfig(); diff --git a/src/Nethermind/Nethermind.Core/CodeSizeConstants.cs b/src/Nethermind/Nethermind.Core/CodeSizeConstants.cs index d2bb15289e7..5e31e7c177b 100644 --- a/src/Nethermind/Nethermind.Core/CodeSizeConstants.cs +++ b/src/Nethermind/Nethermind.Core/CodeSizeConstants.cs @@ -6,5 +6,6 @@ namespace Nethermind.Core; public static class CodeSizeConstants { public const int MaxCodeSizeEip170 = 24_576; // 24KiB + public const int MaxCodeSizeEip7954 = 32_768; // 32KiB public const int MaxCodeSizeEip7907 = 262_144; // 256KiB } diff --git a/src/Nethermind/Nethermind.Core/Eip7825Constants.cs b/src/Nethermind/Nethermind.Core/Eip7825Constants.cs index 4270e928897..beabe71dc46 100644 --- a/src/Nethermind/Nethermind.Core/Eip7825Constants.cs +++ b/src/Nethermind/Nethermind.Core/Eip7825Constants.cs @@ -9,5 +9,7 @@ public static class Eip7825Constants { public static readonly long DefaultTxGasLimitCap = 16_777_216; public static long GetTxGasLimitCap(this IReleaseSpec spec) - => spec.IsEip7825Enabled ? DefaultTxGasLimitCap : long.MaxValue; + => spec.IsEip7825Enabled && !spec.IsEip8037Enabled + ? DefaultTxGasLimitCap + : long.MaxValue; } diff --git a/src/Nethermind/Nethermind.Core/Extensions/Bytes.cs b/src/Nethermind/Nethermind.Core/Extensions/Bytes.cs index 8c495974386..4c3aa824fc6 100644 --- a/src/Nethermind/Nethermind.Core/Extensions/Bytes.cs +++ b/src/Nethermind/Nethermind.Core/Extensions/Bytes.cs @@ -333,6 +333,7 @@ public static void ReverseInPlace(byte[] bytes) } } + public static BigInteger ToUnsignedBigInteger(this byte[] bytes) { return ToUnsignedBigInteger(bytes.AsSpan()); diff --git a/src/Nethermind/Nethermind.Core/GasCostOf.cs b/src/Nethermind/Nethermind.Core/GasCostOf.cs index b8001f83c66..24311b8d7ef 100644 --- a/src/Nethermind/Nethermind.Core/GasCostOf.cs +++ b/src/Nethermind/Nethermind.Core/GasCostOf.cs @@ -69,6 +69,20 @@ public static class GasCostOf public const long PerAuthBaseCost = 12500; // eip-7702 public const long TotalCostFloorPerTokenEip7623 = 10; // eip-7632 + // EIP-8037: Two-dimensional gas metering constants. + // Devnet-3 keeps CPSB hardcoded and replaces it with dynamic CPSB in devnet-4. + public const long CostPerStateByte = 1174; + public const long SSetRegular = 2_900; + public const long SSetState = 32 * CostPerStateByte; + public const long CreateRegular = 9_000; + public const long CreateState = 112 * CostPerStateByte; + public const long NewAccountState = 112 * CostPerStateByte; + public const long CodeDepositRegularPerWord = 6; + public const long CodeDepositState = CostPerStateByte; + public const long PerAuthBaseRegular = 7_500; + public const long PerAuthBaseState = 23 * CostPerStateByte; + public const long PerEmptyAccountState = 112 * CostPerStateByte; + public const long TxDataNonZeroMultiplier = TxDataNonZero / TxDataZero; public const long TxDataNonZeroMultiplierEip2028 = TxDataNonZeroEip2028 / TxDataZero; diff --git a/src/Nethermind/Nethermind.Core/RefundOf.cs b/src/Nethermind/Nethermind.Core/RefundOf.cs index 12f9e23c49e..4727472c4b7 100644 --- a/src/Nethermind/Nethermind.Core/RefundOf.cs +++ b/src/Nethermind/Nethermind.Core/RefundOf.cs @@ -11,6 +11,7 @@ public static class RefundOf public const long SResetReversedEip2200 = GasCostOf.SReset - GasCostOf.SStoreNetMeteredEip2200; public const long SSetReversedHotCold = GasCostOf.SSet - GasCostOf.WarmStateRead; public const long SResetReversedHotCold = GasCostOf.SReset - GasCostOf.ColdSLoad - GasCostOf.WarmStateRead; + public const long SSetReversedEip8037 = GasCostOf.SSetState + GasCostOf.SSetRegular - GasCostOf.WarmStateRead; public const long SClearAfterEip3529 = GasCostOf.SReset - GasCostOf.ColdSLoad + GasCostOf.AccessStorageListEntry; public const long SClearBeforeEip3529 = 15000; public const long DestroyBeforeEip3529 = 24000; diff --git a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs index 223d85180f8..918ec77d0bd 100644 --- a/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs @@ -459,7 +459,13 @@ public interface IReleaseSpec : IEip1559Spec, IReceiptSpec bool BlockLevelAccessListsEnabled => IsEip7928Enabled; /// - /// EIP-7708: ETH transfers emit a log + /// EIP-8037: Cost Per State Byte / State Size Limit. + /// Two-dimensional gas metering for state growth control. + /// + public bool IsEip8037Enabled { get; } + + /// + /// EIP-7708: ETH transfers and burns emit a log /// public bool IsEip7708Enabled { get; } @@ -467,6 +473,13 @@ public interface IReleaseSpec : IEip1559Spec, IReceiptSpec /// EIP-7843: SLOTNUM opcode /// public bool IsEip7843Enabled { get; } + + /// + /// EIP-7954: Increase Maximum Contract Size + /// + public bool IsEip7954Enabled { get; } + + /// /// Precomputed gas cost and refund constants derived from this spec. /// Values are cached per spec instance (singletons per fork) to avoid /// repeated interface dispatch on the EVM opcode hot path. diff --git a/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs b/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs index d122f2d21a2..7079f7a763c 100644 --- a/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs +++ b/src/Nethermind/Nethermind.Core/Specs/ReleaseSpecDecorator.cs @@ -115,9 +115,11 @@ public class ReleaseSpecDecorator(IReleaseSpec spec) : IReleaseSpec public virtual bool IsEip7907Enabled => spec.IsEip7907Enabled; public virtual bool IsRip7728Enabled => spec.IsRip7728Enabled; public virtual bool IsEip7928Enabled => spec.IsEip7928Enabled; + public virtual bool IsEip8037Enabled => spec.IsEip8037Enabled; public virtual bool IsEip7708Enabled => spec.IsEip7708Enabled; public virtual bool IsEip7778Enabled => spec.IsEip7778Enabled; public virtual bool IsEip7843Enabled => spec.IsEip7843Enabled; + public virtual bool IsEip7954Enabled => spec.IsEip7954Enabled; 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 8cd60c9a9ff..ae3b04ce112 100644 --- a/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs +++ b/src/Nethermind/Nethermind.Core/Specs/SpecGasCosts.cs @@ -114,6 +114,16 @@ public SpecGasCosts(IReleaseSpec spec) _hashCode = HashCode.Combine(hashCode1, hashCode2); } + public long RefundFromReversal(bool originalIsZero) + where TEip8037 : struct, IFlag + { + return originalIsZero + ? TEip8037.IsActive + ? RefundOf.SSetReversedEip8037 + : SetReversalRefund + : ClearReversalRefund; + } + public bool Equals(SpecGasCosts? other) { if (other is null) return false; diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip2200Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip2200Tests.cs index df8b99a330d..6c723cc4010 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Eip2200Tests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Eip2200Tests.cs @@ -5,8 +5,10 @@ using Nethermind.Core; using Nethermind.Core.Extensions; using Nethermind.Core.Specs; +using Nethermind.Evm.GasPolicy; using Nethermind.Evm.State; using Nethermind.Specs; +using Nethermind.Specs.Forks; using NUnit.Framework; namespace Nethermind.Evm.Test @@ -37,64 +39,225 @@ public class Eip2200Tests : VirtualMachineTestsBase [TestCase("0x600060005560016000556000600055", 10818, 19200, 1)] public void Test(string codeHex, long gasUsed, long refund, byte originalValue) { - TestState.CreateAccount(Recipient, 0); - TestState.Set(new StorageCell(Recipient, 0), new[] { originalValue }); - TestState.Commit(MainnetSpecProvider.Instance.GenesisSpec); + SetupStorage(originalValue); TestAllTracerWithOutput receipt = Execute(Bytes.FromHexString(codeHex)); AssertGas(receipt, gasUsed + GasCostOf.Transaction - Math.Min((gasUsed + GasCostOf.Transaction) / 2, refund)); } - [TestCase("0x60006000556000600055", 1612, 0, 0, true)] - [TestCase("0x60016000556000600055", 20812, 19200, 0, true)] - [TestCase("0x60016000556002600055", 20812, 0, 0, true)] - [TestCase("0x60016000556001600055", 20812, 0, 0, true)] - [TestCase("0x60006000556000600055", 5812, 15000, 1, true)] - [TestCase("0x60006000556001600055", 5812, 4200, 1, true)] - [TestCase("0x60026000556000600055", 5812, 15000, 1, true)] - [TestCase("0x60026000556003600055", 5812, 0, 1, true)] - [TestCase("0x60026000556001600055", 5812, 4200, 1, true)] - [TestCase("0x60026000556002600055", 5812, 0, 1, true)] - [TestCase("0x60016000556001600055", 1612, 0, 1, true)] - [TestCase("0x60006000556002600055", 5812, 0, 1, true)] - - [TestCase("0x60016000556000600055", 5812, 15000, 1, false)] - [TestCase("0x60016000556002600055", 5812, 0, 1, false)] - [TestCase("0x600160005560006000556001600055", 40818, 19200, 0, false)] - [TestCase("0x600060005560016000556000600055", 10818, 19200, 1, false)] - [TestCase("0x60006000556001600055", 20812, 0, 0, false)] - public void Test_when_gas_at_stipend(string codeHex, long gasUsed, long refund, byte originalValue, bool outOfGasExpected) + [TestCase("0x60006000556000600055", 1612, 0, 2300, true)] + [TestCase("0x60016000556000600055", 20812, 0, 2300, true)] + [TestCase("0x60016000556002600055", 20812, 0, 2300, true)] + [TestCase("0x60016000556001600055", 20812, 0, 2300, true)] + [TestCase("0x60006000556000600055", 5812, 1, 2300, true)] + [TestCase("0x60006000556001600055", 5812, 1, 2300, true)] + [TestCase("0x60026000556000600055", 5812, 1, 2300, true)] + [TestCase("0x60026000556003600055", 5812, 1, 2300, true)] + [TestCase("0x60026000556001600055", 5812, 1, 2300, true)] + [TestCase("0x60026000556002600055", 5812, 1, 2300, true)] + [TestCase("0x60016000556001600055", 1612, 1, 2300, true)] + [TestCase("0x60006000556002600055", 5812, 1, 2300, true)] + [TestCase("0x60016000556000600055", 5812, 1, 2300, false)] + [TestCase("0x60016000556002600055", 5812, 1, 2300, false)] + [TestCase("0x600160005560006000556001600055", 40818, 0, 2300, false)] + [TestCase("0x600060005560016000556000600055", 10818, 1, 2300, false)] + [TestCase("0x60006000556001600055", 20812, 0, 2300, false)] + [TestCase("0x60006000556000600055", 1612, 0, 2301, false)] + [TestCase("0x60016000556001600055", 1612, 1, 2301, false)] + [TestCase("0x60006000556000600055", 1612, 0, 2299, true)] + [TestCase("0x60016000556001600055", 1612, 1, 2299, true)] + public void Test_at_stipend_boundary(string codeHex, long gasUsed, byte originalValue, int stipend, bool outOfGasExpected) { - TestState.CreateAccount(Recipient, 0); - TestState.Set(new StorageCell(Recipient, 0), new[] { originalValue }); - TestState.Commit(MainnetSpecProvider.Instance.GenesisSpec); + SetupStorage(originalValue); - TestAllTracerWithOutput receipt = Execute(BlockNumber, 21000 + gasUsed + (2300 - 800), Bytes.FromHexString(codeHex)); + TestAllTracerWithOutput receipt = Execute(BlockNumber, 21000 + gasUsed + (stipend - 800), Bytes.FromHexString(codeHex)); Assert.That(receipt.StatusCode, Is.EqualTo(outOfGasExpected ? 0 : 1)); } - [TestCase("0x60006000556000600055", 1612, 0, 0)] - [TestCase("0x60016000556001600055", 1612, 0, 1)] - public void Test_when_gas_just_above_stipend(string codeHex, long gasUsed, long refund, byte originalValue) + private void SetupStorage(byte originalValue) { TestState.CreateAccount(Recipient, 0); TestState.Set(new StorageCell(Recipient, 0), new[] { originalValue }); TestState.Commit(MainnetSpecProvider.Instance.GenesisSpec); + } - TestAllTracerWithOutput receipt = Execute(BlockNumber, 21000 + gasUsed + (2301 - 800), Bytes.FromHexString(codeHex)); - Assert.That(receipt.StatusCode, Is.EqualTo(1)); + [Test] + public void Eip8037_constants_are_calculated_correctly() + { + Assert.That(GasCostOf.CostPerStateByte, Is.EqualTo(1174)); + Assert.That(GasCostOf.SSetState, Is.EqualTo(37568)); + Assert.That(GasCostOf.CreateState, Is.EqualTo(131488)); + Assert.That(GasCostOf.NewAccountState, Is.EqualTo(131488)); + Assert.That(GasCostOf.PerAuthBaseState, Is.EqualTo(27002)); } - [TestCase("0x60006000556000600055", 1612, 0, 0)] - [TestCase("0x60016000556001600055", 1612, 0, 1)] - public void Test_when_gas_just_below_stipend(string codeHex, long gasUsed, long refund, byte originalValue) + [TestCase(1, 6, 1174)] + [TestCase(32, 6, 37568)] + [TestCase(33, 12, 38742)] + public void Eip8037_code_deposit_costs_are_split(int codeLength, long expectedRegular, long expectedState) { - TestState.CreateAccount(Recipient, 0); - TestState.Set(new StorageCell(Recipient, 0), new[] { originalValue }); - TestState.Commit(MainnetSpecProvider.Instance.GenesisSpec); + IReleaseSpec spec = Amsterdam.Instance; + + bool valid = CodeDepositHandler.CalculateCost(spec, codeLength, out long regularCost, out long stateCost); + + Assert.That(valid, Is.True); + Assert.That(regularCost, Is.EqualTo(expectedRegular)); + Assert.That(stateCost, Is.EqualTo(expectedState)); + } + + [Test] + public void Eip8037_state_gas_consumption_spills_to_regular_gas() + { + EthereumGasPolicy gas = new() + { + Value = 100, + StateReservoir = 50, + StateGasUsed = 0, + }; + + bool consumed = EthereumGasPolicy.ConsumeStateGas(ref gas, 70); + + Assert.That(consumed, Is.True); + Assert.That(gas.StateReservoir, Is.EqualTo(0)); + Assert.That(gas.Value, Is.EqualTo(80)); + Assert.That(gas.StateGasUsed, Is.EqualTo(70)); + } + + [Test] + public void Eip8037_child_frame_gets_full_state_reservoir() + { + EthereumGasPolicy parent = new() + { + Value = 1_000, + StateReservoir = 333, + StateGasUsed = 50, + }; + + EthereumGasPolicy child = EthereumGasPolicy.CreateChildFrameGas(ref parent, 444); + + Assert.That(parent.Value, Is.EqualTo(1_000)); + Assert.That(parent.StateReservoir, Is.EqualTo(0)); + Assert.That(parent.StateGasUsed, Is.EqualTo(50)); + Assert.That(child.Value, Is.EqualTo(444)); + Assert.That(child.StateReservoir, Is.EqualTo(333)); + Assert.That(child.StateGasUsed, Is.EqualTo(0)); + } + + [Test] + public void Eip8037_child_frame_refund_restores_remaining_state_reservoir() + { + EthereumGasPolicy parent = new() + { + Value = 1_000, + StateReservoir = 333, + StateGasUsed = 50, + }; + + EthereumGasPolicy child = EthereumGasPolicy.CreateChildFrameGas(ref parent, 444); + bool stateConsumed = EthereumGasPolicy.ConsumeStateGas(ref child, 100); + bool regularConsumed = EthereumGasPolicy.UpdateGas(ref child, 150); + + Assert.That(stateConsumed, Is.True); + Assert.That(regularConsumed, Is.True); + + EthereumGasPolicy.Refund(ref parent, in child); + + Assert.That(parent.Value, Is.EqualTo(1_294)); + Assert.That(parent.StateReservoir, Is.EqualTo(233)); + Assert.That(parent.StateGasUsed, Is.EqualTo(150)); + } + + [Test] + public void Eip8037_state_refund_is_clamped_to_intrinsic_state_floor() + { + EthereumGasPolicy gas = new() + { + Value = 100, + StateReservoir = 0, + StateGasUsed = 120, + }; + + EthereumGasPolicy.RefundStateGas(ref gas, 200, stateGasFloor: 40); + + Assert.That(gas.StateReservoir, Is.EqualTo(200)); + Assert.That(gas.StateGasUsed, Is.EqualTo(0)); + } + + [Test] + public void Eip8037_exceptional_halt_preserves_state_gas() + { + EthereumGasPolicy parent = new() + { + Value = 1_000, + StateReservoir = 500, + StateGasUsed = 10, + }; + + EthereumGasPolicy child = EthereumGasPolicy.CreateChildFrameGas(ref parent, 600); + Assert.That(parent.StateReservoir, Is.EqualTo(0)); + + // Child consumes some state gas + EthereumGasPolicy.ConsumeStateGas(ref child, 200); + Assert.That(child.StateReservoir, Is.EqualTo(300)); + Assert.That(child.StateGasUsed, Is.EqualTo(200)); + + // Exceptional halt zeroes Value but preserves StateReservoir + EthereumGasPolicy.SetOutOfGas(ref child); + Assert.That(child.Value, Is.EqualTo(0)); + Assert.That(child.StateReservoir, Is.EqualTo(300)); + + // Restore returns full original reservoir to parent + EthereumGasPolicy.RestoreChildStateGas(ref parent, in child, 500); + Assert.That(parent.StateReservoir, Is.EqualTo(500)); + Assert.That(parent.StateGasUsed, Is.EqualTo(10)); + } + + [Test] + public void Eip8037_revert_restores_state_gas_to_parent_reservoir() + { + EthereumGasPolicy parent = new() + { + Value = 1_000, + StateReservoir = 400, + StateGasUsed = 20, + }; + + // Simulate parent allocating 600 regular gas to child (done by VM before CreateChildFrameGas) + EthereumGasPolicy.Consume(ref parent, 600); + EthereumGasPolicy child = EthereumGasPolicy.CreateChildFrameGas(ref parent, 600); + + // Child uses some regular gas and state gas + EthereumGasPolicy.UpdateGas(ref child, 100); + EthereumGasPolicy.ConsumeStateGas(ref child, 150); + + // Simulate revert path: return remaining regular gas, restore state gas + EthereumGasPolicy.UpdateGasUp(ref parent, EthereumGasPolicy.GetRemainingGas(in child)); + EthereumGasPolicy.RestoreChildStateGas(ref parent, in child, 400); + + // Parent gets remaining regular gas back + Assert.That(parent.Value, Is.EqualTo(900)); + // Parent's reservoir is fully restored (child's remaining 250 + child's used 150 = 400) + Assert.That(parent.StateReservoir, Is.EqualTo(400)); + // Parent's StateGasUsed is NOT merged with child's + Assert.That(parent.StateGasUsed, Is.EqualTo(20)); + } + + [Test] + public void Eip8037_spent_gas_subtracts_state_reservoir() + { + long gasLimit = 10_000; + EthereumGasPolicy gas = new() + { + Value = 3_000, + StateReservoir = 2_000, + StateGasUsed = 500, + }; + + long spentGas = gasLimit + - EthereumGasPolicy.GetRemainingGas(in gas) + - EthereumGasPolicy.GetStateReservoir(in gas); - TestAllTracerWithOutput receipt = Execute(BlockNumber, 21000 + gasUsed + (2299 - 800), Bytes.FromHexString(codeHex)); - Assert.That(receipt.StatusCode, Is.EqualTo(0)); + Assert.That(spentGas, Is.EqualTo(5_000)); } } } diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip7708Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip7708Tests.cs index d6da55fa947..ac1184069ec 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Eip7708Tests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Eip7708Tests.cs @@ -154,10 +154,12 @@ public async Task SelfDestruct_ToDifferentAccount_EmitsTransferLog(ulong contrac AssertLogs(chain.ReceiptStorage.Get(block), [ExpectedTransferLog(contractAddress, inheritor, contractBalance)], contractBalance != 0); } - [TestCase(1_000_000ul, 1, TestName = "selfdestruct to self")] - [TestCase(0ul, 0, TestName = "selfdestruct to self zero balance")] - public async Task SelfDestruct_ToSelf_EmitsSelfDestructLog(ulong contractBalance, int expectedLogCountWhenEnabled) + [TestCase(1_000_000ul, TestName = "selfdestruct to self")] + [TestCase(0ul, TestName = "selfdestruct to self zero balance")] + public async Task SelfDestruct_ToSelf_NoOp_EmitsNoLog(ulong contractBalance) { + // Post-EIP-6780: selfdestruct to self when contract was NOT created in the same tx + // is a complete no-op — no destruction, no ETH movement, no log. BasicTestBlockchain chain = await CreateChain(); UInt256 senderNonce = chain.StateReader.GetNonce(chain.BlockTree.Head!.Header, TestItem.AddressA); @@ -196,7 +198,8 @@ public async Task SelfDestruct_ToSelf_EmitsSelfDestructLog(ulong contractBalance Block block = await chain.AddBlock(callTx); - AssertLogs(chain.ReceiptStorage.Get(block), [ExpectedSelfDestructLog(contractAddress, contractBalance)], contractBalance != 0); + // No-op selfdestruct should emit no logs — no ETH moves + AssertLogs(chain.ReceiptStorage.Get(block), []); } [Test] diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip7928Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip7928Tests.cs index c847e0c17fd..a47889b96b9 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Eip7928Tests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Eip7928Tests.cs @@ -32,7 +32,7 @@ public class Eip7928Tests() : VirtualMachineTestsBase private static readonly EthereumEcdsa _ecdsa = new(0); private static readonly UInt256 _accountBalance = 10.Ether; private static readonly UInt256 _testAccountBalance = 1.Ether; - private static readonly long _gasLimit = 100000; + private static readonly long _gasLimit = 150000; private static readonly Address _testAddress = ContractAddress.From(TestItem.AddressA, 0); private static readonly Address _callTargetAddress = TestItem.AddressC; private static readonly Address _delegationTargetAddress = TestItem.AddressD; @@ -55,9 +55,16 @@ public async Task Constructs_BAL_when_processing_code( UInt256 value = _testAccountBalance; + Transaction templateTx = Build.A.Transaction + .WithCode(code) + .WithGasLimit(0) + .WithValue(value) + .TestObject; + long gasLimit = IntrinsicGasCalculator.Calculate(templateTx, Amsterdam.Instance).MinimalGas + _gasLimit; + Transaction createTx = Build.A.Transaction .WithCode(code) - .WithGasLimit(_gasLimit) + .WithGasLimit(gasLimit) .WithValue(value) .SignedAndResolved(_ecdsa, TestItem.PrivateKeyA).TestObject; Block block = Build.A.Block.TestObject; diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip7954Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip7954Tests.cs new file mode 100644 index 00000000000..f9537d97b64 --- /dev/null +++ b/src/Nethermind/Nethermind.Evm.Test/Eip7954Tests.cs @@ -0,0 +1,97 @@ +// 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.Tracing; +using Nethermind.Evm.TransactionProcessing; +using Nethermind.Specs; +using Nethermind.Specs.Forks; +using NUnit.Framework; + +namespace Nethermind.Evm.Test; + +public class Eip7954Tests : VirtualMachineTestsBase +{ + protected override long BlockNumber => MainnetSpecProvider.ParisBlockNumber; + protected override ulong Timestamp => MainnetSpecProvider.AmsterdamBlockTimestamp; + + [Test] + public void MaxCodeSize_and_MaxInitCodeSize_are_correct() + { + Assert.That(Spec.MaxCodeSize, Is.EqualTo(CodeSizeConstants.MaxCodeSizeEip7954)); + Assert.That(Spec.MaxInitCodeSize, Is.EqualTo(2L * CodeSizeConstants.MaxCodeSizeEip7954)); + } + + [TestCase(true, 55000, ExpectedResult = false, TestName = "InitCode_between_old_and_new_limit_accepted")] + [TestCase(false, 55000, ExpectedResult = true, TestName = "InitCode_above_old_limit_rejected_before_eip7954")] + [TestCase(true, 2 * CodeSizeConstants.MaxCodeSizeEip7954 + 1, ExpectedResult = true, TestName = "InitCode_exceeding_new_limit_rejected")] + public bool InitCode_size_validation(bool eip7954Enabled, int initCodeSize) => + ExecuteRawCreateTransaction(eip7954Enabled ? Timestamp : MainnetSpecProvider.ShanghaiBlockTimestamp, initCodeSize) == TransactionResult.TransactionSizeOverMaxInitCodeSize; + + [TestCase(30000, ExpectedResult = StatusCode.Success, TestName = "Code_deposit_between_old_and_new_limit_succeeds")] + [TestCase(CodeSizeConstants.MaxCodeSizeEip7954 + 1, ExpectedResult = StatusCode.Failure, TestName = "Code_deposit_above_new_limit_fails")] + public byte Code_deposit_size_validation(int deployedCodeSize) => ExecuteDeployTransaction(Timestamp, deployedCodeSize).StatusCode; + + private TransactionResult ExecuteRawCreateTransaction(ulong timestamp, int initCodeSize) + { + byte[] initCode = new byte[initCodeSize]; + + TestState.CreateAccount(TestItem.AddressC, 1.Ether); + + (Block block, Transaction transaction) = PrepareTx((BlockNumber, timestamp), 5_000_000, initCode); + + transaction.GasPrice = 2.GWei; + transaction.To = null; + transaction.Data = initCode; + return _processor.Execute(transaction, new BlockExecutionContext(block.Header, SpecProvider.GetSpec(block.Header)), NullTxTracer.Instance); + } + + [Test] + public void Eip8037_floor_gas_enforced_in_validate_gas() + { + // Craft a calldata-heavy tx where floor gas exceeds regular + state gas. + // 100 non-zero bytes: tokens = 100*4 = 400 + // regularGas = 21000 + 400*4 = 22600, stateGas = 0, floorGas = 21000 + 400*10 = 25000 + // gasLimit = 23000 is between regularGas and floorGas — must be rejected. + byte[] calldata = new byte[100]; + for (int i = 0; i < calldata.Length; i++) calldata[i] = 0xFF; + + TestState.CreateAccount(TestItem.AddressC, 1.Ether); + + (Block block, Transaction transaction) = PrepareTx(Activation, 23000, null); + transaction.Data = calldata; + transaction.To = TestItem.AddressC; + + TransactionResult result = _processor.Execute( + transaction, + new BlockExecutionContext(block.Header, SpecProvider.GetSpec(block.Header)), + NullTxTracer.Instance); + + Assert.That(result, Is.EqualTo(TransactionResult.GasLimitBelowIntrinsicGas)); + } + + private TestAllTracerWithOutput ExecuteDeployTransaction(ulong timestamp, int deployedCodeSize) + { + // Build minimal init code: PUSH size, PUSH 0, RETURN + // Returns deployedCodeSize zero bytes from memory as deployed contract code + byte[] initCode = Prepare.EvmCode + .PushData(deployedCodeSize) + .PushData(0) + .Op(Instruction.RETURN) + .Done; + + TestState.CreateAccount(TestItem.AddressC, 1.Ether); + + (Block block, Transaction transaction) = PrepareTx((BlockNumber, timestamp), 7_500_000, initCode, blockGasLimit: 50_000_000); + + transaction.GasPrice = 2.GWei; + transaction.To = null; + transaction.Data = initCode; + TestAllTracerWithOutput tracer = CreateTracer(); + _processor.Execute(transaction, new BlockExecutionContext(block.Header, Amsterdam.NoEip8037Instance), tracer); + return tracer; + } +} diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip8024Tests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip8024Tests.cs index 9995669d7aa..f355266fd14 100644 --- a/src/Nethermind/Nethermind.Evm.Test/Eip8024Tests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/Eip8024Tests.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System.Collections.Generic; using FluentAssertions; using Nethermind.Core; using Nethermind.Core.Specs; @@ -18,562 +19,163 @@ public class Eip8024Tests : VirtualMachineTestsBase { protected override long BlockNumber => MainnetSpecProvider.ParisBlockNumber; protected override ulong Timestamp => MainnetSpecProvider.OsakaBlockTimestamp; + protected override ISpecProvider SpecProvider => new TestSpecProvider(new Osaka { IsEip8024Enabled = true }); - protected override ISpecProvider SpecProvider => new TestSpecProvider(new Osaka() { IsEip8024Enabled = true }); + private static Prepare PushNValues(int count) => Prepare.EvmCode.For(count, static (p, i) => p.PushData(i + 1)); + private static Prepare PushZeros(int count) => Prepare.EvmCode.For(count, static (p, _) => p.PushData(0)); - [Test] - public void DupN_ValidImmediate_DuplicatesStackElement() + private static IEnumerable SuccessTestCases() { - // Push values 1-20 onto the stack, then DUPN with immediate 0x00 (depth=17) - // Should duplicate the 17th element from top - byte[] code = Prepare.EvmCode - .PushData(1).PushData(2).PushData(3).PushData(4).PushData(5) - .PushData(6).PushData(7).PushData(8).PushData(9).PushData(10) - .PushData(11).PushData(12).PushData(13).PushData(14).PushData(15) - .PushData(16).PushData(17).PushData(18).PushData(19).PushData(20) - .Op(Instruction.DUPN).Data(0x00) // DUPN with immediate 0x00 -> depth=17 - .MSTORE(0) - .Return(32, 0) - .Done; - - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Success); + // DUPN valid: 20 items, depth=17, position 17 from top = value 4 + yield return new TestCaseData(PushNValues(20).Op(Instruction.DUPN).Data(0x80).MSTORE(0).Return(32, 0).Done, 4).SetName("DupN_ValidImmediate"); - // Stack: [1, 2, 3, 4, 5, ..., 20] with 20 on top (20 items) - // Position 17 from top = index 20-17 = 3 = value 4 - // After DUPN: top = 4 - new UInt256(result.ReturnValue, true).Should().Be(4); - } + // SWAPN valid: 20 items, depth=17, swap top (20) with 18th from top (3) + yield return new TestCaseData(PushNValues(20).Op(Instruction.SWAPN).Data(0x80).MSTORE(0).Return(32, 0).Done, 3).SetName("SwapN_ValidImmediate"); - [Test] - public void DupN_DisallowedImmediate_Fails() - { - // DUPN with disallowed immediate 0x5b - byte[] code = Prepare.EvmCode - .PushData(1).PushData(2).PushData(3).PushData(4).PushData(5) - .PushData(6).PushData(7).PushData(8).PushData(9).PushData(10) - .PushData(11).PushData(12).PushData(13).PushData(14).PushData(15) - .PushData(16).PushData(17).PushData(18).PushData(19).PushData(20) - .Op(Instruction.DUPN).Data(0x5b) // Disallowed immediate - .Done; + // Exchange valid: 5 items, (n=3,m=4) swaps 1-based stack positions 3 and 4, top unchanged + yield return new TestCaseData(PushNValues(5).Op(Instruction.EXCHANGE).Data(0x9d).MSTORE(0).Return(32, 0).Done, 5).SetName("Exchange_ValidImmediate"); - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Failure); - } + // Exchange edge cases: newly valid 0x50 (was disallowed in old range 80-127) + yield return new TestCaseData(PushNValues(17).Op(Instruction.EXCHANGE).Data(0x50).MSTORE(0).Return(32, 0).Done, 17).SetName("Exchange_NewlyValid_0x50"); - [Test] - public void DupN_StackUnderflow_Fails() - { - // DUPN with immediate 0x00 (depth=17) but only 10 elements on stack - byte[] code = Prepare.EvmCode - .PushData(1).PushData(2).PushData(3).PushData(4).PushData(5) - .PushData(6).PushData(7).PushData(8).PushData(9).PushData(10) - .Op(Instruction.DUPN).Data(0x00) // depth=17, but only 10 elements - .Done; + // Exchange edge cases: newly valid 0x51 + yield return new TestCaseData(PushNValues(16).Op(Instruction.EXCHANGE).Data(0x51).MSTORE(0).Return(32, 0).Done, 16).SetName("Exchange_NewlyValid_0x51"); - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Failure); - } + // Exchange high range: 0x2f -> k=0xa0, (n=2,m=20) 1-indexed, needs 20 items + yield return new TestCaseData(PushNValues(20).Op(Instruction.EXCHANGE).Data(0x2f).MSTORE(0).Return(32, 0).Done, 20).SetName("Exchange_HighRange_0x2f"); - [Test] - public void SwapN_ValidImmediate_SwapsStackElements() - { - // Push values, then SWAPN with immediate 0x00 (depth=17) - // Should swap top with 17th element from top - byte[] code = Prepare.EvmCode - .PushData(1).PushData(2).PushData(3).PushData(4).PushData(5) - .PushData(6).PushData(7).PushData(8).PushData(9).PushData(10) - .PushData(11).PushData(12).PushData(13).PushData(14).PushData(15) - .PushData(16).PushData(17).PushData(18).PushData(19).PushData(20) - .Op(Instruction.SWAPN).Data(0x00) // SWAPN with immediate 0x00 -> depth=17 - .MSTORE(0) // Store new top - .Return(32, 0) - .Done; + // Exchange edge: 0xC0 -> k=0x4f=79, (n=6,m=17) 1-indexed, needs 17 items + yield return new TestCaseData(PushNValues(17).Op(Instruction.EXCHANGE).Data(0xC0).MSTORE(0).Return(32, 0).Done, 17).SetName("Exchange_EdgeCase_0xC0"); - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Success); + // Exchange edge: 0xDF -> k=0x50=80, (n=2,m=25) 1-indexed, needs 25 items + yield return new TestCaseData(PushNValues(25).Op(Instruction.EXCHANGE).Data(0xDF).MSTORE(0).Return(32, 0).Done, 25).SetName("Exchange_EdgeCase_0xDF"); - // Stack: [1, 2, 3, 4, 5, ..., 20] with 20 on top (20 items) - // SWAPN decode(0x00)=17: swap top (20) with (n+1)th=18th item from top (value 3) - // After swap: top = 3 - new UInt256(result.ReturnValue, true).Should().Be(3); - } + // EIP test vector: PUSH1 1, PUSH1 0, DUP1 x15, DUPN 0x80 -> duplicates bottom item (1) + yield return new TestCaseData(Prepare.EvmCode.PushData(1).PushData(0).Dup1Chain(15).Op(Instruction.DUPN).Data(0x80).MSTORE(0).Return(32, 0).Done, 1).SetName("EipTestVector_DupN_18Items"); - [Test] - public void SwapN_DisallowedImmediate_Fails() - { - // SWAPN with disallowed immediate 0x7f - byte[] code = Prepare.EvmCode - .PushData(1).PushData(2).PushData(3).PushData(4).PushData(5) - .PushData(6).PushData(7).PushData(8).PushData(9).PushData(10) - .PushData(11).PushData(12).PushData(13).PushData(14).PushData(15) - .PushData(16).PushData(17).PushData(18).PushData(19).PushData(20) - .Op(Instruction.SWAPN).Data(0x7f) // Disallowed immediate - .Done; + // EIP test vector: PUSH1 1, PUSH1 0, DUP1 x15, PUSH1 2, SWAPN 0x80 -> swap top (2) with bottom (1) + yield return new TestCaseData(Prepare.EvmCode.PushData(1).PushData(0).Dup1Chain(15).PushData(2).Op(Instruction.SWAPN).Data(0x80).MSTORE(0).Return(32, 0).Done, 1).SetName("EipTestVector_SwapN_18Items"); - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Failure); + // EIP test vector: PUSH1 0, PUSH1 1, PUSH1 2, EXCHANGE 0x8e -> swaps positions 1,2, top stays 2 + yield return new TestCaseData(Prepare.EvmCode.PushData(0).PushData(1).PushData(2).Op(Instruction.EXCHANGE).Data(0x8e).MSTORE(0).Return(32, 0).Done, 2).SetName("EipTestVector_Exchange_3Items"); } - [Test] - public void SwapN_StackUnderflow_Fails() - { - // SWAPN with immediate 0x00 (depth=17) but only 10 elements - byte[] code = Prepare.EvmCode - .PushData(1).PushData(2).PushData(3).PushData(4).PushData(5) - .PushData(6).PushData(7).PushData(8).PushData(9).PushData(10) - .Op(Instruction.SWAPN).Data(0x00) - .Done; - - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Failure); - } - - [Test] - public void Exchange_ValidImmediate_ExchangesStackElements() + [TestCaseSource(nameof(SuccessTestCases))] + public void ValidOperation_Succeeds(byte[] code, int expectedReturn) { - // Push values, then EXCHANGE with immediate 0x12 -> decode_pair gives (3, 4) - // Exchange positions 3 and 4 from top - byte[] code = Prepare.EvmCode - .PushData(1).PushData(2).PushData(3).PushData(4).PushData(5) - .Op(Instruction.EXCHANGE).Data(0x12) // n=3, m=4 -> exchange positions 3 and 4 - .MSTORE(0) // Store top - .Return(32, 0) - .Done; - TestAllTracerWithOutput result = Execute(code); result.StatusCode.Should().Be(StatusCode.Success); - - // Stack before EXCHANGE: [1, 2, 3, 4, 5] (5 on top) - // decode_pair(0x12) returns n=3, m=4 - // EXCHANGE swaps positions 3 and 4 from top (values 3 and 2) - // After EXCHANGE: stack = [1, 3, 2, 4, 5] (5 still on top) - // MSTORE stores 5 at offset 0 - new UInt256(result.ReturnValue, true).Should().Be(5); + new UInt256(result.ReturnValue, true).Should().Be((UInt256)expectedReturn); } - [Test] - public void Exchange_DisallowedImmediate_Fails() + private static IEnumerable FailureTestCases() { - // EXCHANGE with disallowed immediate 0x50 - byte[] code = Prepare.EvmCode - .PushData(1).PushData(2).PushData(3).PushData(4).PushData(5) - .Op(Instruction.EXCHANGE).Data(0x50) // Disallowed immediate - .Done; + // Disallowed immediates (91-127 for DUPN/SWAPN, 82-127 for EXCHANGE) + yield return new TestCaseData(PushNValues(20).Op(Instruction.DUPN).Data(0x5b).Done).SetName("DupN_Disallowed_0x5b"); + yield return new TestCaseData(PushNValues(20).Op(Instruction.SWAPN).Data(0x7f).Done).SetName("SwapN_Disallowed_0x7f"); + yield return new TestCaseData(PushNValues(5).Op(Instruction.EXCHANGE).Data(0x52).Done).SetName("Exchange_Disallowed_0x52"); - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Failure); - } + // Stack underflow: depth exceeds available items + yield return new TestCaseData(PushNValues(10).Op(Instruction.DUPN).Data(0x80).Done).SetName("DupN_StackUnderflow"); + yield return new TestCaseData(PushNValues(10).Op(Instruction.SWAPN).Data(0x80).Done).SetName("SwapN_StackUnderflow"); + yield return new TestCaseData(PushNValues(2).Op(Instruction.EXCHANGE).Data(0x9d).Done).SetName("Exchange_StackUnderflow"); - [Test] - public void Exchange_DisallowedRange_AllFail() - { - // Full disallowed range 0x50-0x7f - for (byte imm = 0x50; imm <= 0x7f; imm++) - { - Prepare prepare = Prepare.EvmCode; - for (int i = 0; i < 32; i++) prepare.PushData(i); - byte[] code = prepare.Op(Instruction.EXCHANGE).Data(imm).Done; + // Missing immediate at end of code is now a graceful STOP (EIP-8024 spec) + // Moved to EndOfCode_ActsAsStop test below - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Failure, $"Immediate 0x{imm:X2} should fail"); - } - } + // Max depth: immediate 0x5a -> depth=235, only 234 items + yield return new TestCaseData(PushZeros(234).Op(Instruction.DUPN).Data(0x5a).Done).SetName("DupN_MaxDepth_235"); + yield return new TestCaseData(PushZeros(234).Op(Instruction.SWAPN).Data(0x5a).Done).SetName("SwapN_MaxDepth_235"); - [Test] - public void Exchange_HighRangeImmediate_Succeeds() - { - // Test 0xd0: k=160, q=10, r=0, q>=r -> n=r+2=2, m=(29-q)+1=20 - // Exchange(2, 20) needs 20 items on stack - Prepare prepare = Prepare.EvmCode; - for (int i = 1; i <= 20; i++) prepare.PushData(i); - byte[] code = prepare - .Op(Instruction.EXCHANGE).Data(0xd0) - .MSTORE(0).Return(32, 0) - .Done; - - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Success); - // Stack top is 20, which is stored - new UInt256(result.ReturnValue, true).Should().Be(20); - } - - [Test] - public void Exchange_EdgeCase_0x4f_Succeeds() - { - // 0x4f: k=79, q=4, r=15, q (5, 16) -> Exchange(6, 17) - // Need 17 items on stack - Prepare prepare = Prepare.EvmCode; - for (int i = 1; i <= 17; i++) prepare.PushData(i); - byte[] code = prepare - .Op(Instruction.EXCHANGE).Data(0x4f) - .MSTORE(0).Return(32, 0) - .Done; - - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Success); - } - - [Test] - public void Exchange_EdgeCase_0x80_Succeeds() - { - // 0x80: k=80 (128-48), q=5, r=0, q>=r -> n=r+2=2, m=(29-5)+1=25 - // Exchange(2, 25) needs 25 items on stack - Prepare prepare = Prepare.EvmCode; - for (int i = 1; i <= 25; i++) prepare.PushData(i); - byte[] code = prepare - .Op(Instruction.EXCHANGE).Data(0x80) - .MSTORE(0).Return(32, 0) - .Done; - - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Success); - } - - [Test] - public void Exchange_StackUnderflow_Fails() - { - // EXCHANGE with immediate 0x12 -> decode_pair gives (3, 4) - // Exchange(n+1, m+1) = Exchange(3, 4) requires at least 4 items on stack - byte[] code = Prepare.EvmCode - .PushData(1).PushData(2) - .Op(Instruction.EXCHANGE).Data(0x12) // n=3, m=4 -> needs at least 4 elements - .Done; - - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Failure); - } + // Exchange max depth: immediate 0x8f -> (n=2,m=30) 1-indexed, needs 30 items, only 29 + yield return new TestCaseData(PushZeros(29).Op(Instruction.EXCHANGE).Data(0x8f).Done).SetName("Exchange_MaxDepth_30"); - [Test] - public void DupN_MissingImmediateAtEnd_Fails() - { - byte[] code = Prepare.EvmCode - .Op(Instruction.DUPN) - .Done; + // EIP test vector: SWAPN with disallowed immediate 0x5b + yield return new TestCaseData(new byte[] { 0xe7, 0x5b }).SetName("EipTestVector_InvalidSwapn"); - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Failure); + // EIP test vector: 16 items (PUSH1 0 + DUP1 x15), DUPN depth=17 -> underflow + yield return new TestCaseData(PushZeros(1).Dup1Chain(15).Op(Instruction.DUPN).Data(0x80).Op(Instruction.STOP).Done).SetName("EipTestVector_DupN_StackUnderflow"); } - [Test] - public void SwapN_MissingImmediateAtEnd_Fails() + [TestCaseSource(nameof(FailureTestCases))] + public void InvalidOperation_Fails(byte[] code) { - byte[] code = Prepare.EvmCode - .Op(Instruction.SWAPN) - .Done; - TestAllTracerWithOutput result = Execute(code); result.StatusCode.Should().Be(StatusCode.Failure); } - [Test] - public void Exchange_MissingImmediateAtEnd_Fails() + private static IEnumerable GasCostTestCases() { - byte[] code = Prepare.EvmCode - .Op(Instruction.EXCHANGE) - .Done; + long Gas(int pushCount) => GasCostOf.Transaction + GasCostOf.VeryLow * pushCount + GasCostOf.VeryLow; - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Failure); + yield return new TestCaseData(PushNValues(20).Op(Instruction.DUPN).Data(0x80).Op(Instruction.STOP).Done, Gas(20)).SetName("DupN_GasCost"); + yield return new TestCaseData(PushNValues(20).Op(Instruction.SWAPN).Data(0x80).Op(Instruction.STOP).Done, Gas(20)).SetName("SwapN_GasCost"); + yield return new TestCaseData(PushNValues(5).Op(Instruction.EXCHANGE).Data(0x9d).Op(Instruction.STOP).Done, Gas(5)).SetName("Exchange_GasCost"); } - [Test] - public void DupN_MaxDepth_StackUnderflow() - { - Prepare prepare = Prepare.EvmCode; - for (int i = 0; i < 234; i++) - { - prepare.PushData(0); - } - - byte[] code = prepare - .Op(Instruction.DUPN).Data(0xff) // depth=235 - .Done; - - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Failure); - } - - [Test] - public void SwapN_MaxDepth_StackUnderflow() + [TestCaseSource(nameof(GasCostTestCases))] + public void Opcode_CostsVeryLowGas(byte[] code, long expectedGas) { - Prepare prepare = Prepare.EvmCode; - for (int i = 0; i < 234; i++) - { - prepare.PushData(0); - } - - byte[] code = prepare - .Op(Instruction.SWAPN).Data(0xff) // depth=235 - .Done; - - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Failure); - } - - [Test] - public void Exchange_MaxDepth_StackUnderflow() - { - Prepare prepare = Prepare.EvmCode; - for (int i = 0; i < 29; i++) - { - prepare.PushData(0); - } - - byte[] code = prepare - .Op(Instruction.EXCHANGE).Data(0x00) // n=2, m=30 -> needs depth 30, only 29 items - .Done; - - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Failure); - } - - [Test] - public void DupN_CostsVeryLowGas() - { - // Gas cost should be 3 (VeryLow) - const long expectedGas = GasCostOf.Transaction + GasCostOf.VeryLow * 20 + GasCostOf.VeryLow; // 20 PUSH + DUPN - - byte[] code = Prepare.EvmCode - .PushData(1).PushData(2).PushData(3).PushData(4).PushData(5) - .PushData(6).PushData(7).PushData(8).PushData(9).PushData(10) - .PushData(11).PushData(12).PushData(13).PushData(14).PushData(15) - .PushData(16).PushData(17).PushData(18).PushData(19).PushData(20) - .Op(Instruction.DUPN).Data(0x00) - .Op(Instruction.STOP) - .Done; - - TestAllTracerWithOutput result = Execute(Activation, 100000, code); - result.StatusCode.Should().Be(StatusCode.Success); - AssertGas(result, expectedGas); - } - - [Test] - public void SwapN_CostsVeryLowGas() - { - // Gas cost should be 3 (VeryLow) - const long expectedGas = GasCostOf.Transaction + GasCostOf.VeryLow * 20 + GasCostOf.VeryLow; // 20 PUSH + SWAPN - - byte[] code = Prepare.EvmCode - .PushData(1).PushData(2).PushData(3).PushData(4).PushData(5) - .PushData(6).PushData(7).PushData(8).PushData(9).PushData(10) - .PushData(11).PushData(12).PushData(13).PushData(14).PushData(15) - .PushData(16).PushData(17).PushData(18).PushData(19).PushData(20) - .Op(Instruction.SWAPN).Data(0x00) - .Op(Instruction.STOP) - .Done; - TestAllTracerWithOutput result = Execute(Activation, 100000, code); result.StatusCode.Should().Be(StatusCode.Success); AssertGas(result, expectedGas); } - [Test] - public void Exchange_CostsVeryLowGas() - { - // Gas cost should be 3 (VeryLow) - const long expectedGas = GasCostOf.Transaction + GasCostOf.VeryLow * 5 + GasCostOf.VeryLow; // 5 PUSH + EXCHANGE - - byte[] code = Prepare.EvmCode - .PushData(1).PushData(2).PushData(3).PushData(4).PushData(5) - .Op(Instruction.EXCHANGE).Data(0x12) // decode_pair(0x12) = (3, 4) - .Op(Instruction.STOP) - .Done; - - TestAllTracerWithOutput result = Execute(Activation, 100000, code); - result.StatusCode.Should().Be(StatusCode.Success); - AssertGas(result, expectedGas); - } - - [Test] - public void EipTestVector_DupN_18Items_TopIs1() - { - // EIP test: 60016000808080808080808080808080808080e600 - // PUSH1 01, PUSH1 00, DUP1 x15, DUPN 0x00 - // Note: The bytecode has 15 DUP1s (30 hex chars = 15 bytes) - // After setup: stack = [1, 0, 0, ...] (17 items, 1 at bottom) - // DUPN with decode(0)=17: duplicate 17th item from top = bottom item = 1 - // Result: 18 items with top = 1 - byte[] code = Prepare.EvmCode - .PushData(1) - .PushData(0) - .Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1) - .Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1) - .Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1) - .Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1) // 15 DUP1s total - .Op(Instruction.DUPN).Data(0x00) - .MSTORE(0) - .Return(32, 0) - .Done; - - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Success); - new UInt256(result.ReturnValue, true).Should().Be(1); - } - - [Test] - public void EipTestVector_SwapN_18Items_TopIs0() - { - // EIP test: 600160008080808080808080808080808080806002e700 - // PUSH1 01, PUSH1 00, DUP1 x15, PUSH1 02, SWAPN 0x00 - // After setup: stack = [1, 0, 0, ..., 0, 2] (18 items with 2 on top, 1 at bottom) - // SWAPN decode(0x00)=17: swap top (2) with (n+1)th=18th item from top (value 1) - // Result: top = 1 - byte[] code = Prepare.EvmCode - .PushData(1) - .PushData(0) - .Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1) - .Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1) - .Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1) - .Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1) // 15 DUP1s total - .PushData(2) - .Op(Instruction.SWAPN).Data(0x00) // SWAPN with depth=17 - .MSTORE(0) - .Return(32, 0) - .Done; - - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Success); - // After SWAPN decode(0)=17: swap top (2) with (n+1)th=18th from top = 1 - new UInt256(result.ReturnValue, true).Should().Be(1); - } - - [Test] - public void EipTestVector_Exchange_3Items() + [TestCase(new byte[] { 0xe6 }, TestName = "DupN_MissingImmediate")] + [TestCase(new byte[] { 0xe7 }, TestName = "SwapN_MissingImmediate")] + [TestCase(new byte[] { 0xe8 }, TestName = "Exchange_MissingImmediate")] + public void EndOfCode_ActsAsStop(byte[] code) { - // EIP test: 600060016002e801 - // PUSH1 00, PUSH1 01, PUSH1 02, EXCHANGE 0x01 - // Stack before: [0, 1, 2] (2 on top) - // decode_pair(0x01): k=1, q=0, r=1, q (1, 2) - // TryDecodePair returns n=2, m=3, so EXCHANGE swaps stack positions 2 and 3 from the top. - // After: [1, 0, 2] (2 on top), matching the EIP vector. - byte[] code = Prepare.EvmCode - .PushData(0) - .PushData(1) - .PushData(2) - .Op(Instruction.EXCHANGE).Data(0x01) - .MSTORE(0) - .Return(32, 0) - .Done; - + // When DUPN/SWAPN/EXCHANGE appears at end of code with no immediate byte, + // it acts as a graceful STOP per EIP-8024 spec. TestAllTracerWithOutput result = Execute(code); result.StatusCode.Should().Be(StatusCode.Success); - new UInt256(result.ReturnValue, true).Should().Be(2); } [Test] - public void EipTestVector_InvalidSwapn_Reverts() + public void Exchange_DisallowedRange_AllFail() { - // EIP test: e75b - SWAPN with disallowed immediate 0x5b causes revert - byte[] code = new byte[] { 0xe7, 0x5b }; - - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Failure); + for (byte imm = 0x52; imm <= 0x7f; imm++) + { + byte[] code = PushNValues(32).Op(Instruction.EXCHANGE).Data(imm).Done; + TestAllTracerWithOutput result = Execute(code); + result.StatusCode.Should().Be(StatusCode.Failure, $"Immediate 0x{imm:X2} should fail"); + } } [Test] public void EipTestVector_JumpOverInvalidDupn_Succeeds() { - // EIP test: 600456e65b - // PUSH1 04, JUMP, INVALID_DUPN (e6), JUMPDEST (5b) - // The JUMP skips over the invalid DUPN to land on JUMPDEST - // This tests that e65b is properly parsed as INVALID_DUPN followed by JUMPDEST - byte[] code = new byte[] { 0x60, 0x04, 0x56, 0xe6, 0x5b, 0x00 }; // Added STOP at end - + // PUSH1 04, JUMP, DUPN (e6), JUMPDEST (5b), STOP + // JUMP skips over the invalid DUPN to land on JUMPDEST + byte[] code = [0x60, 0x04, 0x56, 0xe6, 0x5b, 0x00]; TestAllTracerWithOutput result = Execute(code); result.StatusCode.Should().Be(StatusCode.Success); } - [Test] - public void EipTestVector_DupN_StackUnderflow_ExceptionalHalt() - { - // EIP test: 6000808080808080808080808080808080e600 - // PUSH1 00, DUP1 x15, DUPN 0x00 - // Stack has 16 items, DUPN 17 needs depth 17 -> exceptional halt. - byte[] code = Prepare.EvmCode - .PushData(0) - .Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1) - .Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1) - .Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1) - .Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1) // 15 DUP1s total - .Op(Instruction.DUPN).Data(0x00) - .Op(Instruction.STOP) - .Done; - - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Failure); - } - - [Test] - public void EipTestVector_DupN_16Items_StackUnderflow() - { - // Test actual underflow: 16 items but need depth 17 - byte[] code = Prepare.EvmCode - .PushData(0) - .Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1) - .Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1) - .Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1) - .Op(Instruction.DUP1).Op(Instruction.DUP1).Op(Instruction.DUP1) // Only 15 DUP1s = 16 items - .Op(Instruction.DUPN).Data(0x00) // depth=17, but only 16 items - .Done; - - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Failure); - } - - /// - /// Test class with EIP-8024 disabled to verify opcodes fail when not enabled. - /// public class Eip8024DisabledTests : VirtualMachineTestsBase { protected override long BlockNumber => MainnetSpecProvider.ParisBlockNumber; protected override ulong Timestamp => MainnetSpecProvider.OsakaBlockTimestamp; - - // EIP-8024 is NOT enabled in this test class protected override ISpecProvider SpecProvider => new TestSpecProvider(new Osaka() { IsEip8024Enabled = false }); - [Test] - public void DupN_WhenDisabled_Fails() + private static IEnumerable DisabledTestCases() { - byte[] code = Prepare.EvmCode - .PushData(1).PushData(2).PushData(3).PushData(4).PushData(5) - .PushData(6).PushData(7).PushData(8).PushData(9).PushData(10) - .PushData(11).PushData(12).PushData(13).PushData(14).PushData(15) - .PushData(16).PushData(17).PushData(18).PushData(19).PushData(20) - .Op(Instruction.DUPN).Data(0x00) - .Done; - - TestAllTracerWithOutput result = Execute(code); - - // The DUPN opcode should fail as a bad instruction when EIP-8024 is not enabled - result.StatusCode.Should().Be(StatusCode.Failure); + yield return new TestCaseData(Prepare.EvmCode.For(20, static (p, i) => p.PushData(i + 1)).Op(Instruction.DUPN).Data(0x80).Done).SetName("DupN_WhenDisabled"); + yield return new TestCaseData(Prepare.EvmCode.For(20, static (p, i) => p.PushData(i + 1)).Op(Instruction.SWAPN).Data(0x80).Done).SetName("SwapN_WhenDisabled"); + yield return new TestCaseData(Prepare.EvmCode.For(5, static (p, i) => p.PushData(i + 1)).Op(Instruction.EXCHANGE).Data(0x9d).Done).SetName("Exchange_WhenDisabled"); } - [Test] - public void SwapN_WhenDisabled_Fails() + [TestCaseSource(nameof(DisabledTestCases))] + public void Opcode_WhenDisabled_Fails(byte[] code) { - byte[] code = Prepare.EvmCode - .PushData(1).PushData(2).PushData(3).PushData(4).PushData(5) - .PushData(6).PushData(7).PushData(8).PushData(9).PushData(10) - .PushData(11).PushData(12).PushData(13).PushData(14).PushData(15) - .PushData(16).PushData(17).PushData(18).PushData(19).PushData(20) - .Op(Instruction.SWAPN).Data(0x00) - .Done; - - TestAllTracerWithOutput result = Execute(code); - result.StatusCode.Should().Be(StatusCode.Failure); - } - - [Test] - public void Exchange_WhenDisabled_Fails() - { - byte[] code = Prepare.EvmCode - .PushData(1).PushData(2).PushData(3).PushData(4).PushData(5) - .Op(Instruction.EXCHANGE).Data(0x12) // decode_pair(0x12) = (3, 4) - .Done; - TestAllTracerWithOutput result = Execute(code); result.StatusCode.Should().Be(StatusCode.Failure); } } } + +internal static class PrepareExtensions +{ + public static Prepare Dup1Chain(this Prepare prepare, int count) => prepare.For(count, static (p, _) => p.Op(Instruction.DUP1)); +} diff --git a/src/Nethermind/Nethermind.Evm.Test/Eip8037RegressionTests.cs b/src/Nethermind/Nethermind.Evm.Test/Eip8037RegressionTests.cs new file mode 100644 index 00000000000..81817d42322 --- /dev/null +++ b/src/Nethermind/Nethermind.Evm.Test/Eip8037RegressionTests.cs @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Extensions; +using Nethermind.Core.Specs; +using Nethermind.Evm.Tracing; +using Nethermind.Int256; +using Nethermind.Specs; +using NUnit.Framework; + +namespace Nethermind.Evm.Test; + +public class Eip8037RegressionTests : VirtualMachineTestsBase +{ + protected override long BlockNumber => MainnetSpecProvider.ParisBlockNumber; + protected override ulong Timestamp => MainnetSpecProvider.AmsterdamBlockTimestamp; + + /// + /// When a nested CREATE's child frame has too little regular gas to cover both + /// the regular code deposit cost AND the state-gas spill, the CREATE must fail. + /// + /// Gas budget (1-byte deployed contract, child state reservoir = 0): + /// regularDepositCost = 6 (CodeDepositRegularPerWord × 1 word) + /// stateDepositCost = 1174 (CostPerStateByte × 1 byte) + /// stateSpill = 1174 (entire stateDepositCost spills into regular gas) + /// total regular needed = 6 + 1174 = 1180 + /// + /// Child ends with 1175 regular gas after init code — 5 short. + /// Without the fix, the pre-check passes (1175 ≥ 6 and 1175 ≥ 1174) and the + /// charge runs on the merged parent+child pool, silently borrowing parent gas. + /// + [Test] + public void Eip8037_nested_create_code_deposit_must_not_borrow_parent_regular_gas() + { + // Init code: deploys 1 byte of zeros from memory + // PUSH1 1, PUSH1 0, RETURN = 5 bytes, costs 9 gas (3+3+3 memory expansion) + byte[] initCode = Prepare.EvmCode + .PushData(1) + .PushData(0) + .Op(Instruction.RETURN) + .Done; + + // Factory code: CREATE(value=0, initCode), then RETURN the result (address or 0) + byte[] factoryCode = Prepare.EvmCode + .Create(initCode, UInt256.Zero) + // Stack: [address or 0] + .PushData(0) + .Op(Instruction.MSTORE) // store result at memory[0] + .PushData(32) + .PushData(0) + .Op(Instruction.RETURN) // return 32 bytes + .Done; + + // Gas calculation: + // Intrinsic (CALL to existing account): 21000 + // Factory pre-CREATE opcodes: 21 gas + // CREATE opcode costs: + // CreateRegular(9000) + InitCodeWord(2) = 9002 regular + // CreateState(131488) → spills entirely to regular (factory has 0 state reservoir) + // Total: 140490 regular + // Remaining after CREATE costs: 1202 + // 63/64 rule: callGas = 1202 - floor(1202/64) = 1184, factory retains 18 + // Child: 1184 gas → 9 for init code → 1175 remaining for code deposit + // Factory post-CREATE: 12 gas (PUSH, MSTORE, PUSH, PUSH, RETURN) + // Total: 21000 + 21 + 140490 + 1202 = 162713 + long gasLimit = 162713; + + TestAllTracerWithOutput tracer = Execute(Activation, gasLimit, factoryCode); + + // Transaction succeeds (factory runs fine), but the nested CREATE must fail + // because the child can't afford the code deposit from its own gas alone. + Assert.That(tracer.StatusCode, Is.EqualTo(StatusCode.Success), "Factory execution should succeed"); + + // CREATE result: 0 = failure (returned in the 32-byte output) + byte[] returnData = tracer.ReturnValue; + Assert.That(returnData.IsZero(), Is.True, + "Nested CREATE should fail: child has 1175 gas but needs 1180 for code deposit (6 regular + 1174 state spill)"); + } +} diff --git a/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs b/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs index 23d53fa5540..a2694cdd6bc 100644 --- a/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs +++ b/src/Nethermind/Nethermind.Evm.Test/IntrinsicGasCalculatorTests.cs @@ -11,7 +11,9 @@ using Nethermind.Core.Extensions; using Nethermind.Core.Specs; using Nethermind.Core.Test.Builders; +using Nethermind.Evm.GasPolicy; using Nethermind.Int256; +using Nethermind.Specs; using Nethermind.Specs.Forks; using NUnit.Framework; @@ -226,5 +228,57 @@ public void Calculate_TxHasAuthorizationListBeforePrague_ThrowsInvalidDataExcept Assert.That(() => IntrinsicGasCalculator.Calculate(tx, Cancun.Instance), Throws.InstanceOf()); } + + [Test] + public void Eip8037_policy_intrinsic_gas_splits_authorization_cost() + { + Transaction tx = Build.A.Transaction.SignedAndResolved() + .WithAuthorizationCode(new AuthorizationTuple(1, TestItem.AddressF, 0, 0, UInt256.One, UInt256.One)) + .TestObject; + IntrinsicGas intrinsicGas = EthereumGasPolicy.CalculateIntrinsicGas(tx, Amsterdam.Instance); + + Assert.That(intrinsicGas.Standard.Value, Is.EqualTo(GasCostOf.Transaction + GasCostOf.PerAuthBaseRegular)); + Assert.That(intrinsicGas.Standard.StateReservoir, Is.EqualTo(GasCostOf.NewAccountState + GasCostOf.PerAuthBaseState)); + } + + [Test] + public void Eip8037_nongeneric_intrinsic_gas_includes_state_gas_for_create() + { + Transaction tx = Build.A.Transaction.SignedAndResolved() + .WithCode(Array.Empty()) + .TestObject; + EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, Amsterdam.Instance); + + long expectedRegular = GasCostOf.Transaction + GasCostOf.CreateRegular; + long expectedState = GasCostOf.CreateState; + Assert.That(gas.Standard, Is.EqualTo(expectedRegular + expectedState)); + Assert.That(gas.MinimalGas, Is.EqualTo(Math.Max(gas.Standard, gas.FloorGas))); + } + + [Test] + public void Eip8037_nongeneric_intrinsic_gas_includes_state_gas_for_setcode() + { + Transaction tx = Build.A.Transaction.SignedAndResolved() + .WithAuthorizationCode(new AuthorizationTuple(1, TestItem.AddressF, 0, 0, UInt256.One, UInt256.One)) + .TestObject; + EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, Amsterdam.Instance); + + long expectedRegular = GasCostOf.Transaction + GasCostOf.PerAuthBaseRegular; + long expectedState = GasCostOf.NewAccountState + GasCostOf.PerAuthBaseState; + Assert.That(gas.Standard, Is.EqualTo(expectedRegular + expectedState)); + } + + [Test] + public void Eip8037_nongeneric_minimal_gas_is_at_least_regular_plus_state() + { + // A create tx with no calldata: floor gas is low, Standard = regular + state + Transaction tx = Build.A.Transaction.SignedAndResolved() + .WithCode(Array.Empty()) + .TestObject; + EthereumIntrinsicGas gas = IntrinsicGasCalculator.Calculate(tx, Amsterdam.Instance); + + long regularPlusState = GasCostOf.Transaction + GasCostOf.CreateRegular + GasCostOf.CreateState; + Assert.That(gas.MinimalGas, Is.GreaterThanOrEqualTo(regularPlusState)); + } } } diff --git a/src/Nethermind/Nethermind.Evm/ByteCodeBuilder.cs b/src/Nethermind/Nethermind.Evm/ByteCodeBuilder.cs index 5fdc2fe0da7..25701530a76 100644 --- a/src/Nethermind/Nethermind.Evm/ByteCodeBuilder.cs +++ b/src/Nethermind/Nethermind.Evm/ByteCodeBuilder.cs @@ -407,5 +407,17 @@ public Prepare ReturnInnerCallResult() Op(Instruction.RETURN); return this; } + + public Prepare For(int count, Action action) + { + ArgumentNullException.ThrowIfNull(action); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(count, 0); + for (int i = 0; i < count; i++) + { + action(this, i); + } + + return this; + } } } diff --git a/src/Nethermind/Nethermind.Evm/CodeDepositHandler.cs b/src/Nethermind/Nethermind.Evm/CodeDepositHandler.cs index 0e10d7b58fe..ea44bf69e96 100644 --- a/src/Nethermind/Nethermind.Evm/CodeDepositHandler.cs +++ b/src/Nethermind/Nethermind.Evm/CodeDepositHandler.cs @@ -11,10 +11,40 @@ namespace Nethermind.Evm public static class CodeDepositHandler { private const byte InvalidStartingCodeByte = 0xEF; + public static long CalculateCost(IReleaseSpec spec, int byteCodeLength) => - spec.LimitCodeSize && byteCodeLength > spec.MaxCodeSize - ? long.MaxValue - : GasCostOf.CodeDeposit * byteCodeLength; + CalculateCost(spec, byteCodeLength, out long regularCost, out long stateCost) + ? regularCost + stateCost + : long.MaxValue; + + public static bool CalculateCost(IReleaseSpec spec, int byteCodeLength, out long regularCost, out long stateCost) + { + stateCost = 0; + + if (spec.LimitCodeSize && byteCodeLength > spec.MaxCodeSize) + { + regularCost = long.MaxValue; + return false; + } + + if (!spec.IsEip8037Enabled) + { + regularCost = GasCostOf.CodeDeposit * byteCodeLength; + return true; + } + + long words = EvmCalculations.Div32Ceiling((ulong)byteCodeLength, out bool outOfGas); + if (outOfGas) + { + regularCost = long.MaxValue; + stateCost = long.MaxValue; + return false; + } + + regularCost = GasCostOf.CodeDepositRegularPerWord * words; + stateCost = GasCostOf.CodeDepositState * byteCodeLength; + return true; + } public static bool CodeIsInvalid(IReleaseSpec spec, ReadOnlyMemory code, int fromVersion) => !CodeIsValid(spec, code, fromVersion); diff --git a/src/Nethermind/Nethermind.Evm/CodeInfoRepository.cs b/src/Nethermind/Nethermind.Evm/CodeInfoRepository.cs index e1fc2cba125..b086c06949b 100644 --- a/src/Nethermind/Nethermind.Evm/CodeInfoRepository.cs +++ b/src/Nethermind/Nethermind.Evm/CodeInfoRepository.cs @@ -24,6 +24,7 @@ public class CodeInfoRepository : ICodeInfoRepository private readonly FrozenDictionary _localPrecompiles; private readonly IWorldState _worldState; private readonly Func _codeInfoLoader; + private readonly IBlockAccessListBuilder? _balBuilder; public CodeInfoRepository(IWorldState worldState, IPrecompileProvider precompileProvider) : this(worldState, precompileProvider, codeInfoLoader: null) @@ -34,6 +35,7 @@ internal CodeInfoRepository(IWorldState worldState, IPrecompileProvider precompi { _localPrecompiles = precompileProvider.GetPrecompiles(); _worldState = worldState; + _balBuilder = _worldState as IBlockAccessListBuilder; _codeInfoLoader = codeInfoLoader ?? DefaultLoad; CodeInfo DefaultLoad(ValueHash256 codeHash, IReleaseSpec spec) => @@ -45,7 +47,10 @@ public CodeInfo GetCachedCodeInfo(Address codeSource, bool followDelegation, IRe delegationAddress = null; if (vmSpec.IsPrecompile(codeSource)) { - _worldState.AddAccountRead(codeSource); + if (_balBuilder is not null && _balBuilder.TracingEnabled) + { + _balBuilder.AddAccountRead(codeSource); + } return _localPrecompiles[codeSource]; } diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs index 9415278537f..3bc803f9e72 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/EthereumGasPolicy.cs @@ -1,9 +1,7 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.IO; +using System; using System.Runtime.CompilerServices; using Nethermind.Core; using Nethermind.Core.Specs; @@ -12,11 +10,19 @@ namespace Nethermind.Evm.GasPolicy; /// -/// Standard Ethereum single-dimensional gas.Value policy. +/// Unified Ethereum gas policy supporting both legacy single-dimensional behavior and EIP-8037 +/// two-dimensional behavior when opcode dispatch and spec flags enable it. /// public struct EthereumGasPolicy : IGasPolicy { + /// Regular gas budget (legacy gas_left). public long Value; + /// State gas reservoir used by EIP-8037 paths. + public long StateReservoir; + /// Cumulative state gas used for block accounting. + public long StateGasUsed; + /// State gas that spilled from gas_left (for block regular gas exclusion). + public long StateGasSpill; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static EthereumGasPolicy FromLong(long value) => new() { Value = value }; @@ -25,8 +31,39 @@ public struct EthereumGasPolicy : IGasPolicy public static long GetRemainingGas(in EthereumGasPolicy gas) => gas.Value; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Consume(ref EthereumGasPolicy gas, long cost) => - gas.Value -= cost; + public static long GetStateReservoir(in EthereumGasPolicy gas) => gas.StateReservoir; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long GetStateGasUsed(in EthereumGasPolicy gas) => gas.StateGasUsed; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long GetStateGasSpill(in EthereumGasPolicy gas) => gas.StateGasSpill; + + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Consume(ref EthereumGasPolicy gas, long cost) => gas.Value -= cost; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ConsumeStateGas(ref EthereumGasPolicy gas, long stateGasCost) + { + if (gas.StateReservoir >= stateGasCost) + { + gas.StateReservoir -= stateGasCost; + gas.StateGasUsed += stateGasCost; + return true; + } + + long spillAmount = stateGasCost - gas.StateReservoir; + gas.StateReservoir = 0; + if (!UpdateGas(ref gas, spillAmount)) + { + return false; + } + + gas.StateGasUsed += stateGasCost; + gas.StateGasSpill += spillAmount; + return true; + } public static bool ConsumeSelfDestructGas(ref EthereumGasPolicy gas) => UpdateGas(ref gas, GasCostOf.SelfDestructEip150); @@ -39,8 +76,35 @@ public static void ConsumeCodeDeposit(ref EthereumGasPolicy gas, long cost) => Consume(ref gas, cost); [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void Refund(ref EthereumGasPolicy gas, in EthereumGasPolicy childGas) => + public static void Refund(ref EthereumGasPolicy gas, in EthereumGasPolicy childGas) + { gas.Value += childGas.Value; + gas.StateReservoir += childGas.StateReservoir; + gas.StateGasUsed += childGas.StateGasUsed; + gas.StateGasSpill += childGas.StateGasSpill; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void RestoreChildStateGas(ref EthereumGasPolicy parentGas, in EthereumGasPolicy childGas, long initialStateReservoir) + { + // On halt/revert, restore the full initial reservoir that was transferred to the child. + // Also restore the spill amount: state gas that spilled from gas_left is returned to the + // reservoir because the child's state operations are being reverted. + parentGas.StateReservoir += initialStateReservoir + childGas.StateGasSpill; + // Propagate spills — gas_left was consumed for state ops even though they're being reverted. + parentGas.StateGasSpill += childGas.StateGasSpill; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void RevertRefundToHalt(ref EthereumGasPolicy parentGas, in EthereumGasPolicy childGas, long initialStateReservoir) + { + // Code deposit failure is an exceptional halt: the child's state is reverted. + // Refund already added child.StateReservoir, child.StateGasUsed, and child.StateGasSpill to parent. + // Halt semantics require restoring the full initial reservoir (plus spill) and discarding child's StateGasUsed. + // The spill is added to the reservoir because the child's state ops are being reverted. + parentGas.StateReservoir += initialStateReservoir + childGas.StateGasSpill - childGas.StateReservoir; + parentGas.StateGasUsed -= childGas.StateGasUsed; + } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void SetOutOfGas(ref EthereumGasPolicy gas) => gas.Value = 0; @@ -73,18 +137,15 @@ public static bool ConsumeAccountAccessGas(ref EthereumGasPolicy gas, { if (isTracingAccess) { - // Ensure that tracing simulates access-list behavior. accessTracker.WarmUp(address); } - // If the account is cold (and not a precompile), charge the cold access cost. if (!spec.IsPrecompile(address) && accessTracker.WarmUp(address)) { result = UpdateGas(ref gas, GasCostOf.ColdAccountAccess); } else if (chargeForWarm) { - // Otherwise, if warm access should be charged, apply the warm read cost. result = UpdateGas(ref gas, GasCostOf.WarmStateRead); } } @@ -99,19 +160,15 @@ public static bool ConsumeStorageAccessGas(ref EthereumGasPolicy gas, StorageAccessType storageAccessType, IReleaseSpec spec) { - // If the spec requires hot/cold storage tracking, determine if extra gas should be charged. if (!spec.UseHotAndColdStorage) return true; - // When tracing access, ensure the storage cell is marked as warm to simulate inclusion in the access list. if (isTracingAccess) { accessTracker.WarmUp(in storageCell); } - // If the storage cell is still cold, apply the higher cold access cost and mark it as warm. if (accessTracker.WarmUp(in storageCell)) return UpdateGas(ref gas, GasCostOf.ColdSLoad); - // For SLOAD operations on already warmed-up storage, apply a lower warm-read cost. if (storageAccessType == StorageAccessType.SLOAD) return UpdateGas(ref gas, GasCostOf.WarmStateRead); return true; @@ -132,7 +189,7 @@ public static bool UpdateMemoryCost(ref EthereumGasPolicy gas, public static bool UpdateGas(ref EthereumGasPolicy gas, long gasCost) { - if (GetRemainingGas(gas) < gasCost) + if (GetRemainingGas(in gas) < gasCost) return false; Consume(ref gas, gasCost); @@ -147,10 +204,25 @@ public static void UpdateGasUp(ref EthereumGasPolicy gas, } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool ConsumeStorageWrite(ref EthereumGasPolicy gas, bool isSlotCreation, IReleaseSpec spec) + public static void RefundStateGas(ref EthereumGasPolicy gas, long amount, long stateGasFloor) { - long cost = isSlotCreation ? GasCostOf.SSet : spec.GasCosts.SStoreResetCost; - return UpdateGas(ref gas, cost); + gas.StateReservoir += amount; + long newFloor = Math.Max(0, stateGasFloor - amount); + gas.StateGasUsed = Math.Max(gas.StateGasUsed - amount, newFloor); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long GetCodeInsertRegularRefund(int codeInsertRefunds, IReleaseSpec spec) => + spec.IsEip8037Enabled || codeInsertRefunds <= 0 + ? 0 + : (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) + RefundStateGas(ref gas, GasCostOf.NewAccountState * codeInsertRefunds, stateGasFloor); + return GetCodeInsertRegularRefund(codeInsertRefunds, spec); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -159,7 +231,7 @@ public static bool ConsumeCallValueTransfer(ref EthereumGasPolicy gas) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ConsumeNewAccountCreation(ref EthereumGasPolicy gas) - => UpdateGas(ref gas, GasCostOf.NewAccount); + => ConsumeStateGas(ref gas, GasCostOf.NewAccountState); [MethodImpl(MethodImplOptions.AggressiveInlining)] public static bool ConsumeLogEmission(ref EthereumGasPolicy gas, long topicCount, long dataSize) @@ -182,40 +254,81 @@ public static void OnAfterInstructionTrace(in EthereumGasPolicy gas) { } public static EthereumGasPolicy Max(in EthereumGasPolicy a, in EthereumGasPolicy b) => a.Value >= b.Value ? a : b; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static EthereumGasPolicy CreateChildFrameGas(ref EthereumGasPolicy parentGas, long childRegularGas) + { + long childStateReservoir = parentGas.StateReservoir; + parentGas.StateReservoir = 0; + + return new EthereumGasPolicy + { + Value = childRegularGas, + StateReservoir = childStateReservoir, + StateGasUsed = 0, + StateGasSpill = 0, + }; + } + public static IntrinsicGas CalculateIntrinsicGas(Transaction tx, IReleaseSpec spec) { long tokensInCallData = IGasPolicy.CalculateTokensInCallData(tx, spec); - long standard = GasCostOf.Transaction - + DataCost(tx, spec, tokensInCallData) - + CreateCost(tx, spec) - + IGasPolicy.AccessListCost(tx, spec) - + AuthorizationListCost(tx, spec); + (long authRegularCost, long authStateCost) = IGasPolicy.AuthorizationListCost(tx, spec); + + long regularGas = GasCostOf.Transaction + + DataCost(tx, spec, tokensInCallData) + + CreateCost(tx, spec) + + IGasPolicy.AccessListCost(tx, spec) + + authRegularCost; long floorCost = IGasPolicy.CalculateFloorCost(tokensInCallData, spec); - return new IntrinsicGas(FromLong(standard), FromLong(floorCost)); + long createStateCost = CreateStateCost(tx, spec); + long totalStateCost = authStateCost + createStateCost; + return spec.IsEip8037Enabled + ? new IntrinsicGas( + new EthereumGasPolicy + { + Value = regularGas, + StateReservoir = totalStateCost, + StateGasUsed = totalStateCost, + }, + FromLong(floorCost)) + : new IntrinsicGas(FromLong(regularGas), FromLong(floorCost)); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static EthereumGasPolicy CreateAvailableFromIntrinsic(long gasLimit, in EthereumGasPolicy intrinsicGas) - => new() { Value = gasLimit - intrinsicGas.Value }; + public static EthereumGasPolicy CreateAvailableFromIntrinsic(long gasLimit, in EthereumGasPolicy intrinsicGas, IReleaseSpec spec) + { + long executionGas = gasLimit - intrinsicGas.Value - intrinsicGas.StateReservoir; + long reservoir = 0; + if (spec.IsEip8037Enabled) + { + // EIP-8037: cap gas_left at TX_MAX_GAS_LIMIT - intrinsic_regular, overflow goes to reservoir + long maxGasLeft = Eip7825Constants.DefaultTxGasLimitCap - intrinsicGas.Value; + reservoir = Math.Max(0, executionGas - maxGasLeft); + executionGas -= reservoir; + } + + return new EthereumGasPolicy + { + Value = executionGas, + StateReservoir = reservoir, + StateGasUsed = intrinsicGas.StateReservoir, + StateGasSpill = 0, + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long CreateCost(Transaction tx, IReleaseSpec spec) => - tx.IsContractCreation && spec.IsEip2Enabled ? GasCostOf.TxCreate : 0; + tx.IsContractCreation && spec.IsEip2Enabled + ? (spec.IsEip8037Enabled ? GasCostOf.CreateRegular : GasCostOf.TxCreate) + : 0; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long CreateStateCost(Transaction tx, IReleaseSpec spec) => + tx.IsContractCreation && spec.IsEip8037Enabled ? GasCostOf.CreateState : 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static long DataCost(Transaction tx, IReleaseSpec spec, long tokensInCallData) => spec.GetBaseDataCost(tx) + tokensInCallData * GasCostOf.TxDataZero; - private static long AuthorizationListCost(Transaction tx, IReleaseSpec spec) - { - AuthorizationTuple[]? authList = tx.AuthorizationList; - if (authList is not null) - { - if (!spec.IsAuthorizationListEnabled) ThrowAuthorizationListNotEnabled(spec); - return authList.Length * GasCostOf.NewAccount; - } - return 0; - - [DoesNotReturn, StackTraceHidden] - static void ThrowAuthorizationListNotEnabled(IReleaseSpec spec) => - throw new InvalidDataException($"Transaction with an authorization list received within the context of {spec.Name}. EIP-7702 is not enabled."); - } } diff --git a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs index e3e783a719d..64daecbc1a6 100644 --- a/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs +++ b/src/Nethermind/Nethermind.Evm/GasPolicy/IGasPolicy.cs @@ -36,6 +36,31 @@ public interface IGasPolicy where TSelf : struct, IGasPolicy /// Remaining gas (negative values indicate out-of-gas) static abstract long GetRemainingGas(in TSelf gas); + /// + /// Gets the remaining state gas reservoir. + /// Pre-EIP-8037 policies return 0. + /// + /// The gas state to query. + /// Remaining state reservoir gas. + static virtual long GetStateReservoir(in TSelf gas) => 0; + + /// + /// Gets state gas consumed by the current execution. + /// Pre-EIP-8037 policies return 0. + /// + /// The gas state to query. + /// Consumed state gas. + static virtual long GetStateGasUsed(in TSelf gas) => 0; + + /// + /// Gets the amount of state gas that spilled into gas_left. + /// Used for block regular gas accounting (excluded from regular gas). + /// Pre-EIP-8037 policies return 0. + /// + /// The gas state to query. + /// State gas drawn from gas_left when reservoir was empty. + static virtual long GetStateGasSpill(in TSelf gas) => 0; + /// /// Consume gas for an EVM operation. /// @@ -64,6 +89,27 @@ public interface IGasPolicy where TSelf : struct, IGasPolicy /// The child gas state to merge from. static abstract void Refund(ref TSelf gas, in TSelf childGas); + /// + /// Restores all state gas from a failed child frame back to the parent's state reservoir. + /// On child revert or exceptional halt, state changes are rolled back so consumed state gas is returned. + /// Pre-EIP-8037 policies are no-ops. + /// + /// The parent gas state to restore into. + /// The child gas state to restore from. + /// The initial state reservoir that was assigned to the child frame. + static virtual void RestoreChildStateGas(ref TSelf parentGas, in TSelf childGas, long initialStateReservoir) { } + + /// + /// Adjusts parent gas state when a child was already applied but the child + /// frame should actually be treated as halted (e.g., code deposit failure). + /// Undoes the state gas portion of Refund and applies halt restoration instead. + /// Pre-EIP-8037 policies are no-ops. + /// + /// The parent gas state to adjust. + /// The child gas state that was previously merged via Refund. + /// The initial state reservoir that was assigned to the child frame. + static virtual void RevertRefundToHalt(ref TSelf parentGas, in TSelf childGas, long initialStateReservoir) { } + /// /// Mark the gas state as out of gas. /// Called when execution exhausts all gas. @@ -150,6 +196,29 @@ static abstract bool UpdateMemoryCost(ref TSelf gas, /// true if there was enough gas; otherwise, false. static abstract bool UpdateGas(ref TSelf gas, long gasCost); + /// + /// Consumes state gas for state-expansion operations. + /// Pre-EIP-8037 fallback treats state gas as regular gas. + /// + /// The gas state to update. + /// The state gas cost to deduct. + /// true if there was enough gas; otherwise, false. + static virtual bool ConsumeStateGas(ref TSelf gas, long stateGasCost) => + TSelf.UpdateGas(ref gas, stateGasCost); + + /// + /// Attempts to consume regular gas and then state gas in sequence. + /// Regular gas (e.g. keccak hash cost) is charged first to prevent + /// state gas spill-then-halt from inflating the reservoir via the error refund path. + /// + /// The gas state to update. + /// State gas component. + /// Regular gas component. + /// true if both deductions succeeded; otherwise, false. + static virtual bool TryConsumeStateAndRegularGas(ref TSelf gas, long stateGasCost, long regularGasCost) => + (regularGasCost <= 0 || TSelf.UpdateGas(ref gas, regularGasCost)) && + (stateGasCost <= 0 || TSelf.ConsumeStateGas(ref gas, stateGasCost)); + /// /// Refunds gas by adding the specified amount back to the available gas. /// @@ -158,14 +227,28 @@ static abstract bool UpdateMemoryCost(ref TSelf gas, static abstract void UpdateGasUp(ref TSelf gas, long refund); /// - /// Charges gas for SSTORE write operation (after cold/warm access cost). - /// Cost is calculated internally based on whether it's a slot creation or update. + /// Refunds state gas back to the state reservoir. + /// Pre-EIP-8037 fallback refunds into regular gas. /// /// The gas state to update. - /// True if creating a new slot (original was zero). - /// The release specification for determining reset cost. - /// True if sufficient gas available - static abstract bool ConsumeStorageWrite(ref TSelf gas, bool isSlotCreation, IReleaseSpec spec); + /// Refunded state gas amount. + /// Minimum state gas used (intrinsic state gas). + static virtual void RefundStateGas(ref TSelf gas, long amount, long stateGasFloor) => TSelf.UpdateGasUp(ref gas, amount); + + /// + /// Returns the regular gas portion of EIP-7702 code insert refunds (for end-of-tx refund cap). + /// Pre-EIP-8037: (NewAccount - PerAuthBaseCost) per refund. EIP-8037: zero (state refund only). + /// + static virtual long GetCodeInsertRegularRefund(int codeInsertRefunds, IReleaseSpec spec) => + codeInsertRefunds > 0 ? (GasCostOf.NewAccount - GasCostOf.PerAuthBaseCost) * codeInsertRefunds : 0; + + /// + /// Applies EIP-7702 code insert refunds: state refund to reservoir + returns regular refund amount. + /// Only call on success paths (state gas accounting must not be modified on error). + /// + /// Minimum state gas used (intrinsic state gas), for clamping refunds. + static virtual long ApplyCodeInsertRefunds(ref TSelf gas, int codeInsertRefunds, IReleaseSpec spec, long stateGasFloor) => + TSelf.GetCodeInsertRegularRefund(codeInsertRefunds, spec); /// /// Charges gas for CALL value transfer. @@ -216,8 +299,19 @@ static abstract bool UpdateMemoryCost(ref TSelf gas, /// /// The transaction gas limit. /// The intrinsic gas to subtract. + /// The release specification for EIP feature detection. /// Available gas with preserved tracking data. - static abstract TSelf CreateAvailableFromIntrinsic(long gasLimit, in TSelf intrinsicGas); + static abstract TSelf CreateAvailableFromIntrinsic(long gasLimit, in TSelf intrinsicGas, IReleaseSpec spec); + + /// + /// Creates a gas state for a child call/create frame. + /// Default behavior initializes child state with regular gas only. + /// EIP-8037 policies can transfer additional state-gas reservoir. + /// + /// Parent gas state (can be mutated when splitting gas dimensions). + /// Regular gas assigned to the child frame. + /// Child frame gas state. + static virtual TSelf CreateChildFrameGas(ref TSelf parentGas, long childRegularGas) => TSelf.FromLong(childRegularGas); /// /// Consumes gas for code copy operations (CODECOPY, CALLDATACOPY, EXTCODECOPY, etc.). @@ -274,6 +368,32 @@ static void ThrowInvalidDataException(IReleaseSpec spec) => throw new InvalidDataException($"Transaction with an access list received within the context of {spec.Name}. EIP-2930 is not enabled."); } + public static (long RegularCost, long StateCost) AuthorizationListCost(Transaction transaction, IReleaseSpec spec) + { + AuthorizationTuple[]? authList = transaction.AuthorizationList; + if (authList is null) + { + return (0, 0); + } + + if (!spec.IsAuthorizationListEnabled) + { + ThrowAuthorizationListNotEnabled(spec); + } + + long authCount = authList.Length; + return spec.IsEip8037Enabled + ? ( + authCount * GasCostOf.PerAuthBaseRegular, + authCount * (GasCostOf.NewAccountState + GasCostOf.PerAuthBaseState) + ) + : (authCount * GasCostOf.NewAccount, 0); + + [DoesNotReturn, StackTraceHidden] + static void ThrowAuthorizationListNotEnabled(IReleaseSpec releaseSpec) => + throw new InvalidDataException($"Transaction with an authorization list received within the context of {releaseSpec.Name}. EIP-7702 is not enabled."); + } + protected static long CalculateFloorCost(long tokensInCallData, IReleaseSpec spec) => spec.IsEip7623Enabled ? GasCostOf.Transaction + tokensInCallData * GasCostOf.TotalCostFloorPerTokenEip7623 diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs index 42edfe66f19..fd4e4fd96db 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Call.cs @@ -89,13 +89,15 @@ public struct OpStaticCall : IOpCall /// An value indicating success or the type of error encountered. /// [SkipLocalsInit] - public static EvmExceptionType InstructionCall(VirtualMachine vm, + public static EvmExceptionType InstructionCall(VirtualMachine vm, ref EvmStack stack, ref TGasPolicy gas, ref int programCounter) where TGasPolicy : struct, IGasPolicy where TOpCall : struct, IOpCall where TTracingInst : struct, IFlag + where TEip8037 : struct, IFlag + where TEip7708 : struct, IFlag { // Increment global call metrics. Metrics.IncrementCalls(); @@ -177,14 +179,20 @@ public static EvmExceptionType InstructionCall !state.AccountExists(target), + true => transferValue != 0 && state.IsDeadAccount(target), + }; + + bool newAccountOutOfGas = chargesNewAccount && !(TEip8037.IsActive switch { - if (!TGasPolicy.ConsumeNewAccountCreation(ref gas)) goto OutOfGas; - } + true => TGasPolicy.ConsumeNewAccountCreation(ref gas), + false => TGasPolicy.UpdateGas(ref gas, GasCostOf.NewAccount), + }); + + if (newAccountOutOfGas) goto OutOfGas; + // Retrieve code information for the call and schedule background analysis if needed. CodeInfo codeInfo = vm.CodeInfoRepository.GetCachedCodeInfo(codeSource, spec); @@ -262,7 +270,7 @@ public static EvmExceptionType InstructionCall(StatusCode.SuccessBytes.Span); TGasPolicy.UpdateGasUp(ref gas, gasLimitUl); - vm.AddTransferLog(caller, target, transferValue); + vm.AddTransferLog(caller, target, transferValue); return FastCall(vm, spec, in transferValue, target); } @@ -289,7 +297,7 @@ public static EvmExceptionType InstructionCall.RentFrame( - gas: TGasPolicy.FromLong(gasLimitUl), + gas: TGasPolicy.CreateChildFrameGas(ref gas, gasLimitUl), outputDestination: outputOffset.ToLong(), outputLength: outputLength.ToLong(), executionType: TOpCall.ExecutionType, diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs index 84e07541c29..bd23b925c4e 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.ControlFlow.cs @@ -202,8 +202,10 @@ public static EvmExceptionType InstructionRevert(VirtualMachine [SkipLocalsInit] - private static EvmExceptionType InstructionSelfDestruct(VirtualMachine vm, ref EvmStack stack, ref TGasPolicy gas, ref int programCounter) + private static EvmExceptionType InstructionSelfDestruct(VirtualMachine vm, ref EvmStack stack, ref TGasPolicy gas, ref int programCounter) where TGasPolicy : struct, IGasPolicy + where TEip8037 : struct, IFlag + where TEip7708 : struct, IFlag { // Increment metrics for self-destruct operations. Metrics.IncrementSelfDestructs(); @@ -235,7 +237,6 @@ private static EvmExceptionType InstructionSelfDestruct(VirtualMachi Address executingAccount = vmState.Env.ExecutingAccount; bool createInSameTx = vmState.AccessTracker.CreateList.Contains(executingAccount); bool selfdestructOnlyOnSameTx = spec.SelfdestructOnlyOnSameTransaction; - bool clearEmpty = spec.ClearEmptyAccountWhenTouched; // Mark the executing account for destruction if allowed. if (!selfdestructOnlyOnSameTx || createInSameTx) vmState.AccessTracker.ToBeDestroyed(executingAccount); @@ -243,32 +244,24 @@ private static EvmExceptionType InstructionSelfDestruct(VirtualMachi // Retrieve the current balance for transfer. UInt256 result = state.GetBalance(executingAccount); - if (executingAccount == inheritor) - { - vm.AddSelfDestructLog(executingAccount, result); - } - else - { - vm.AddTransferLog(executingAccount, inheritor, result); - } - if (vm.TxTracer.IsTracingActions) vm.TxTracer.ReportSelfDestruct(executingAccount, result, inheritor); - // For certain specs, charge gas if transferring to a dead account. - if (clearEmpty && !result.IsZero && state.IsDeadAccount(inheritor)) + // Charge gas if transferring to a dead or non-existent account. + bool inheritorAccountExists = state.AccountExists(inheritor); + bool chargesNewAccount = spec.ClearEmptyAccountWhenTouched switch { - if (!TGasPolicy.UpdateGas(ref gas, GasCostOf.NewAccount)) - goto OutOfGas; - } + true => !result.IsZero && state.IsDeadAccount(inheritor), + false => !inheritorAccountExists && spec.UseShanghaiDDosProtection, + }; - // If account creation rules apply, ensure gas is charged for new accounts. - bool inheritorAccountExists = state.AccountExists(inheritor); - if (!clearEmpty && !inheritorAccountExists && spec.UseShanghaiDDosProtection) + bool outOfGas = chargesNewAccount && !(TEip8037.IsActive switch { - if (!TGasPolicy.UpdateGas(ref gas, GasCostOf.NewAccount)) - goto OutOfGas; - } + true => TGasPolicy.ConsumeNewAccountCreation(ref gas), + false => TGasPolicy.UpdateGas(ref gas, GasCostOf.NewAccount), + }); + + if (outOfGas) goto OutOfGas; // Create or update the inheritor account with the transferred balance. if (!inheritorAccountExists) @@ -281,8 +274,11 @@ private static EvmExceptionType InstructionSelfDestruct(VirtualMachi } // Special handling when SELFDESTRUCT is limited to the same transaction. + // No ETH moves and no log is emitted for this no-op case. if (selfdestructOnlyOnSameTx && !createInSameTx && inheritor.Equals(executingAccount)) - goto Stop; // Avoid burning ETH if contract is not destroyed per EIP clarification + goto Stop; + + vm.AddSelfDestructLog(executingAccount, inheritor, result); // Subtract the balance from the executing account. state.SubtractFromBalance(executingAccount, result, spec); diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs index 367d36889af..0945564dc81 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Create.cs @@ -68,7 +68,7 @@ public struct OpCreate2 : IOpCreate /// Reference to the program counter. /// An indicating success or the type of exception encountered. [SkipLocalsInit] - public static EvmExceptionType InstructionCreate( + public static EvmExceptionType InstructionCreate( VirtualMachine vm, ref EvmStack stack, ref TGasPolicy gas, @@ -76,6 +76,7 @@ public static EvmExceptionType InstructionCreate where TOpCreate : struct, IOpCreate where TTracingInst : struct, IFlag + where TEip8037 : struct, IFlag { // Increment metrics counter for contract creation operations. Metrics.IncrementCreates(); @@ -109,22 +110,32 @@ public static EvmExceptionType InstructionCreate spec.MaxInitCodeSize) + { + // EIP-8037: charge state gas before halting so StateGasSpill is recorded + // for correct block gas accounting (block_regular excludes state gas). + if (TEip8037.IsActive) + TGasPolicy.ConsumeStateGas(ref gas, GasCostOf.CreateState); goto OutOfGas; + } } bool outOfGas = false; - // Calculate the gas cost for the creation, including fixed cost and per-word cost for init code. - // Also include an extra cost for CREATE2 if applicable. - long gasCost = GasCostOf.Create + - (isEip3860 ? GasCostOf.InitCodeWord * EvmCalculations.Div32Ceiling(in initCodeLength, out outOfGas) : 0) + - (typeof(TOpCreate) == typeof(OpCreate2) - ? GasCostOf.Sha3Word * EvmCalculations.Div32Ceiling(in initCodeLength, out outOfGas) - : 0); - - // Check gas sufficiency: if outOfGas flag was set during gas division or if gas update fails. - if (outOfGas || !TGasPolicy.UpdateGas(ref gas, gasCost)) + long initCodeWords = EvmCalculations.Div32Ceiling(in initCodeLength, out outOfGas); + if (outOfGas) goto OutOfGas; + long initCodeWordCost = spec.IsEip3860Enabled ? GasCostOf.InitCodeWord * initCodeWords : 0; + long create2HashCost = typeof(TOpCreate) == typeof(OpCreate2) ? GasCostOf.Sha3Word * initCodeWords : 0; + long extraCost = initCodeWordCost + create2HashCost; + + bool createOutOfGas = TEip8037.IsActive switch + { + true => !TGasPolicy.UpdateGas(ref gas, GasCostOf.CreateRegular + extraCost) || !TGasPolicy.ConsumeStateGas(ref gas, GasCostOf.CreateState), + false => !TGasPolicy.UpdateGas(ref gas, GasCostOf.Create + extraCost), + }; + + if (createOutOfGas) goto OutOfGas; + // Update memory gas cost based on the required memory expansion for the init code. if (!TGasPolicy.UpdateMemoryCost(ref gas, in memoryPositionOfInitCode, in initCodeLength, vm.VmState)) goto OutOfGas; @@ -207,6 +218,7 @@ public static EvmExceptionType InstructionCreate(); @@ -238,7 +250,7 @@ public static EvmExceptionType InstructionCreate.RentFrame( - gas: TGasPolicy.FromLong(callGas), + gas: TGasPolicy.CreateChildFrameGas(ref gas, callGas), outputDestination: 0, outputLength: 0, executionType: TOpCreate.ExecutionType, diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Eof.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Eof.cs index 6fcdd3a58e2..68e23a59f61 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Eof.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Eof.cs @@ -751,7 +751,7 @@ public static EvmExceptionType InstructionEofCreate(Vi inputData: in callData); vm.ReturnData = VmState.RentFrame( - gas: TGasPolicy.FromLong(callGas), + gas: TGasPolicy.CreateChildFrameGas(ref gas, callGas), outputDestination: 0, outputLength: 0, executionType: currentContext, @@ -866,10 +866,11 @@ public static EvmExceptionType InstructionReturnDataLoad [SkipLocalsInit] - public static EvmExceptionType InstructionEofCall(VirtualMachine vm, ref EvmStack stack, ref TGasPolicy gas, ref int programCounter) + public static EvmExceptionType InstructionEofCall(VirtualMachine vm, ref EvmStack stack, ref TGasPolicy gas, ref int programCounter) where TGasPolicy : struct, IGasPolicy where TOpEofCall : struct, IOpEofCall where TTracingInst : struct, IFlag + where TEip8037 : struct, IFlag { Metrics.IncrementCalls(); @@ -946,7 +947,11 @@ public static EvmExceptionType InstructionEofCall !TGasPolicy.ConsumeNewAccountCreation(ref gas), + false => !TGasPolicy.UpdateGas(ref gas, GasCostOf.NewAccount), + }) goto OutOfGas; } @@ -1014,7 +1019,7 @@ public static EvmExceptionType InstructionEofCall.RentFrame( - gas: TGasPolicy.FromLong(callGas), + gas: TGasPolicy.CreateChildFrameGas(ref gas, callGas), outputDestination: 0, outputLength: 0, executionType: TOpEofCall.ExecutionType, diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Stack.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Stack.cs index f34cf7b10aa..586960334c1 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Stack.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Stack.cs @@ -583,11 +583,9 @@ public static EvmExceptionType InstructionDupN(Virtual TGasPolicy.Consume(ref gas, GasCostOf.VeryLow); if (!TryDecodeSingle(vm, ref programCounter, out int depth)) - goto BadInstruction; + return StopOrBadInstruction(vm, programCounter); return stack.Dup(depth); - BadInstruction: - return EvmExceptionType.BadInstruction; } /// @@ -602,11 +600,9 @@ public static EvmExceptionType InstructionSwapN(Virtua TGasPolicy.Consume(ref gas, GasCostOf.VeryLow); if (!TryDecodeSingle(vm, ref programCounter, out int depth)) - goto BadInstruction; + return StopOrBadInstruction(vm, programCounter); return stack.Swap(depth + 1); - BadInstruction: - return EvmExceptionType.BadInstruction; } /// @@ -621,23 +617,34 @@ public static EvmExceptionType InstructionExchange(Vir TGasPolicy.Consume(ref gas, GasCostOf.VeryLow); if (!TryDecodePair(vm, ref programCounter, out int n, out int m)) - goto BadInstruction; + return StopOrBadInstruction(vm, programCounter); if (!stack.Exchange(n, m)) - goto StackUnderflow; + return EvmExceptionType.StackUnderflow; return EvmExceptionType.None; - BadInstruction: - return EvmExceptionType.BadInstruction; - StackUnderflow: - return EvmExceptionType.StackUnderflow; } + /// + /// Returns Stop if the program counter is at or past end of code, otherwise BadInstruction. + /// Used by EIP-8024 to distinguish end-of-code (graceful stop) from disallowed immediate. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static EvmExceptionType StopOrBadInstruction(VirtualMachine vm, int programCounter) + where TGasPolicy : struct, IGasPolicy + => programCounter >= vm.VmState.Env.CodeInfo.CodeSpan.Length + ? EvmExceptionType.Stop + : EvmExceptionType.BadInstruction; + /// /// Reads and decodes an immediate for EIP-8024 DUPN/SWAPN instructions. + /// + /// /// Handles bounds checking, reading the immediate, and advancing the program counter. + /// Branchless formula: n = (x + 145) % 256. + /// Valid range: 0-90 (n=145-235) and 128-255 (n=17-144). /// Disallowed range: 0x5b-0x7f (91-127) to avoid JUMPDEST/PUSH patterns. - /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryDecodeSingle(VirtualMachine vm, ref int programCounter, out int depth) where TGasPolicy : struct, IGasPolicy @@ -650,8 +657,7 @@ private static bool TryDecodeSingle(VirtualMachine vm, r } byte imm = code[programCounter]; - int mask = (90 - imm) >> 31; - depth = imm + 17 + (mask & -37); + depth = (imm + 145) & 0xFF; if ((uint)(imm - 0x5B) <= 0x24) return false; @@ -662,10 +668,13 @@ private static bool TryDecodeSingle(VirtualMachine vm, r /// /// Reads and decodes an immediate for EIP-8024 EXCHANGE instruction. + /// + /// /// Handles bounds checking, reading the immediate, and advancing the program counter. - /// Disallowed range: 0x50-0x7f (80-127) to avoid PUSH opcode patterns. + /// Branchless formula: k = x ^ 143 (XOR with 0x8F). + /// Valid range: 0-81 (k mapped via XOR) and 128-255. Invalid range: 82-127. /// Returns stack indices ready for direct use with stack.Exchange. - /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryDecodePair(VirtualMachine vm, ref int programCounter, out int n, out int m) where TGasPolicy : struct, IGasPolicy @@ -679,8 +688,7 @@ private static bool TryDecodePair(VirtualMachine vm, ref byte imm = code[programCounter]; - // k = imm if imm <= 79, imm - 48 if imm >= 128 - int k = imm - (~((imm - 0x80) >> 31) & 48); + int k = imm ^ 0x8F; int q = k >> 4; int r = k & 0x0F; @@ -692,7 +700,7 @@ private static bool TryDecodePair(VirtualMachine vm, ref n = ((q & mask) | (r & ~mask)) + 2; m = (((r + 1) & mask) | ((29 - q) & ~mask)) + 1; - if ((uint)(imm - 0x50) <= 0x2F) + if ((uint)(imm - 0x52) <= 0x2D) return false; programCounter++; diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Storage.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Storage.cs index f381f65ee45..cb6e5107640 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Storage.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.Storage.cs @@ -439,10 +439,11 @@ internal static EvmExceptionType InstructionSStoreUnmeteredThe program counter. /// An indicating the outcome. [SkipLocalsInit] - internal static EvmExceptionType InstructionSStoreMetered(VirtualMachine vm, ref EvmStack stack, ref TGasPolicy gas, ref int programCounter) + internal static EvmExceptionType InstructionSStoreMetered(VirtualMachine vm, ref EvmStack stack, ref TGasPolicy gas, ref int programCounter) where TGasPolicy : struct, IGasPolicy where TTracingInst : struct, IFlag where TUseNetGasStipendFix : struct, IFlag + where TEip8037 : struct, IFlag { // Increment the SSTORE opcode metric. Metrics.IncrementSStoreOpcode(); @@ -504,12 +505,17 @@ internal static EvmExceptionType InstructionSStoreMetered !TGasPolicy.ConsumeStateGas(ref gas, GasCostOf.SSetState) || !TGasPolicy.UpdateGas(ref gas, GasCostOf.SSetRegular), + false => !TGasPolicy.UpdateGas(ref gas, GasCostOf.SSet), + }; + + if (ssetOutOfGas) goto OutOfGas; } else { - if (!TGasPolicy.ConsumeStorageWrite(ref gas, isSlotCreation: false, spec)) + if (!TGasPolicy.UpdateGas(ref gas, spec.GasCosts.SStoreResetCost)) goto OutOfGas; if (newIsZero) @@ -548,9 +554,7 @@ internal static EvmExceptionType InstructionSStoreMetered(originalIsZero); vmState.Refund += refundFromReversal; if (vm.TxTracer.IsTracingRefunds) diff --git a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.cs b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.cs index 621cd58bf95..493961309ec 100644 --- a/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.cs +++ b/src/Nethermind/Nethermind.Evm/Instructions/EvmInstructions.cs @@ -151,8 +151,12 @@ internal static unsafe partial class EvmInstructions lookup[(int)Instruction.SLOAD] = &InstructionSLoad; lookup[(int)Instruction.SSTORE] = spec.UseNetGasMetering ? (spec.UseNetGasMeteringWithAStipendFix ? - &InstructionSStoreMetered : - &InstructionSStoreMetered + (spec.IsEip8037Enabled + ? &InstructionSStoreMetered + : &InstructionSStoreMetered) : + (spec.IsEip8037Enabled + ? &InstructionSStoreMetered + : &InstructionSStoreMetered) ) : &InstructionSStoreUnmetered; @@ -288,37 +292,70 @@ internal static unsafe partial class EvmInstructions } // Contract creation and call opcodes. - lookup[(int)Instruction.CREATE] = &InstructionCreate; - lookup[(int)Instruction.CALL] = &InstructionCall; - lookup[(int)Instruction.CALLCODE] = &InstructionCall; + lookup[(int)Instruction.CREATE] = spec.IsEip8037Enabled + ? &InstructionCreate + : &InstructionCreate; + lookup[(int)Instruction.CALL] = (spec.IsEip8037Enabled, spec.IsEip7708Enabled) switch + { + (true, true) => &InstructionCall, + (true, false) => &InstructionCall, + (false, true) => &InstructionCall, + (false, false) => &InstructionCall, + }; + lookup[(int)Instruction.CALLCODE] = (spec.IsEip8037Enabled, spec.IsEip7708Enabled) switch + { + (true, true) => &InstructionCall, + (true, false) => &InstructionCall, + (false, true) => &InstructionCall, + (false, false) => &InstructionCall, + }; lookup[(int)Instruction.RETURN] = &InstructionReturn; if (spec.DelegateCallEnabled) { - lookup[(int)Instruction.DELEGATECALL] = &InstructionCall; + lookup[(int)Instruction.DELEGATECALL] = (spec.IsEip8037Enabled, spec.IsEip7708Enabled) switch + { + (true, true) => &InstructionCall, + (true, false) => &InstructionCall, + (false, true) => &InstructionCall, + (false, false) => &InstructionCall, + }; } if (spec.Create2OpcodeEnabled) { - lookup[(int)Instruction.CREATE2] = &InstructionCreate; + lookup[(int)Instruction.CREATE2] = spec.IsEip8037Enabled + ? &InstructionCreate + : &InstructionCreate; } lookup[(int)Instruction.RETURNDATALOAD] = &InstructionReturnDataLoad; if (spec.StaticCallEnabled) { - lookup[(int)Instruction.STATICCALL] = &InstructionCall; + lookup[(int)Instruction.STATICCALL] = (spec.IsEip8037Enabled, spec.IsEip7708Enabled) switch + { + (true, true) => &InstructionCall, + (true, false) => &InstructionCall, + (false, true) => &InstructionCall, + (false, false) => &InstructionCall, + }; } // Extended call opcodes in EO mode. if (spec.IsEofEnabled) { - lookup[(int)Instruction.EXTCALL] = &InstructionEofCall; + lookup[(int)Instruction.EXTCALL] = spec.IsEip8037Enabled + ? &InstructionEofCall + : &InstructionEofCall; if (spec.DelegateCallEnabled) { - lookup[(int)Instruction.EXTDELEGATECALL] = - &InstructionEofCall; + lookup[(int)Instruction.EXTDELEGATECALL] = spec.IsEip8037Enabled + ? &InstructionEofCall + : &InstructionEofCall; } if (spec.StaticCallEnabled) { - lookup[(int)Instruction.EXTSTATICCALL] = &InstructionEofCall; + lookup[(int)Instruction.EXTSTATICCALL] = spec.IsEip8037Enabled + ? &InstructionEofCall + : &InstructionEofCall; } } @@ -329,7 +366,13 @@ internal static unsafe partial class EvmInstructions // Final opcodes. lookup[(int)Instruction.INVALID] = &InstructionInvalid; - lookup[(int)Instruction.SELFDESTRUCT] = &InstructionSelfDestruct; + lookup[(int)Instruction.SELFDESTRUCT] = (spec.IsEip8037Enabled, spec.IsEip7708Enabled) switch + { + (true, true) => &InstructionSelfDestruct, + (true, false) => &InstructionSelfDestruct, + (false, true) => &InstructionSelfDestruct, + (false, false) => &InstructionSelfDestruct, + }; return lookup; } diff --git a/src/Nethermind/Nethermind.Evm/IntrinsicGasCalculator.cs b/src/Nethermind/Nethermind.Evm/IntrinsicGasCalculator.cs index 438b52c3b1c..0d48366fd1e 100644 --- a/src/Nethermind/Nethermind.Evm/IntrinsicGasCalculator.cs +++ b/src/Nethermind/Nethermind.Evm/IntrinsicGasCalculator.cs @@ -16,7 +16,7 @@ public readonly record struct EthereumIntrinsicGas(long Standard, long FloorGas) public long MinimalGas { get; } = Math.Max(Standard, FloorGas); public static explicit operator long(EthereumIntrinsicGas gas) => gas.MinimalGas; public static implicit operator EthereumIntrinsicGas(IntrinsicGas gas) => - new(gas.Standard.Value, gas.FloorGas.Value); + new(gas.Standard.Value + gas.Standard.StateReservoir, gas.FloorGas.Value); } public static class IntrinsicGasCalculator diff --git a/src/Nethermind/Nethermind.Evm/State/WrappedWorldState.cs b/src/Nethermind/Nethermind.Evm/State/WrappedWorldState.cs index 8787b4f6e5d..f4937d38a80 100644 --- a/src/Nethermind/Nethermind.Evm/State/WrappedWorldState.cs +++ b/src/Nethermind/Nethermind.Evm/State/WrappedWorldState.cs @@ -29,9 +29,6 @@ public virtual void AddToBalance(Address address, in UInt256 balanceChange, IRel public virtual void AddToBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) => _innerWorldState.AddToBalance(address, balanceChange, spec, out oldBalance); - public virtual bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balanceChange, IReleaseSpec spec) - => _innerWorldState.AddToBalanceAndCreateIfNotExists(address, balanceChange, spec); - public virtual bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) => _innerWorldState.AddToBalanceAndCreateIfNotExists(address, balanceChange, spec, out oldBalance); @@ -89,9 +86,6 @@ public ReadOnlySpan GetTransientState(in StorageCell storageCell) public bool HasStateForBlock(BlockHeader? baseBlock) => _innerWorldState.HasStateForBlock(baseBlock); - public virtual void IncrementNonce(Address address, UInt256 delta) - => _innerWorldState.IncrementNonce(address, delta); - public virtual void IncrementNonce(Address address, UInt256 delta, out UInt256 oldNonce) => _innerWorldState.IncrementNonce(address, delta, out oldNonce); @@ -125,9 +119,6 @@ public virtual void SetNonce(Address address, in UInt256 nonce) public void SetTransientState(in StorageCell storageCell, byte[] newValue) => _innerWorldState.SetTransientState(storageCell, newValue); - public virtual void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec) - => _innerWorldState.SubtractFromBalance(address, balanceChange, spec); - public virtual void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) => _innerWorldState.SubtractFromBalance(address, balanceChange, spec, out oldBalance); diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/GasConsumed.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/GasConsumed.cs index 0e9e1654f9d..ca5a7ca3eb5 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/GasConsumed.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/GasConsumed.cs @@ -8,11 +8,12 @@ namespace Nethermind.Evm.TransactionProcessing; /// /// Gas after refunds (what user pays). /// Gas used for EVM operations. -/// EIP-7778: Gas before refunds (for block gas accounting). When 0, use SpentGas. -public readonly record struct GasConsumed(long SpentGas, long OperationGas, long BlockGas = 0) +/// EIP-7778: Regular gas for block accounting (pre-refund). When 0, use SpentGas. +/// EIP-8037: State gas for block accounting. Block gasUsed = max(sum_regular, sum_state). +public readonly record struct GasConsumed(long SpentGas, long OperationGas, long BlockGas = 0, long BlockStateGas = 0) { /// - /// Gets the effective gas for block accounting. When EIP-7778 is enabled, + /// Gets the effective regular gas for block accounting. When EIP-7778 is enabled, /// this returns BlockGas (pre-refund), otherwise returns SpentGas. /// public long EffectiveBlockGas => BlockGas > 0 ? BlockGas : SpentGas; diff --git a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs index 0dcfeffbd26..aa9e6eb25a3 100644 --- a/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs @@ -220,7 +220,7 @@ protected virtual TransactionResult Execute(Transaction tx, ITxTracer tracer, Ex int delegationRefunds = (!spec.IsEip7702Enabled || !tx.HasAuthorizationList) ? 0 : ProcessDelegations(tx, spec, accessTracker); - if (!(result = CalculateAvailableGas(tx, in intrinsicGas, out TGasPolicy gasAvailable))) return result; + if (!(result = CalculateAvailableGas(tx, spec, in intrinsicGas, out TGasPolicy gasAvailable))) return result; if (!(result = BuildExecutionEnvironment(tx, spec, _codeInfoRepository, accessTracker, out ExecutionEnvironment e))) return result; using ExecutionEnvironment env = e; @@ -229,6 +229,22 @@ protected virtual TransactionResult Execute(Transaction tx, ITxTracer tracer, Ex ExecuteEvmCall(tx, header, spec, tracer, opts, delegationRefunds, intrinsicGas, accessTracker, gasAvailable, env, out substate, out spentGas); PayFees(tx, header, spec, tracer, in substate, spentGas.SpentGas, premiumPerGas, blobBaseFee, statusCode); + + // EIP-8037+EIP-7708: process destroy list after PayFees so burn logs include + // the priority fee in the destroyed account's balance. + if (spec.IsEip8037Enabled && spec.IsEip7708Enabled && statusCode == StatusCode.Success) + { + foreach (Address toBeDestroyed in substate.DestroyList) + { + UInt256 balance = WorldState.GetBalance(toBeDestroyed); + if (!balance.IsZero) + substate.Logs.Add(TransferLog.CreateBurn(toBeDestroyed, balance)); + + WorldState.ClearStorage(toBeDestroyed); + WorldState.DeleteAccount(toBeDestroyed); + } + } + tx.BlockGasUsed = spentGas.EffectiveBlockGas; //only main thread updates transaction @@ -290,9 +306,9 @@ protected virtual TransactionResult Execute(Transaction tx, ITxTracer tracer, Ex : TransactionResult.Ok; } - protected virtual TransactionResult CalculateAvailableGas(Transaction tx, in IntrinsicGas intrinsicGas, out TGasPolicy gasAvailable) + protected virtual TransactionResult CalculateAvailableGas(Transaction tx, IReleaseSpec spec, in IntrinsicGas intrinsicGas, out TGasPolicy gasAvailable) { - gasAvailable = TGasPolicy.CreateAvailableFromIntrinsic(tx.GasLimit, intrinsicGas.Standard); + gasAvailable = TGasPolicy.CreateAvailableFromIntrinsic(tx.GasLimit, intrinsicGas.Standard, spec); return TransactionResult.Ok; } @@ -310,6 +326,11 @@ 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}"); + + if (_balBuilder is not null && _balBuilder.TracingEnabled && IncludeAccountRead(authorizationResult)) + { + _balBuilder.AddAccountRead(authority); + } } else { @@ -330,6 +351,9 @@ private int ProcessDelegations(Transaction tx, IReleaseSpec spec, in StackAccess return refunds; } + private static bool IncludeAccountRead(in AuthorizationTupleResult res) + => res is AuthorizationTupleResult.IncorrectNonce or AuthorizationTupleResult.InvalidAsCodeDeployed; + private enum AuthorizationTupleResult { Valid, @@ -449,7 +473,12 @@ protected virtual TransactionResult ValidateStatic( return TransactionResult.TransactionSizeOverMaxInitCodeSize; } - return ValidateGas(tx, header, TGasPolicy.GetRemainingGas(intrinsicGas.MinimalGas)); + TGasPolicy standard = intrinsicGas.Standard; + TGasPolicy minimal = intrinsicGas.MinimalGas; + long minGasRequired = spec.IsEip8037Enabled + ? Math.Max(TGasPolicy.GetRemainingGas(in standard) + TGasPolicy.GetStateReservoir(in standard), TGasPolicy.GetRemainingGas(in minimal)) + : TGasPolicy.GetRemainingGas(in minimal); + return ValidateGas(tx, header, minGasRequired); } protected virtual TransactionResult ValidateGas(Transaction tx, BlockHeader header, long minGasRequired) @@ -764,28 +793,33 @@ private int ExecuteEvmCall( } } - bool eip7708Enabled = spec.IsEip7708Enabled; - bool tracingRefunds = tracer.IsTracingRefunds; - foreach (Address toBeDestroyed in substate.DestroyList) + // EIP-8037: defer destroy list processing to after PayFees so that + // burn logs include the priority fee in the balance. + bool deferFinalization = spec.IsEip7708Enabled && spec.IsEip8037Enabled; + if (!deferFinalization) { - if (Logger.IsTrace) Logger.Trace($"Destroying account {toBeDestroyed}"); - - if (eip7708Enabled) + bool eip7708Enabled = spec.IsEip7708Enabled; + bool tracingRefunds = tracer.IsTracingRefunds; + foreach (Address toBeDestroyed in substate.DestroyList) { - UInt256 balance = WorldState.GetBalance(toBeDestroyed); - if (!balance.IsZero) + if (Logger.IsTrace) Logger.Trace($"Destroying account {toBeDestroyed}"); + + if (eip7708Enabled) { - substate.Logs.Add(TransferLog.CreateSelfDestruct(toBeDestroyed, balance)); + UInt256 balance = WorldState.GetBalance(toBeDestroyed); + if (!balance.IsZero) + { + substate.Logs.Add(TransferLog.CreateSelfDestruct(toBeDestroyed, balance)); + } } - } - - WorldState.ClearStorage(toBeDestroyed); - WorldState.DeleteAccount(toBeDestroyed); + WorldState.ClearStorage(toBeDestroyed); + WorldState.DeleteAccount(toBeDestroyed); - if (tracingRefunds) - { - tracer.ReportRefund(spec.GasCosts.DestroyRefund); + if (tracingRefunds) + { + tracer.ReportRefund(spec.GasCosts.DestroyRefund); + } } } @@ -793,12 +827,17 @@ private int ExecuteEvmCall( } } - gasConsumed = Refund(tx, header, spec, opts, in substate, gasAvailable, VirtualMachine.TxExecutionContext.GasPrice, delegationRefunds, gas.FloorGas); + gasConsumed = Refund(tx, header, spec, opts, in substate, gasAvailable, VirtualMachine.TxExecutionContext.GasPrice, delegationRefunds, gas.FloorGas, gas.Standard); goto Complete; FailContractCreate: if (Logger.IsTrace) Logger.Trace("Restoring state from before transaction"); WorldState.Restore(snapshot); gasConsumed = RefundOnFailContractCreation(tx, header, spec, opts); + if (gasConsumed.SpentGas < tx.GasLimit) + { + UInt256 refundAmount = (ulong)(tx.GasLimit - gasConsumed.SpentGas) * VirtualMachine.TxExecutionContext.GasPrice; + PayRefund(tx, refundAmount, spec); + } Complete: if (!opts.HasFlag(ExecutionOptions.SkipValidation)) { @@ -809,35 +848,34 @@ private int ExecuteEvmCall( } - protected virtual GasConsumed RefundOnFailContractCreation(Transaction tx, BlockHeader header, IReleaseSpec spec, ExecutionOptions opts) => tx.GasLimit; + protected virtual GasConsumed RefundOnFailContractCreation(Transaction tx, BlockHeader header, IReleaseSpec spec, ExecutionOptions opts) + { + if (!spec.IsEip8037Enabled) + return tx.GasLimit; + + // EIP-8037: compute intrinsic state cost and initial reservoir. + // All regular gas is consumed as penalty, but the unused state reservoir is refunded. + IntrinsicGas intrinsicGas = CalculateIntrinsicGas(tx, spec); + long intrinsicState = TGasPolicy.GetStateReservoir(intrinsicGas.Standard); + long initialReservoir = Math.Max(0, tx.GasLimit - intrinsicState - Eip7825Constants.DefaultTxGasLimitCap); + + long spentGas = tx.GasLimit - initialReservoir; + long blockGas = spentGas - intrinsicState; + long blockStateGas = intrinsicState; + + return new GasConsumed(spentGas, spentGas, blockGas, blockStateGas); + } protected virtual bool DeployLegacyContract(IReleaseSpec spec, Address codeOwner, in TransactionSubstate substate, in StackAccessTracker accessedItems, ref TGasPolicy unspentGas) { - long codeDepositGasCost = CodeDepositHandler.CalculateCost(spec, substate.Output.Bytes.Length); - if (TGasPolicy.GetRemainingGas(unspentGas) < codeDepositGasCost && spec.ChargeForTopLevelCreate) - { + if (!CodeDepositHandler.CalculateCost(spec, substate.Output.Bytes.Length, out long regularDepositCost, out long stateDepositCost)) return false; - } if (CodeDepositHandler.CodeIsInvalid(spec, substate.Output.Bytes, 0)) - { return false; - } - - if (TGasPolicy.GetRemainingGas(unspentGas) >= codeDepositGasCost) - { - // Copy the bytes so it's not live memory that will be used in another tx - byte[] code = substate.Output.Bytes.ToArray(); - _codeInfoRepository.InsertCode(code, codeOwner, spec); - if (code.Length > CodeSizeConstants.MaxCodeSizeEip170) - { - accessedItems.WarmUpLargeContract(codeOwner); - } - - TGasPolicy.ConsumeCodeDeposit(ref unspentGas, codeDepositGasCost); - } - return true; + // Copy the bytes so it's not live memory that will be used in another tx. + return TryChargeCodeDeposit(spec, codeOwner, in accessedItems, ref unspentGas, regularDepositCost, stateDepositCost, substate.Output.Bytes.ToArray()); } private bool DeployEofContract(IReleaseSpec spec, Address codeOwner, in TransactionSubstate substate, in StackAccessTracker accessedItems, ref TGasPolicy unspentGas) @@ -846,17 +884,14 @@ private bool DeployEofContract(IReleaseSpec spec, Address codeOwner, in Transact ReadOnlySpan auxExtraData = substate.Output.Bytes.Span; EofCodeInfo deployCodeInfo = (EofCodeInfo)substate.Output.DeployCode; - long codeDepositGasCost = CodeDepositHandler.CalculateCost(spec, deployCodeInfo.Code.Length + auxExtraData.Length); - if (TGasPolicy.GetRemainingGas(unspentGas) < codeDepositGasCost && spec.ChargeForTopLevelCreate) - { + if (!CodeDepositHandler.CalculateCost(spec, deployCodeInfo.Code.Length + auxExtraData.Length, out long regularDepositCost, out long stateDepositCost)) return false; - } + int codeLength = deployCodeInfo.Code.Length + auxExtraData.Length; // 3 - if updated deploy container size exceeds MAX_CODE_SIZE instruction exceptionally aborts if (codeLength > spec.MaxCodeSize) - { return false; - } + // 2 - concatenate data section with (aux_data_offset, aux_data_offset + aux_data_size) memory segment and update data size in the header byte[] bytecodeResult = new byte[codeLength]; // 2 - 1 - 1 - copy old container @@ -876,16 +911,35 @@ private bool DeployEofContract(IReleaseSpec spec, Address codeOwner, in Transact bytecodeResult[dataSubHeaderSectionStart + 1] = (byte)(dataSize >> 8); bytecodeResult[dataSubHeaderSectionStart + 2] = (byte)(dataSize & 0xFF); - if (TGasPolicy.GetRemainingGas(unspentGas) >= codeDepositGasCost) + // 4 - set state[new_address].code to the updated deploy container + return TryChargeCodeDeposit(spec, codeOwner, in accessedItems, ref unspentGas, regularDepositCost, stateDepositCost, bytecodeResult); + } + + private bool TryChargeCodeDeposit( + IReleaseSpec spec, + Address codeOwner, + in StackAccessTracker accessedItems, + ref TGasPolicy unspentGas, + long regularDepositCost, + long stateDepositCost, + byte[] code) + { + bool hasEnoughRegularGas = TGasPolicy.GetRemainingGas(in unspentGas) >= regularDepositCost; + bool hasEnoughStateGas = TGasPolicy.GetRemainingGas(in unspentGas) + TGasPolicy.GetStateReservoir(in unspentGas) >= stateDepositCost; + if ((!hasEnoughRegularGas || !hasEnoughStateGas) && spec.ChargeForTopLevelCreate) + return false; + + if (hasEnoughRegularGas && hasEnoughStateGas) { - // 4 - set state[new_address].code to the updated deploy container - // push new_address onto the stack (already done before the ifs) - _codeInfoRepository.InsertCode(bytecodeResult, codeOwner, spec); - if (bytecodeResult.Length > CodeSizeConstants.MaxCodeSizeEip170) - { + TGasPolicy gasAfterCodeDeposit = unspentGas; + if (!TGasPolicy.TryConsumeStateAndRegularGas(ref gasAfterCodeDeposit, stateDepositCost, regularDepositCost)) + return false; + + _codeInfoRepository.InsertCode(code, codeOwner, spec); + if (code.Length > CodeSizeConstants.MaxCodeSizeEip170) accessedItems.WarmUpLargeContract(codeOwner); - } - TGasPolicy.ConsumeCodeDeposit(ref unspentGas, codeDepositGasCost); + + unspentGas = gasAfterCodeDeposit; } return true; @@ -901,8 +955,9 @@ protected virtual void PayFees(Transaction tx, BlockHeader header, IReleaseSpec UInt256 fees = premiumPerGas * (ulong)spentGas; // n.b. destroyed accounts already set to zero balance + // EIP-8037: always pay coinbase — deferred finalization will burn the balance bool gasBeneficiaryNotDestroyed = !substate.DestroyList.Contains(header.GasBeneficiary); - if (statusCode == StatusCode.Failure || gasBeneficiaryNotDestroyed) + if (statusCode == StatusCode.Failure || gasBeneficiaryNotDestroyed || spec.IsEip8037Enabled) { WorldState.AddToBalanceAndCreateIfNotExists(header.GasBeneficiary!, fees, spec); } @@ -945,30 +1000,37 @@ 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, TGasPolicy floorGas) + in TransactionSubstate substate, in TGasPolicy unspentGas, in UInt256 gasPrice, int codeInsertRefunds, TGasPolicy floorGas, in TGasPolicy intrinsicGasStandard) { + TGasPolicy gasAfterExecution = unspentGas; long spentGas = tx.GasLimit; long actualRefund = 0; - long codeInsertRefund = (GasCostOf.NewAccount - GasCostOf.PerAuthBaseCost) * codeInsertRefunds; + long stateGasFloor = TGasPolicy.GetStateReservoir(in intrinsicGasStandard); if (!substate.IsError) { - spentGas -= TGasPolicy.GetRemainingGas(unspentGas); + long codeInsertRegularRefund = TGasPolicy.ApplyCodeInsertRefunds(ref gasAfterExecution, codeInsertRefunds, spec, stateGasFloor); + spentGas -= TGasPolicy.GetRemainingGas(in gasAfterExecution) + TGasPolicy.GetStateReservoir(in gasAfterExecution); - long totalToRefund = codeInsertRefund; + long totalToRefund = codeInsertRegularRefund; if (!substate.ShouldRevert) totalToRefund += substate.Refund + substate.DestroyList.Count * spec.GasCosts.DestroyRefund; actualRefund = CalculateClaimableRefund(spentGas, totalToRefund, spec); if (Logger.IsTrace) - Logger.Trace("Refunding unused gas of " + TGasPolicy.GetRemainingGas(unspentGas) + " and refund of " + actualRefund); + Logger.Trace("Refunding unused gas of " + TGasPolicy.GetRemainingGas(in gasAfterExecution) + " and refund of " + actualRefund); } - else if (codeInsertRefund > 0) + else if (codeInsertRefunds > 0) { - actualRefund = CalculateClaimableRefund(spentGas, codeInsertRefund, spec); + // On error, only regular refund applies; state refund is not applied. + long codeInsertRegularRefund = TGasPolicy.GetCodeInsertRegularRefund(codeInsertRefunds, spec); + if (codeInsertRegularRefund > 0) + { + actualRefund = CalculateClaimableRefund(spentGas, codeInsertRegularRefund, spec); - if (Logger.IsTrace) - Logger.Trace("Refunding delegations only: " + actualRefund); + if (Logger.IsTrace) + Logger.Trace("Refunding delegations only: " + actualRefund); + } } // EIP-7778: Track pre-refund gas for block gas accounting @@ -977,13 +1039,34 @@ protected virtual GasConsumed Refund(Transaction tx, BlockHeader header, IReleas long operationGas = spentGas; long floorGasLong = TGasPolicy.GetRemainingGas(floorGas); - long blockGas = spec.IsEip7778Enabled ? Math.Max(preRefundGas, floorGasLong) : 0; + + // EIP-8037: two-dimensional block gas accounting. + // block_regular excludes all state charges (intrinsic state, reservoir consumed, gas_left spills). + // block_state = committed state gas only. + // Block level: gasUsed = max(sum_regular, sum_state). + long blockGas; + long blockStateGas; + if (spec.IsEip8037Enabled) + { + long intrinsicState = TGasPolicy.GetStateReservoir(in intrinsicGasStandard); + long initialReservoir = Math.Max(0, tx.GasLimit - intrinsicState - Eip7825Constants.DefaultTxGasLimitCap); + long reservoirConsumed = initialReservoir - TGasPolicy.GetStateReservoir(in gasAfterExecution); + long stateGasSpill = TGasPolicy.GetStateGasSpill(in gasAfterExecution); + long txRegularGas = preRefundGas - intrinsicState - reservoirConsumed - stateGasSpill; + blockGas = Math.Max(txRegularGas, floorGasLong); + blockStateGas = TGasPolicy.GetStateGasUsed(in gasAfterExecution); + } + else + { + blockGas = spec.IsEip7778Enabled ? Math.Max(preRefundGas, floorGasLong) : 0; + blockStateGas = 0; + } spentGas = Math.Max(spentGas, floorGasLong); UInt256 refundAmount = (ulong)(tx.GasLimit - spentGas) * gasPrice; PayRefund(tx, refundAmount, spec); - return new GasConsumed(spentGas, operationGas, blockGas); + return new GasConsumed(spentGas, operationGas, blockGas, blockStateGas); } protected virtual void PayRefund(Transaction tx, UInt256 refundAmount, IReleaseSpec spec) diff --git a/src/Nethermind/Nethermind.Evm/TransferLog.cs b/src/Nethermind/Nethermind.Evm/TransferLog.cs index 0db51ecd135..4a690c24743 100644 --- a/src/Nethermind/Nethermind.Evm/TransferLog.cs +++ b/src/Nethermind/Nethermind.Evm/TransferLog.cs @@ -11,6 +11,8 @@ public static class TransferLog { // keccak256('Transfer(address,address,uint256)') public static readonly Hash256 TransferSignature = new("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"); + // keccak256('Burn(address,uint256)') + public static readonly Hash256 BurnSignature = new("0xcc16f5dbb4873280815c1ee09dbd06736cffcc184412cf7a71a0fdb75d397ca5"); // keccak256('Selfdestruct(address,uint256)') public static readonly Hash256 SelfDestructSignature = new("0x4bfaba3443c1a1836cd362418edc679fc96cae8449cbefccb6457cdf2c943083"); public static readonly Address Sender = Address.SystemUser; @@ -19,6 +21,9 @@ public static class TransferLog public static LogEntry CreateTransfer(Address from, Address to, in UInt256 amount) => CreateTransferInternal(Sender, from, to, amount); + public static LogEntry CreateBurn(Address account, in UInt256 amount) => + new(Sender, amount.ToBigEndian(), [BurnSignature, account.ToHash().ToHash256()]); + public static LogEntry CreateSelfDestruct(Address contract, in UInt256 amount) => new(Sender, amount.ToBigEndian(), [SelfDestructSignature, contract.ToHash().ToHash256()]); diff --git a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs index 591f2c1960c..fbe43230bbf 100644 --- a/src/Nethermind/Nethermind.Evm/VirtualMachine.cs +++ b/src/Nethermind/Nethermind.Evm/VirtualMachine.cs @@ -262,12 +262,12 @@ public TransactionSubstate ExecuteTransaction( // Restore the previous state from the stack and mark it as a continuation. _currentState = _stateStack.Pop(); _currentState.IsContinuation = true; - // Refund the remaining gas from the completed call frame. - TGasPolicy.Refund(ref _currentState.Gas, in previousState.Gas); bool previousStateSucceeded = true; if (!callResult.ShouldRevert) { + // Refund the remaining gas from the completed call frame (success path). + TGasPolicy.Refund(ref _currentState.Gas, in previousState.Gas); long gasAvailableForCodeDeposit = TGasPolicy.GetRemainingGas(previousState.Gas); // Process contract creation calls differently from regular calls. @@ -305,6 +305,9 @@ public TransactionSubstate ExecuteTransaction( } else { + // On revert, return remaining regular gas and restore all state gas to parent reservoir. + TGasPolicy.UpdateGasUp(ref _currentState.Gas, TGasPolicy.GetRemainingGas(in previousState.Gas)); + TGasPolicy.RestoreChildStateGas(ref _currentState.Gas, in previousState.Gas, previousState.InitialStateReservoir); // Revert state changes for the previous call frame when a revert condition is signaled. HandleRevert(previousState, callResult, ref previousCallOutput); } @@ -413,41 +416,15 @@ protected void HandleEofCreate(in CallResult callResult, VmState pre IReleaseSpec spec = BlockExecutionContext.Spec; // 3 - if updated deploy container size exceeds MAX_CODE_SIZE instruction exceptionally aborts - bool invalidCode = bytecodeResultArray.Length > spec.MaxCodeSize; - long codeDepositGasCost = CodeDepositHandler.CalculateCost(spec, bytecodeResultArray?.Length ?? 0); - if (gasAvailableForCodeDeposit >= codeDepositGasCost && !invalidCode) + if (!CodeDepositHandler.CalculateCost(spec, bytecodeResultArray.Length, out long regularDepositCost, out long stateDepositCost)) { - // 4 - set state[new_address].code to the updated deploy container - // push new_address onto the stack (already done before the ifs) - _codeInfoRepository.InsertCode(bytecodeResultArray, callCodeOwner, spec); - TGasPolicy.ConsumeCodeDeposit(ref _currentState.Gas, codeDepositGasCost); - - if (_txTracer.IsTracingActions) - { - _txTracer.ReportActionEnd(TGasPolicy.GetRemainingGas(previousState.Gas) - codeDepositGasCost, callCodeOwner, bytecodeResultArray); - } + regularDepositCost = long.MaxValue; + stateDepositCost = long.MaxValue; } - else if (spec.FailOnOutOfGasCodeDeposit || invalidCode) - { - TGasPolicy.Consume(ref _currentState.Gas, gasAvailableForCodeDeposit); - _worldState.Restore(previousState.Snapshot); - if (!previousState.IsCreateOnPreExistingAccount) - { - _worldState.DeleteAccount(callCodeOwner); - } - - _previousCallResult = BytesZero; - previousStateSucceeded = false; - if (_txTracer.IsTracingActions) - { - _txTracer.ReportActionError(invalidCode ? EvmExceptionType.InvalidCode : EvmExceptionType.OutOfGas); - } - } - else if (_txTracer.IsTracingActions) - { - _txTracer.ReportActionEnd(0L, callCodeOwner, bytecodeResultArray); - } + bool invalidCode = bytecodeResultArray.Length > spec.MaxCodeSize; + TryChargeAndDepositCode(previousState, gasAvailableForCodeDeposit, ref previousStateSucceeded, + regularDepositCost, stateDepositCost, invalidCode, bytecodeResultArray); } /// @@ -474,83 +451,91 @@ protected void HandleLegacyCreate( long gasAvailableForCodeDeposit, ref bool previousStateSucceeded) { - // Cache whether transaction tracing is enabled to avoid multiple property lookups. - bool isTracing = _txTracer.IsTracingActions; - - // Get the address of the account that initiated the contract creation. - Address callCodeOwner = previousState.Env.ExecutingAccount; - IReleaseSpec spec = BlockExecutionContext.Spec; - // Calculate the gas cost required to deposit the contract code using legacy cost rules. - long codeDepositGasCost = CodeDepositHandler.CalculateCost(spec, callResult.Output.Bytes.Length); + if (!CodeDepositHandler.CalculateCost(spec, callResult.Output.Bytes.Length, out long regularDepositCost, out long stateDepositCost)) + { + regularDepositCost = long.MaxValue; + stateDepositCost = long.MaxValue; + } - // Validate the code against legacy rules; mark it invalid if it fails these checks. bool invalidCode = !CodeDepositHandler.IsValidWithLegacyRules(spec, callResult.Output.Bytes); + TryChargeAndDepositCode(previousState, gasAvailableForCodeDeposit, ref previousStateSucceeded, + regularDepositCost, stateDepositCost, invalidCode, callResult.Output.Bytes); + } - // Check if there is sufficient gas and the code is valid. - if (gasAvailableForCodeDeposit >= codeDepositGasCost && !invalidCode) - { - // Deposit the contract code into the repository. - ReadOnlyMemory code = callResult.Output.Bytes; - _codeInfoRepository.InsertCode(code, callCodeOwner, spec); + protected TransactionSubstate PrepareTopLevelSubstate(scoped in CallResult callResult) + { + return new TransactionSubstate( + callResult.Output, + _currentState.Refund, + _currentState.AccessTracker.DestroyList, + _currentState.AccessTracker.Logs, + callResult.ShouldRevert, + isTracerConnected: _txTracer.IsTracing, + callResult.ExceptionType, + _logger); + } - // Deduct the gas cost for the code deposit from the current state's available gas. - TGasPolicy.ConsumeCodeDeposit(ref _currentState.Gas, codeDepositGasCost); + private void TryChargeAndDepositCode( + VmState previousState, + long gasAvailableForCodeDeposit, + ref bool previousStateSucceeded, + long regularDepositCost, + long stateDepositCost, + bool invalidCode, + ReadOnlyMemory code) + { + IReleaseSpec spec = BlockExecutionContext.Spec; + Address callCodeOwner = previousState.Env.ExecutingAccount; + + long childStateReservoir = TGasPolicy.GetStateReservoir(in previousState.Gas); + long stateSpill = Math.Max(0, stateDepositCost - childStateReservoir); + bool hasEnoughGas = gasAvailableForCodeDeposit >= regularDepositCost + stateSpill; + bool chargedCodeDeposit = false; - // If tracing is enabled, report the successful code deposit operation. - if (isTracing) + if (hasEnoughGas && !invalidCode) + { + TGasPolicy gasAfterCodeDeposit = _currentState.Gas; + chargedCodeDeposit = TGasPolicy.TryConsumeStateAndRegularGas(ref gasAfterCodeDeposit, stateDepositCost, regularDepositCost); + if (chargedCodeDeposit) { - _txTracer.ReportActionEnd(TGasPolicy.GetRemainingGas(previousState.Gas) - codeDepositGasCost, callCodeOwner, callResult.Output.Bytes); + _currentState.Gas = gasAfterCodeDeposit; + _codeInfoRepository.InsertCode(code, callCodeOwner, spec); + if (_txTracer.IsTracingActions) + { + _txTracer.ReportActionEnd(TGasPolicy.GetRemainingGas(previousState.Gas) - regularDepositCost, callCodeOwner, code); + } } } - // If the code deposit should fail due to out-of-gas or invalid code conditions... - else if (spec.FailOnOutOfGasCodeDeposit || invalidCode) + + if (!chargedCodeDeposit && (spec.FailOnOutOfGasCodeDeposit || invalidCode)) { - // Consume all remaining gas allocated for the code deposit. TGasPolicy.Consume(ref _currentState.Gas, gasAvailableForCodeDeposit); - - // Roll back the world state to its snapshot from before the creation attempt. + // Code deposit failure is an exceptional halt of the child CREATE frame. + // Refund already merged the child's state gas (reservoir, stateGasUsed) into the parent, + // but halt semantics require restoring the full initial state reservoir and discarding + // the child's stateGasUsed (since the child's state changes are being reverted). + TGasPolicy.RevertRefundToHalt(ref _currentState.Gas, in previousState.Gas, previousState.InitialStateReservoir); _worldState.Restore(previousState.Snapshot); - - // If the contract creation did not target a pre-existing account, delete the account. if (!previousState.IsCreateOnPreExistingAccount) { _worldState.DeleteAccount(callCodeOwner); } - // Reset the previous call result to indicate that no valid code was deployed. _previousCallResult = BytesZero; - - // Mark the previous state execution as failed. previousStateSucceeded = false; - // Report an error via the tracer, indicating whether the failure was due to invalid code or gas exhaustion. - if (isTracing) + if (_txTracer.IsTracingActions) { _txTracer.ReportActionError(invalidCode ? EvmExceptionType.InvalidCode : EvmExceptionType.OutOfGas); } } - // In scenarios where the code deposit does not strictly mandate a failure, - // report the end of the action if tracing is enabled. - else if (isTracing) + else if (!chargedCodeDeposit && _txTracer.IsTracingActions) { - _txTracer.ReportActionEnd(TGasPolicy.GetRemainingGas(previousState.Gas) - codeDepositGasCost, callCodeOwner, callResult.Output.Bytes); + _txTracer.ReportActionEnd(0L, callCodeOwner, code); } } - protected TransactionSubstate PrepareTopLevelSubstate(scoped in CallResult callResult) - { - return new TransactionSubstate( - callResult.Output, - _currentState.Refund, - _currentState.AccessTracker.DestroyList, - _currentState.AccessTracker.Logs, - callResult.ShouldRevert, - isTracerConnected: _txTracer.IsTracing, - callResult.ExceptionType, - _logger); - } - /// /// Reverts the state changes made during the execution of a call frame. /// This method restores the world state to a previous snapshot, sets appropriate @@ -673,15 +658,26 @@ protected TransactionSubstate HandleFailure(Exception failure, str ReturnDataBuffer = Array.Empty(); previousCallOutput = ZeroPaddedSpan.Empty; - // Dispose of the current failing state and restore the previous call frame from the stack. - _currentState.Dispose(); - _currentState = _stateStack.Pop(); - _currentState.IsContinuation = true; + PopAndRestoreParentState(); shouldExit = false; return default; } + /// + /// Pops the child state, restores state gas to the parent frame, and disposes the child. + /// Must be called before the child is disposed so that + /// spill/usage accounting is not lost. + /// + private void PopAndRestoreParentState() + { + VmState childState = _currentState; + _currentState = _stateStack.Pop(); + TGasPolicy.RestoreChildStateGas(ref _currentState.Gas, in childState.Gas, childState.InitialStateReservoir); + _currentState.IsContinuation = true; + childState.Dispose(); + } + /// /// Prepares the execution environment for the next call frame by updating the current state /// and resetting relevant output fields. @@ -759,10 +755,7 @@ protected TransactionSubstate HandleException(scoped in CallResult callResult, s ReturnDataBuffer = Array.Empty(); previousCallOutput = ZeroPaddedSpan.Empty; - // Clean up the current failing state and pop the parent call frame from the state stack. - _currentState.Dispose(); - _currentState = _stateStack.Pop(); - _currentState.IsContinuation = true; + PopAndRestoreParentState(); // Return null to indicate that the failure was handled and execution should continue in the parent frame. shouldExit = false; @@ -1332,6 +1325,9 @@ protected CallResult RunByteCode( // Set the exception type to OutOfGas if gas has been exhausted. exceptionType = EvmExceptionType.OutOfGas; ReturnFailure: + // EIP-8037: write gas back to state on failure so RestoreChildStateGas + // can read accumulated StateGasUsed/StateGasSpill from the child frame. + _currentState.Gas = gas; // Return a failure CallResult based on the remaining gas and the exception type. return GetFailureReturn(TGasPolicy.GetRemainingGas(in gas), exceptionType); @@ -1463,24 +1459,36 @@ private void AddTransferLog(VmState currentState) // CALLCODE: value is transferred from ExecutingAccount to ExecutingAccount (self-transfer), so no log if (currentState.ExecutionType is not (ExecutionType.DELEGATECALL or ExecutionType.CALLCODE)) { - AddTransferLog(currentState.From, currentState.To, currentState.Env.Value); + // Runtime check acceptable here — called once per frame entry, not per instruction. + if (Spec.IsEip7708Enabled && !currentState.Env.Value.IsZero && currentState.From != currentState.To) + AddLog(TransferLog.CreateTransfer(currentState.From, currentState.To, currentState.Env.Value)); } } - internal void AddTransferLog(Address from, Address to, in UInt256 value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void AddTransferLog(Address from, Address to, in UInt256 value) + where TEip7708 : struct, IFlag { - // Self-transfers don't change balances, so don't log them - if (Spec.IsEip7708Enabled && !value.IsZero && from != to) - { + if (TEip7708.IsActive && !value.IsZero && from != to) AddLog(TransferLog.CreateTransfer(from, to, value)); - } } - internal void AddSelfDestructLog(Address contract, in UInt256 value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void AddSelfDestructLog(Address executingAccount, Address inheritor, in UInt256 value) + where TEip8037 : struct, IFlag + where TEip7708 : struct, IFlag { - if (Spec.IsEip7708Enabled && !value.IsZero) + if (!TEip7708.IsActive || value.IsZero) return; + + if (executingAccount == inheritor) + { + AddLog(TEip8037.IsActive + ? TransferLog.CreateBurn(executingAccount, value) + : TransferLog.CreateSelfDestruct(executingAccount, value)); + } + else { - AddLog(TransferLog.CreateSelfDestruct(contract, value)); + AddLog(TransferLog.CreateTransfer(executingAccount, inheritor, value)); } } } diff --git a/src/Nethermind/Nethermind.Evm/VmState.cs b/src/Nethermind/Nethermind.Evm/VmState.cs index f0f21f3009f..ed6e9b91f55 100644 --- a/src/Nethermind/Nethermind.Evm/VmState.cs +++ b/src/Nethermind/Nethermind.Evm/VmState.cs @@ -81,6 +81,7 @@ public class VmState : IDisposable public byte[]? DataStack; public ReturnState[]? ReturnStack; public TGasPolicy Gas; + public long InitialStateReservoir; internal long OutputDestination { get; private set; } // TODO: move to CallEnv internal long OutputLength { get; private set; } // TODO: move to CallEnv public long Refund { get; set; } @@ -182,6 +183,7 @@ private void Initialize( } _accessTracker.TakeSnapshot(); Gas = gas; + InitialStateReservoir = TGasPolicy.GetStateReservoir(in gas); OutputDestination = outputDestination; OutputLength = outputLength; Refund = 0; diff --git a/src/Nethermind/Nethermind.Merge.AuRa.Test/AuRaMergeEngineModuleTests.cs b/src/Nethermind/Nethermind.Merge.AuRa.Test/AuRaMergeEngineModuleTests.cs index e5433af5f9b..ebfc467bff4 100644 --- a/src/Nethermind/Nethermind.Merge.AuRa.Test/AuRaMergeEngineModuleTests.cs +++ b/src/Nethermind/Nethermind.Merge.AuRa.Test/AuRaMergeEngineModuleTests.cs @@ -36,8 +36,7 @@ namespace Nethermind.Merge.AuRa.Test; public class AuRaMergeEngineModuleTests : EngineModuleTests { - protected override MergeTestBlockchain CreateBaseBlockchain( - IMergeConfig? mergeConfig = null) + protected override MergeTestBlockchain CreateBaseBlockchain(IMergeConfig? mergeConfig = null) => new MergeAuRaTestBlockchain(mergeConfig); protected override Hash256 ExpectedBlockHash => new("0x990d377b67dbffee4a60db6f189ae479ffb406e8abea16af55e0469b8524cf46"); @@ -60,18 +59,11 @@ int ErrorCode public override Task Should_process_block_as_expected_V4(string latestValidHash, string blockHash, string stateRoot, string payloadId) => base.Should_process_block_as_expected_V4(latestValidHash, blockHash, stateRoot, payloadId); - [TestCase( - "0xca2fbb93848df6500fcc33f9036f43f33db9844719f0a5fc69079d8d90dbb28f", - "0x4b8e5a6567229461665f1475a39665a3df55b367ca5fd9cc861fe70d4d5836c3", - "0xd4ab6af74f5566d54b164115a9b00726bd35e2170d206e466c4be30ebfe23894", - "0x103ea062e6e09c06")] + [TestCase("0xca2fbb93848df6500fcc33f9036f43f33db9844719f0a5fc69079d8d90dbb28f", "0x4b8e5a6567229461665f1475a39665a3df55b367ca5fd9cc861fe70d4d5836c3", "0xd4ab6af74f5566d54b164115a9b00726bd35e2170d206e466c4be30ebfe23894", "0x103ea062e6e09c06")] public override Task Should_process_block_as_expected_V2(string latestValidHash, string blockHash, string stateRoot, string payloadId) => base.Should_process_block_as_expected_V2(latestValidHash, blockHash, stateRoot, payloadId); - [TestCase( - "0xe4333fcde906675e50500bf53a6c73bc51b2517509bc3cff2d24d0de9b8dd23e", - "0xe168b70ac8a6f7d90734010030801fbb2dcce03a657155c4024b36ba8d1e3926", - "0xb22228e153345f9b")] + [TestCase("0xe4333fcde906675e50500bf53a6c73bc51b2517509bc3cff2d24d0de9b8dd23e", "0xe168b70ac8a6f7d90734010030801fbb2dcce03a657155c4024b36ba8d1e3926", "0xb22228e153345f9b")] public override Task processing_block_should_serialize_valid_responses(string blockHash, string latestValidHash, string payloadId) => base.processing_block_should_serialize_valid_responses(blockHash, latestValidHash, payloadId); @@ -81,101 +73,37 @@ public override Task processing_block_should_serialize_valid_responses(string bl "0xb8a1a0780980ab4e20a46237a3c533af8cd0386cf4c74d05c8ec5e9bf5cbc482", "0x2802e8a8c34cd1ea", _auraWithdrawalContractAddress)] - public override async Task Should_process_block_as_expected_V6(string latestValidHash, string blockHash, string stateRoot, string payloadId, string? auraWithdrawalContractAddress) - => await base.Should_process_block_as_expected_V6(latestValidHash, blockHash, stateRoot, payloadId, auraWithdrawalContractAddress); + public override async Task Should_process_block_as_expected_V6(string latestValidHash, string blockHash, string stateRoot, string payloadId, string? customWithdrawalContractAddress) + => await base.Should_process_block_as_expected_V6(latestValidHash, blockHash, stateRoot, payloadId, customWithdrawalContractAddress); - [TestCase( - "0x14d7d22cfaa851f3b79a790d6f961f0cc4da2e714cd15b16bce8468f25152911", - "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", - "0x3e98244425fbc5413150a01fd823bece9ae66ef182f11597f0abdfd251d9aa16")] - public override Task NewPayloadV5_accepts_valid_BAL(string blockHash, string receiptsRoot, string stateRoot) - => NewPayloadV5( - blockHash, - receiptsRoot, - stateRoot, - auraWithdrawalContractAddress: _auraWithdrawalContractAddress); + [TestCase("0x14d7d22cfaa851f3b79a790d6f961f0cc4da2e714cd15b16bce8468f25152911", "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", "0x3e98244425fbc5413150a01fd823bece9ae66ef182f11597f0abdfd251d9aa16", false, false)] + public override Task NewPayloadV5_accepts_valid_BAL(string? blockHash, string? receiptsRoot, string? stateRoot, bool eip8037Enabled, bool useEnginePipeline) + => NewPayloadV5_via_manual_block(blockHash, receiptsRoot, stateRoot, customWithdrawalContractAddress: _auraWithdrawalContractAddress); - [TestCase( - "0x0f125b68c09e5dc3b57cc47e93189d431fbb2d02d0aceb001eda8938ae933e21", - "0x914892da85e1a085a90e8a02f9a9cf0777d73c5798047c7324859b1c5ad9b67f", - "0x7255eb3f45136fccaa3449d2787f80e33e197b4fbc417f1d62423a72a76b5d43", - "0xcf205144eb1991b718be9c4694f22d6b0937740c17e2d811c8fc3c999d596fcf", - _auraWithdrawalContractAddress)] - public override Task NewPayloadV5_rejects_invalid_BAL_after_processing(string blockHash, string stateRoot, string invalidBalHash, string expectedBalHash, string? auraWithdrawalContractAddress) - => base.NewPayloadV5_rejects_invalid_BAL_after_processing(blockHash, stateRoot, invalidBalHash, expectedBalHash, auraWithdrawalContractAddress); + [TestCase("0x0f125b68c09e5dc3b57cc47e93189d431fbb2d02d0aceb001eda8938ae933e21", "0x914892da85e1a085a90e8a02f9a9cf0777d73c5798047c7324859b1c5ad9b67f", "0x7255eb3f45136fccaa3449d2787f80e33e197b4fbc417f1d62423a72a76b5d43", "0xcf205144eb1991b718be9c4694f22d6b0937740c17e2d811c8fc3c999d596fcf", _auraWithdrawalContractAddress)] + public override Task NewPayloadV5_rejects_invalid_BAL_after_processing(string blockHash, string stateRoot, string invalidBalHash, string expectedBalHash, string? customWithdrawalContractAddress) + => base.NewPayloadV5_rejects_invalid_BAL_after_processing(blockHash, stateRoot, invalidBalHash, expectedBalHash, customWithdrawalContractAddress); - [TestCase( - "0x5ab84199bdbe0d5806de6bffbbd52cf31ede2248f842395aa9a850a45ad9f4db", - "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", - "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b")] - public override Task NewPayloadV5_rejects_invalid_BAL_with_incorrect_changes_early(string blockHash, string receiptsRoot, string stateRoot) - => NewPayloadV5( - blockHash, - receiptsRoot, - stateRoot, - "InvalidBlockLevelAccessList: Suggested block-level access list contained incorrect changes for 0xdc98b4d0af603b4fb5ccdd840406a0210e5deff8 at index 3.", - withIncorrectChange: true, - auraWithdrawalContractAddress: _auraWithdrawalContractAddress); - - [TestCase( - "0x56f188e232e95462ad7235ca53b336f5f73cc208992d307033210c085ea6f959", - "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", - "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b")] - public override Task NewPayloadV5_rejects_invalid_BAL_with_missing_changes_early(string blockHash, string receiptsRoot, string stateRoot) - => NewPayloadV5( - blockHash, - receiptsRoot, - stateRoot, - "InvalidBlockLevelAccessList: Suggested block-level access list missing account changes for 0xdc98b4d0af603b4fb5ccdd840406a0210e5deff8 at index 2.", - withMissingChange: true, - auraWithdrawalContractAddress: _auraWithdrawalContractAddress); + [TestCase("0x5ab84199bdbe0d5806de6bffbbd52cf31ede2248f842395aa9a850a45ad9f4db", "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b", false, false, BalErrorKind.IncorrectChange)] + [TestCase("0x56f188e232e95462ad7235ca53b336f5f73cc208992d307033210c085ea6f959", "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b", false, false, BalErrorKind.MissingChange)] + [TestCase("0x1625b8215c5d6ab493105efb8cc20b7409d4957ca46d98996c6cc01e50b69ab3", "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b", false, false, BalErrorKind.SurplusChange)] + [TestCase("0x91e03d0f1b756f6577cab73c9f910f9b18fbe45ac27bb346ada0fa912a71dac8", "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b", false, false, BalErrorKind.SurplusReads)] + public override Task NewPayloadV5_rejects_invalid_BAL_early(string? blockHash, string? receiptsRoot, string? stateRoot, bool eip8037Enabled, bool useEnginePipeline, BalErrorKind errorKind) => + NewPayloadV5_via_manual_block(blockHash, receiptsRoot, stateRoot, GetExpectedBalError(errorKind), errorKind, customWithdrawalContractAddress: _auraWithdrawalContractAddress); - [TestCase( - "0x1625b8215c5d6ab493105efb8cc20b7409d4957ca46d98996c6cc01e50b69ab3", - "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", - "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b")] - public override Task NewPayloadV5_rejects_invalid_BAL_with_surplus_changes_early(string blockHash, string receiptsRoot, string stateRoot) - => NewPayloadV5( - blockHash, - receiptsRoot, - stateRoot, - "InvalidBlockLevelAccessList: Suggested block-level access list contained surplus changes for 0x65942aaf2c32a1aca4f14e82e94fce91960893a2 at index 2.", - withSurplusChange: true, - auraWithdrawalContractAddress: _auraWithdrawalContractAddress); - - [TestCase( - "0x91e03d0f1b756f6577cab73c9f910f9b18fbe45ac27bb346ada0fa912a71dac8", - "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", - "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b")] - public override Task NewPayloadV5_rejects_invalid_BAL_with_surplus_reads_early(string blockHash, string receiptsRoot, string stateRoot) - => NewPayloadV5( - blockHash, - receiptsRoot, - stateRoot, - "InvalidBlockLevelAccessList: Suggested block-level access list contained invalid storage reads.", - withSurplusReads: true, - auraWithdrawalContractAddress: _auraWithdrawalContractAddress); - - [Test] [TestCase(_auraWithdrawalContractAddress)] - public override async Task GetPayloadV6_builds_block_with_BAL(string? auraWithdrawalContractAddress) - => await base.GetPayloadV6_builds_block_with_BAL(auraWithdrawalContractAddress); + public override async Task GetPayloadV6_builds_block_with_BAL(string? customWithdrawalContractAddress) => + await base.GetPayloadV6_builds_block_with_BAL(customWithdrawalContractAddress); - [Test] - [TestCase( - "0xa66ec67b117f57388da53271f00c22a68e6c297b564f67c5904e6f2662881875", - "0xe168b70ac8a6f7d90734010030801fbb2dcce03a657155c4024b36ba8d1e3926" - )] + [TestCase("0xa66ec67b117f57388da53271f00c22a68e6c297b564f67c5904e6f2662881875", "0xe168b70ac8a6f7d90734010030801fbb2dcce03a657155c4024b36ba8d1e3926")] [Parallelizable(ParallelScope.None)] [Obsolete] public override Task forkchoiceUpdatedV1_should_communicate_with_boost_relay_through_http(string blockHash, string parentHash) => base.forkchoiceUpdatedV1_should_communicate_with_boost_relay_through_http(blockHash, parentHash); [Ignore("Withdrawals are not withdrawn due to lack of Aura contract in tests")] - public override Task Can_apply_withdrawals_correctly((Withdrawal[][] Withdrawals, (Address Account, UInt256 BalanceIncrease)[] ExpectedAccountIncrease) input) - { - return base.Can_apply_withdrawals_correctly(input); - } + public override Task Can_apply_withdrawals_correctly((Withdrawal[][] Withdrawals, (Address Account, UInt256 BalanceIncrease)[] ExpectedAccountIncrease) input) => + base.Can_apply_withdrawals_correctly(input); [Test] [Retry(3)] @@ -194,7 +122,7 @@ public MergeAuRaTestBlockchain(IMergeConfig? mergeConfig = null) protected override ContainerBuilder ConfigureContainer(ContainerBuilder builder, IConfigProvider configProvider) { return base.ConfigureContainer(builder, configProvider) - .AddDecorator((ctx, specProvider) => + .AddDecorator((_, specProvider) => { // I guess ideally, just make a wrapper for `ISpecProvider` that replace only SealEngine. ISpecProvider unwrappedSpecProvider = specProvider; @@ -204,7 +132,7 @@ protected override ContainerBuilder ConfigureContainer(ContainerBuilder builder, provider.SealEngine = SealEngineType; return specProvider; }) - .WithGenesisPostProcessor((block, state) => + .WithGenesisPostProcessor((block, _) => { block.Header.AuRaStep = 0; block.Header.AuRaSignature = new byte[65]; @@ -229,7 +157,7 @@ protected override ContainerBuilder ConfigureContainer(ContainerBuilder builder, // AuRa was never configured correctly in test. .AddScoped() - .AddDecorator((ctx, api) => + .AddDecorator((_, api) => { // Yes getting from `TestBlockchain` itself, since steps are not run // and some of these are not from DI. you know... chicken and egg, but don't forget about the rooster. @@ -271,7 +199,7 @@ protected override IBlockProducer CreateTestBlockProducer() IAuRaStepCalculator auraStepCalculator = Substitute.For(); auraStepCalculator.TimeToNextStep.Returns(TimeSpan.FromMilliseconds(0)); - var env = BlockProducerEnvFactory.Create(); + IBlockProducerEnv env = BlockProducerEnvFactory.Create(); FollowOtherMiners gasLimitCalculator = new(MainnetSpecProvider.Instance); AuRaBlockProducer preMergeBlockProducer = new( env.TxSource, diff --git a/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V6.cs b/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V6.cs index f2bd28a0cc1..1f95263535b 100644 --- a/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V6.cs +++ b/src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V6.cs @@ -8,6 +8,7 @@ using Nethermind.Core.Extensions; using Nethermind.JsonRpc; using Nethermind.Merge.Plugin.Data; +using Nethermind.Core.Specs; using Nethermind.Specs; using Nethermind.Specs.Forks; using NUnit.Framework; @@ -22,24 +23,33 @@ using Nethermind.Int256; using Nethermind.JsonRpc.Test; using System; +using Nethermind.Core.ExecutionRequest; using Nethermind.Core.Test; +using Nethermind.Crypto; +using Nethermind.State.Proofs; namespace Nethermind.Merge.Plugin.Test; public partial class EngineModuleTests { + public enum BalErrorKind + { + None, + IncorrectChange, + MissingChange, + SurplusChange, + SurplusReads, + } - [TestCase( - "0xb54389c226c76c61de0a8ebea2fe74cb0119295d34b8c01d0897901867c41c63", - "0x14c38ed94cf91d5323eb3aaa7ff6c64c4c059a0a898658fcbc37f9723c25e6b3", - "0x8a792f3d13211724decede460a451cdac669b5aaae37a01c2110d9f3114bc8a2", - "0xfe420b1626a1f16d", - null)] - public virtual async Task Should_process_block_as_expected_V6(string latestValidHash, string blockHash, - string stateRoot, string payloadId, string? auraWithdrawalContractAddress) + [TestCase("0xb54389c226c76c61de0a8ebea2fe74cb0119295d34b8c01d0897901867c41c63", "0x14c38ed94cf91d5323eb3aaa7ff6c64c4c059a0a898658fcbc37f9723c25e6b3", "0x8a792f3d13211724decede460a451cdac669b5aaae37a01c2110d9f3114bc8a2", "0xfe420b1626a1f16d")] + public virtual async Task Should_process_block_as_expected_V6( + string latestValidHash, + string blockHash, + string stateRoot, + string payloadId, + string? customWithdrawalContractAddress = null) { - using MergeTestBlockchain chain = - await CreateBlockchain(Amsterdam.Instance); + using MergeTestBlockchain chain = await CreateBlockchain(Amsterdam.Instance); IEngineRpcModule rpc = chain.EngineRpcModule; Hash256 startingHead = chain.BlockTree.HeadHash; Hash256 prevRandao = Keccak.Zero; @@ -62,13 +72,9 @@ public virtual async Task Should_process_block_as_expected_V6(string latestValid parentBeaconBLockRoot = Keccak.Zero, slotNumber = slotNumber.ToHexString(true), }; - string?[] @params = new string?[] - { - chain.JsonSerializer.Serialize(fcuState), chain.JsonSerializer.Serialize(payloadAttrs) - }; - string expectedPayloadId = payloadId; + object?[] parameters = [chain.JsonSerializer.Serialize(fcuState), chain.JsonSerializer.Serialize(payloadAttrs)]; - string response = await RpcTest.TestSerializedRequest(rpc, "engine_forkchoiceUpdatedV4", @params!); + string response = await RpcTest.TestSerializedRequest(rpc, "engine_forkchoiceUpdatedV4", parameters!); JsonRpcSuccessResponse? successResponse = chain.JsonSerializer.Deserialize(response); using (Assert.EnterMultipleScope()) @@ -79,7 +85,7 @@ public virtual async Task Should_process_block_as_expected_V6(string latestValid Id = successResponse.Id, Result = new ForkchoiceUpdatedV1Result { - PayloadId = expectedPayloadId, + PayloadId = payloadId, PayloadStatus = new PayloadStatusV1 { LatestValidHash = new(latestValidHash), @@ -91,9 +97,9 @@ public virtual async Task Should_process_block_as_expected_V6(string latestValid } BlockAccessListBuilder expectedBalBuilder = Build.A.BlockAccessList.WithPrecompileChanges(startingHead, timestamp); - if (auraWithdrawalContractAddress is not null) + if (customWithdrawalContractAddress is not null) { - expectedBalBuilder.WithAccountChanges([new(new Address(auraWithdrawalContractAddress)), new(Address.SystemUser)]); + expectedBalBuilder.WithAccountChanges([new(new Address(customWithdrawalContractAddress)), new(Address.SystemUser)]); } Hash256 expectedBlockHash = new(blockHash); @@ -127,7 +133,7 @@ public virtual async Task Should_process_block_as_expected_V6(string latestValid expectedBalBuilder.TestObject); GetPayloadV6Result expectedPayload = new(block, UInt256.Zero, new BlobsBundleV2(block), executionRequests: [], shouldOverrideBuilder: false); - response = await RpcTest.TestSerializedRequest(rpc, "engine_getPayloadV6", expectedPayloadId); + response = await RpcTest.TestSerializedRequest(rpc, "engine_getPayloadV6", payloadId); successResponse = chain.JsonSerializer.Deserialize(response); using (Assert.EnterMultipleScope()) @@ -165,9 +171,9 @@ public virtual async Task Should_process_block_as_expected_V6(string latestValid safeBlockHash = expectedBlockHash.ToString(true), finalizedBlockHash = startingHead.ToString(true) }; - @params = new[] { chain.JsonSerializer.Serialize(fcuState), null }; + parameters = [chain.JsonSerializer.Serialize(fcuState), null]; - response = await RpcTest.TestSerializedRequest(rpc, "engine_forkchoiceUpdatedV4", @params!); + response = await RpcTest.TestSerializedRequest(rpc, "engine_forkchoiceUpdatedV4", parameters!); successResponse = chain.JsonSerializer.Deserialize(response); using (Assert.EnterMultipleScope()) @@ -191,16 +197,14 @@ public virtual async Task Should_process_block_as_expected_V6(string latestValid } - [TestCase( - "0x0981253ff1b66ee40650f7fa7efe53f772bc11bd4fef3a3574cf91495a1533dd", - "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", - "0x42a80ba6d5783c392ffcc6b3c15d7ef06be8ae71c2ff5f42377acdec67a5766c")] - public virtual async Task NewPayloadV5_accepts_valid_BAL(string blockHash, string receiptsRoot, string stateRoot) - => await NewPayloadV5( - blockHash, - receiptsRoot, - stateRoot, - null); + [TestCase("0x0981253ff1b66ee40650f7fa7efe53f772bc11bd4fef3a3574cf91495a1533dd", "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", "0x42a80ba6d5783c392ffcc6b3c15d7ef06be8ae71c2ff5f42377acdec67a5766c", false, false)] + [TestCase(null, null, null, false, true)] + [TestCase("0xc7ca0c8c9d0b29e9c432d34bcc6b0dd5adef6732ed94096465847ade2da72aae", "0x056b23fbba480696b65fe5a59b8f2148a1299103c4f57df839233af2cf4ca2d2", "0xfad798172a2bbd423c90a023d345c7a7812e067918edb7630c2388736f197f29", true, false)] + [TestCase("0xc7ca0c8c9d0b29e9c432d34bcc6b0dd5adef6732ed94096465847ade2da72aae", "0x056b23fbba480696b65fe5a59b8f2148a1299103c4f57df839233af2cf4ca2d2", "0xfad798172a2bbd423c90a023d345c7a7812e067918edb7630c2388736f197f29", true, true)] + public virtual Task NewPayloadV5_accepts_valid_BAL(string? blockHash, string? receiptsRoot, string? stateRoot, bool eip8037Enabled, bool useEnginePipeline) => + !eip8037Enabled && !useEnginePipeline + ? NewPayloadV5_via_manual_block(blockHash, receiptsRoot, stateRoot) + : NewPayloadV5_via_engine_built(blockHash, receiptsRoot, stateRoot, eip8037Enabled, useSerializedRpc: !useEnginePipeline); [TestCase( "0x43b3722358b0a8b570fdfd846a5b836ad2fae3f7f58b3ac3519858472a997214", @@ -208,10 +212,9 @@ public virtual async Task NewPayloadV5_accepts_valid_BAL(string blockHash, strin "0xf33cd1904c18109e882bfa965997ba802d408bd834a61920aba651fbaeb78dd3", "0x4de7e37b17928203599e876a1f226dce8512f61f5672e67d4964bbc26ddc1ed4", null)] - public virtual async Task NewPayloadV5_rejects_invalid_BAL_after_processing(string blockHash, string stateRoot, string invalidBalHash, string expectedBalHash, string? auraWithdrawalContractAddress) + public virtual async Task NewPayloadV5_rejects_invalid_BAL_after_processing(string blockHash, string stateRoot, string invalidBalHash, string expectedBalHash, string? customWithdrawalContractAddress) { - using MergeTestBlockchain chain = - await CreateBlockchain(Amsterdam.Instance); + using MergeTestBlockchain chain = await CreateBlockchain(Amsterdam.NoEip8037Instance); IEngineRpcModule rpc = chain.EngineRpcModule; const ulong timestamp = 1000000; @@ -221,9 +224,9 @@ public virtual async Task NewPayloadV5_rejects_invalid_BAL_after_processing(stri BlockAccessListBuilder invalidBalBuilder = Build.A.BlockAccessList .WithPrecompileChanges(parentHash, timestamp) .WithAccountChanges([new(TestItem.AddressA)]); // additional address - if (auraWithdrawalContractAddress is not null) + if (customWithdrawalContractAddress is not null) { - invalidBalBuilder.WithAccountChanges([new(new Address(auraWithdrawalContractAddress)), new(Address.SystemUser)]); + invalidBalBuilder.WithAccountChanges([new(new Address(customWithdrawalContractAddress)), new(Address.SystemUser)]); } BlockAccessList invalidBal = invalidBalBuilder.TestObject; @@ -276,57 +279,60 @@ public virtual async Task NewPayloadV5_rejects_invalid_BAL_after_processing(stri } } - [TestCase( - "0x2753a5a3fe321381e637a7c0d7673b61555a366bdf75359616b0035f9b405fab", - "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", - "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b")] - public virtual async Task NewPayloadV5_rejects_invalid_BAL_with_incorrect_changes_early(string blockHash, string receiptsRoot, string stateRoot) - => await NewPayloadV5( - blockHash, - receiptsRoot, - stateRoot, - "InvalidBlockLevelAccessList: Suggested block-level access list contained incorrect changes for 0xdc98b4d0af603b4fb5ccdd840406a0210e5deff8 at index 3.", - withIncorrectChange: true); + protected static IEnumerable InvalidBalEarlyTestCases() + { + (string blockHash, BalErrorKind errorKind)[] perKindCases = + [ + ("0x2753a5a3fe321381e637a7c0d7673b61555a366bdf75359616b0035f9b405fab", BalErrorKind.IncorrectChange), + ("0x9f19c60fe32bb002e4b959abddd1ebfd396ddae2e65e9ff87b1c4a0715ade9ad", BalErrorKind.MissingChange), + ("0x383a5a61b956150bc79762844dc40395c9f85e9caae8930a0de2b9e687902eae", BalErrorKind.SurplusChange), + ("0x66478724575325c99be695cc33d2698b6c87bdc7fe4ee0a54813de367f2bf037", BalErrorKind.SurplusReads), + ]; + + foreach ((string blockHash, BalErrorKind errorKind) in perKindCases) + { + yield return new TestCaseData(blockHash, "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b", false, false, errorKind); + yield return new TestCaseData(null, null, null, false, true, errorKind); + yield return new TestCaseData("0xc7ca0c8c9d0b29e9c432d34bcc6b0dd5adef6732ed94096465847ade2da72aae", "0x056b23fbba480696b65fe5a59b8f2148a1299103c4f57df839233af2cf4ca2d2", "0xfad798172a2bbd423c90a023d345c7a7812e067918edb7630c2388736f197f29", true, false, errorKind); + yield return new TestCaseData("0xc7ca0c8c9d0b29e9c432d34bcc6b0dd5adef6732ed94096465847ade2da72aae", "0x056b23fbba480696b65fe5a59b8f2148a1299103c4f57df839233af2cf4ca2d2", "0xfad798172a2bbd423c90a023d345c7a7812e067918edb7630c2388736f197f29", true, true, errorKind); + } + } - [TestCase( - "0x9f19c60fe32bb002e4b959abddd1ebfd396ddae2e65e9ff87b1c4a0715ade9ad", - "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", - "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b")] - public virtual async Task NewPayloadV5_rejects_invalid_BAL_with_missing_changes_early(string blockHash, string receiptsRoot, string stateRoot) - => await NewPayloadV5( - blockHash, - receiptsRoot, - stateRoot, - "InvalidBlockLevelAccessList: Suggested block-level access list missing account changes for 0xdc98b4d0af603b4fb5ccdd840406a0210e5deff8 at index 2.", - withMissingChange: true); + protected static string GetExpectedBalError(BalErrorKind errorKind, bool exactMatch = true) + { + return exactMatch + ? errorKind switch + { + BalErrorKind.IncorrectChange => "InvalidBlockLevelAccessList: Suggested block-level access list contained incorrect changes for 0xdc98b4d0af603b4fb5ccdd840406a0210e5deff8 at index 3.", + BalErrorKind.MissingChange => "InvalidBlockLevelAccessList: Suggested block-level access list missing account changes for 0xdc98b4d0af603b4fb5ccdd840406a0210e5deff8 at index 2.", + BalErrorKind.SurplusChange => "InvalidBlockLevelAccessList: Suggested block-level access list contained surplus changes for 0x65942aaf2c32a1aca4f14e82e94fce91960893a2 at index 2.", + _ => "InvalidBlockLevelAccessList: Suggested block-level access list contained invalid storage reads.", + } + : errorKind switch + { + BalErrorKind.IncorrectChange => "incorrect changes", + BalErrorKind.MissingChange => "missing account changes", + BalErrorKind.SurplusChange => "surplus changes", + _ => "invalid storage reads", + }; + } - [TestCase( - "0x383a5a61b956150bc79762844dc40395c9f85e9caae8930a0de2b9e687902eae", - "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", - "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b")] - public virtual async Task NewPayloadV5_rejects_invalid_BAL_with_surplus_changes_early(string blockHash, string receiptsRoot, string stateRoot) - => await NewPayloadV5( - blockHash, - receiptsRoot, - stateRoot, - "InvalidBlockLevelAccessList: Suggested block-level access list contained surplus changes for 0x65942aaf2c32a1aca4f14e82e94fce91960893a2 at index 2.", - withSurplusChange: true); + [TestCaseSource(nameof(InvalidBalEarlyTestCases))] + public virtual Task NewPayloadV5_rejects_invalid_BAL_early( + string? blockHash, string? receiptsRoot, string? stateRoot, + bool eip8037Enabled, bool useEnginePipeline, BalErrorKind errorKind) + { + bool useManualBlock = !eip8037Enabled && !useEnginePipeline; + string expectedError = GetExpectedBalError(errorKind, exactMatch: useManualBlock); - [TestCase( - "0x66478724575325c99be695cc33d2698b6c87bdc7fe4ee0a54813de367f2bf037", - "0x3d4548dff4e45f6e7838b223bf9476cd5ba4fd05366e8cb4e6c9b65763209569", - "0xd2e92dcdc98864f0cf2dbe7112ed1b0246c401eff3b863e196da0bfb0dec8e3b")] - public virtual async Task NewPayloadV5_rejects_invalid_BAL_with_surplus_reads_early(string blockHash, string receiptsRoot, string stateRoot) - => await NewPayloadV5( - blockHash, - receiptsRoot, - stateRoot, - "InvalidBlockLevelAccessList: Suggested block-level access list contained invalid storage reads.", - withSurplusReads: true); + return useManualBlock + ? NewPayloadV5_via_manual_block(blockHash, receiptsRoot, stateRoot, expectedError, errorKind) + : NewPayloadV5_via_engine_built(blockHash, receiptsRoot, stateRoot, eip8037Enabled, expectedError, errorKind, useSerializedRpc: !useEnginePipeline); + } [Test] [TestCase(null)] - public virtual async Task GetPayloadV6_builds_block_with_BAL(string? auraWithdrawalContractAddress) + public virtual async Task GetPayloadV6_builds_block_with_BAL(string? customWithdrawalContractAddress) { ulong timestamp = 12; TestSpecProvider specProvider = new(Amsterdam.Instance); @@ -366,26 +372,26 @@ public virtual async Task GetPayloadV6_builds_block_with_BAL(string? auraWithdra BlockAccessList bal = Rlp.Decode(new Rlp(res.ExecutionPayload.BlockAccessList)); BlockAccessListBuilder expectedBalBuilder = Build.A.BlockAccessList - .WithAccountChanges([ - Build.An.AccountChanges - .WithAddress(TestItem.AddressA) - .WithBalanceChanges([new(1, new UInt256(Bytes.FromHexString("0x3635c9adc5de9fadf7"), isBigEndian: true))]) - .WithNonceChanges([new(1, 1)]) - .TestObject, - Build.An.AccountChanges - .WithAddress(TestItem.AddressB) - .WithBalanceChanges([new(1, new UInt256(Bytes.FromHexString("0x3635c9adc5dea00001"), isBigEndian: true))]) - .TestObject, - Build.An.AccountChanges - .WithAddress(Address.Zero) - .WithBalanceChanges([new(1, 0x5208)]) - .TestObject, - ]) + .WithAccountChanges(Build.An.AccountChanges + .WithAddress(TestItem.AddressA) + .WithBalanceChanges([ + new(1, new UInt256(Bytes.FromHexString("0x3635c9adc5de9fadf7"), isBigEndian: true)) + ]) + .WithNonceChanges([new(1, 1)]) + .TestObject, Build.An.AccountChanges + .WithAddress(TestItem.AddressB) + .WithBalanceChanges([ + new(1, new UInt256(Bytes.FromHexString("0x3635c9adc5dea00001"), isBigEndian: true)) + ]) + .TestObject, Build.An.AccountChanges + .WithAddress(Address.Zero) + .WithBalanceChanges([new(1, 0x5208)]) + .TestObject) .WithPrecompileChanges(genesis.Header.Hash!, timestamp); - if (auraWithdrawalContractAddress is not null) + if (customWithdrawalContractAddress is not null) { - expectedBalBuilder.WithAccountChanges([new(new Address(auraWithdrawalContractAddress))]); + expectedBalBuilder.WithAccountChanges([new(new Address(customWithdrawalContractAddress))]); } BlockAccessList expected = expectedBalBuilder.TestObject; @@ -478,19 +484,18 @@ private async Task AddNewBlockV6(IEngineRpcModule rpcModule, return payload.ExecutionPayload; } - protected async Task NewPayloadV5( - string blockHash, - string receiptsRoot, - string stateRoot, + /// + /// Tests BAL validation with a manually constructed block via RPC serialization (no EIP-8037). + /// + protected async Task NewPayloadV5_via_manual_block( + string? blockHash = null, + string? receiptsRoot = null, + string? stateRoot = null, string? expectedError = null, - bool withIncorrectChange = false, - bool withSurplusChange = false, - bool withMissingChange = false, - bool withSurplusReads = false, - string? auraWithdrawalContractAddress = null) + BalErrorKind errorKind = BalErrorKind.None, + string? customWithdrawalContractAddress = null) { - using MergeTestBlockchain chain = - await CreateBlockchain(Amsterdam.Instance); + using MergeTestBlockchain chain = await CreateBlockchain(Amsterdam.NoEip8037Instance); IEngineRpcModule rpc = chain.EngineRpcModule; const long gasUsed = 167340; @@ -500,130 +505,16 @@ protected async Task NewPayloadV5( const ulong timestamp = 1000000; Hash256 parentHash = new(chain.BlockTree.HeadHash); - Transaction tx = Build.A.Transaction - .WithTo(TestItem.AddressB) - .WithSenderAddress(TestItem.AddressA) - .WithValue(0) - .WithGasPrice(gasPrice) - .WithGasLimit(gasLimit) - .SignedAndResolved(TestItem.PrivateKeyA) - .TestObject; - - Transaction tx2 = Build.A.Transaction - .WithTo(null) - .WithSenderAddress(TestItem.AddressA) - .WithValue(0) - .WithNonce(1) - .WithGasPrice(gasPrice) - .WithGasLimit(gasLimit) - .WithCode(Eip2935TestConstants.InitCode) - .SignedAndResolved(TestItem.PrivateKeyA) - .TestObject; - - // Store followed by revert should undo storage change - byte[] code = Prepare.EvmCode - .PushData(1) - .PushData(1) - .SSTORE() - .Op(Instruction.PUSH0) - .Op(Instruction.PUSH0) - .REVERT() - .Done; - Transaction tx3 = Build.A.Transaction - .WithTo(null) - .WithSenderAddress(TestItem.AddressA) - .WithValue(0) - .WithNonce(2) - .WithGasPrice(gasPrice) - .WithGasLimit(gasLimit) - .WithCode(code) - .SignedAndResolved(TestItem.PrivateKeyA) - .TestObject; - - Withdrawal withdrawal = new() - { - Index = 0, - ValidatorIndex = 0, - Address = TestItem.AddressD, - AmountInGwei = 1 - }; + (Transaction tx, Transaction tx2, Transaction tx3, Withdrawal withdrawal) = BuildTestTransactionsAndWithdrawal(gasPrice, gasLimit); Address newContractAddress = ContractAddress.From(TestItem.AddressA, 1); Address newContractAddress2 = ContractAddress.From(TestItem.AddressA, 2); - UInt256 eip4788Slot1 = timestamp % Eip4788Constants.RingBufferSize; - UInt256 eip4788Slot2 = (timestamp % Eip4788Constants.RingBufferSize) + Eip4788Constants.RingBufferSize; - - StorageChange parentHashStorageChange = new(0, new UInt256(parentHash.BytesToArray(), isBigEndian: true)); - StorageChange timestampStorageChange = new(0, 0xF4240); - UInt256 accountBalance = chain.StateReader.GetBalance(chain.BlockTree.Head!.Header, TestItem.AddressA); UInt256 addressABalance = accountBalance - gasPrice * GasCostOf.Transaction; UInt256 addressABalance2 = accountBalance - gasPrice * gasUsedBeforeFinal; UInt256 addressABalance3 = accountBalance - gasPrice * gasUsed; - AccountChangesBuilder newContractAccount = Build.An.AccountChanges - .WithAddress(newContractAddress) - .WithNonceChanges([new(2, 1)]) - .WithCodeChanges([new(2, Eip2935TestConstants.Code)]); - - if (withIncorrectChange) - { - newContractAccount = newContractAccount.WithBalanceChanges([new(3, 1.GWei)]); // incorrect change - } - - if (withSurplusReads) - { - for (ulong i = 0; i < 100; i++) - { - newContractAccount = newContractAccount.WithStorageReads(new UInt256(i)); - } - } - - BlockAccessListBuilder expectedBalBuilder = Build.A.BlockAccessList - .WithAccountChanges( - Build.An.AccountChanges - .WithAddress(TestItem.AddressA) - .WithBalanceChanges([new(1, addressABalance), new(2, addressABalance2), new(3, addressABalance3)]) - .WithNonceChanges([new(1, 1), new(2, 2), new(3, 3)]) - .TestObject, - new(TestItem.AddressB), - Build.An.AccountChanges - .WithAddress(TestItem.AddressE) - .WithBalanceChanges([new(1, new UInt256(GasCostOf.Transaction * gasPrice)), new(2, new UInt256(gasUsedBeforeFinal * gasPrice)), new(3, new UInt256(gasUsed * gasPrice))]) - .TestObject, - Build.An.AccountChanges - .WithAddress(newContractAddress2) - .WithStorageReads(1) - .TestObject) - .WithPrecompileChanges(parentHash, timestamp); - - if (!withMissingChange) - { - expectedBalBuilder.WithAccountChanges(newContractAccount.TestObject); - } - - if (withSurplusChange) - { - expectedBalBuilder.WithAccountChanges( - Build.An.AccountChanges - .WithAddress(TestItem.AddressF) - .WithNonceChanges([new(2, 5)]) - .TestObject); - } - - if (auraWithdrawalContractAddress is not null) - { - expectedBalBuilder.WithAccountChanges([new(new Address(auraWithdrawalContractAddress)), new(Address.SystemUser)]); - } - else - { - expectedBalBuilder.WithAccountChanges([Build.An.AccountChanges - .WithAddress(TestItem.AddressD) - .WithBalanceChanges([new(4, 1.GWei)]) - .TestObject]); - } - Block block = new( new( parentHash, @@ -641,17 +532,17 @@ protected async Task NewPayloadV5( BaseFeePerGas = 0, Bloom = Bloom.Empty, GasUsed = gasUsed, - Hash = new(blockHash), + Hash = new(blockHash!), MixHash = Keccak.Zero, ParentBeaconBlockRoot = Keccak.Zero, - ReceiptsRoot = new(receiptsRoot), - StateRoot = new(stateRoot), + ReceiptsRoot = new(receiptsRoot!), + StateRoot = new(stateRoot!), SlotNumber = 1 }, [tx, tx2, tx3], [], [withdrawal], - expectedBalBuilder.TestObject); + CreateBlockAccessList()); string response = await RpcTest.TestSerializedRequest(rpc, "engine_newPayloadV5", chain.JsonSerializer.Serialize(ExecutionPayloadV4.Create(block)), "[]", Keccak.Zero.ToString(true), "[]"); @@ -691,5 +582,330 @@ protected async Task NewPayloadV5( }))); } } + + BlockAccessList CreateBlockAccessList() + { + AccountChangesBuilder newContractAccount = Build.An.AccountChanges + .WithAddress(newContractAddress) + .WithNonceChanges([new(2, 1)]) + .WithCodeChanges([new(2, Eip2935TestConstants.Code)]); + + if (errorKind is BalErrorKind.IncorrectChange) + { + newContractAccount = newContractAccount.WithBalanceChanges([new(3, 1.GWei)]); // incorrect change + } + + if (errorKind is BalErrorKind.SurplusReads) + { + for (ulong i = 0; i < 100; i++) + { + newContractAccount = newContractAccount.WithStorageReads(new UInt256(i)); + } + } + + BlockAccessListBuilder expectedBalBuilder = Build.A.BlockAccessList + .WithAccountChanges( + Build.An.AccountChanges + .WithAddress(TestItem.AddressA) + .WithBalanceChanges([new(1, addressABalance), new(2, addressABalance2), new(3, addressABalance3)]) + .WithNonceChanges([new(1, 1), new(2, 2), new(3, 3)]) + .TestObject, + new(TestItem.AddressB), + Build.An.AccountChanges + .WithAddress(TestItem.AddressE) + .WithBalanceChanges([new(1, new UInt256(GasCostOf.Transaction * gasPrice)), new(2, new UInt256(gasUsedBeforeFinal * gasPrice)), new(3, new UInt256(gasUsed * gasPrice))]) + .TestObject, + Build.An.AccountChanges + .WithAddress(newContractAddress2) + .WithStorageReads(1) + .TestObject) + .WithPrecompileChanges(parentHash, timestamp); + + if (errorKind is not BalErrorKind.MissingChange) + { + expectedBalBuilder.WithAccountChanges(newContractAccount.TestObject); + } + + if (errorKind is BalErrorKind.SurplusChange) + { + expectedBalBuilder.WithAccountChanges( + Build.An.AccountChanges + .WithAddress(TestItem.AddressF) + .WithNonceChanges([new(2, 5)]) + .TestObject); + } + + if (customWithdrawalContractAddress is not null) + { + expectedBalBuilder.WithAccountChanges([new(new Address(customWithdrawalContractAddress)), new(Address.SystemUser)]); + } + else + { + expectedBalBuilder.WithAccountChanges(Build.An.AccountChanges + .WithAddress(TestItem.AddressD) + .WithBalanceChanges([new(4, 1.GWei)]) + .TestObject); + } + + return expectedBalBuilder.TestObject; + } + } + + /// + /// Tests BAL validation with an engine-built block (via forkchoiceUpdated + getPayload), + /// asserting via either typed API or serialized RPC. + /// + private async Task NewPayloadV5_via_engine_built( + string? expectedBlockHash = null, + string? expectedReceiptsRoot = null, + string? expectedStateRoot = null, + bool eip8037Enabled = true, + string? expectedError = null, + BalErrorKind errorKind = BalErrorKind.None, + bool useSerializedRpc = false) + { + IReleaseSpec spec = eip8037Enabled ? Amsterdam.Instance : Amsterdam.NoEip8037Instance; + (MergeTestBlockchain chain, ExecutionPayloadV4 payload) = await BuildTestBlockViaEngine(spec, expectedBlockHash, expectedReceiptsRoot, expectedStateRoot, errorKind); + using (chain) + { + IEngineRpcModule rpc = chain.EngineRpcModule; + if (useSerializedRpc) + { + string response = await RpcTest.TestSerializedRequest(rpc, "engine_newPayloadV5", + chain.JsonSerializer.Serialize(payload), "[]", Keccak.Zero.ToString(true), "[]"); + JsonRpcSuccessResponse successResponse = chain.JsonSerializer.Deserialize(response); + Assert.That(successResponse, Is.Not.Null); + + if (expectedError is null) + { + Assert.That(response, Is.EqualTo(chain.JsonSerializer.Serialize(new JsonRpcSuccessResponse + { + Id = successResponse.Id, + Result = new PayloadStatusV1 + { + LatestValidHash = payload.BlockHash, + Status = PayloadStatus.Valid, + ValidationError = null + } + }))); + } + else + { + Assert.That(response, Does.Contain(PayloadStatus.Invalid)); + Assert.That(response, Does.Contain(expectedError)); + } + } + else + { + ResultWrapper result = await rpc.engine_newPayloadV5(payload, [], Keccak.Zero, []); + Assert.That(result.Data, Is.Not.Null, $"engine_newPayloadV5 returned error instead of payload status: {result.Result} (code {result.ErrorCode})"); + if (expectedError is null) + { + Assert.That(result.Data.Status, Is.EqualTo(PayloadStatus.Valid)); + Assert.That(result.Data.LatestValidHash, Is.EqualTo(payload.BlockHash)); + } + else + { + Assert.That(result.Data.Status, Is.EqualTo(PayloadStatus.Invalid)); + Assert.That(result.Data.ValidationError, Does.Contain(expectedError)); + } + } + } + } + + private static (Transaction tx, Transaction tx2, Transaction tx3, Withdrawal withdrawal) BuildTestTransactionsAndWithdrawal(ulong gasPrice, long gasLimit) + { + Transaction tx = Build.A.Transaction + .WithTo(TestItem.AddressB) + .WithSenderAddress(TestItem.AddressA) + .WithValue(0) + .WithGasPrice(gasPrice) + .WithGasLimit(gasLimit) + .SignedAndResolved(TestItem.PrivateKeyA) + .TestObject; + + Transaction tx2 = Build.A.Transaction + .WithTo(null) + .WithSenderAddress(TestItem.AddressA) + .WithValue(0) + .WithNonce(1) + .WithGasPrice(gasPrice) + .WithGasLimit(gasLimit) + .WithCode(Eip2935TestConstants.InitCode) + .SignedAndResolved(TestItem.PrivateKeyA) + .TestObject; + + // Store followed by revert should undo storage change + byte[] code = Prepare.EvmCode + .PushData(1) + .PushData(1) + .SSTORE() + .Op(Instruction.PUSH0) + .Op(Instruction.PUSH0) + .REVERT() + .Done; + Transaction tx3 = Build.A.Transaction + .WithTo(null) + .WithSenderAddress(TestItem.AddressA) + .WithValue(0) + .WithNonce(2) + .WithGasPrice(gasPrice) + .WithGasLimit(gasLimit) + .WithCode(code) + .SignedAndResolved(TestItem.PrivateKeyA) + .TestObject; + + Withdrawal withdrawal = new() + { + Index = 0, + ValidatorIndex = 0, + Address = TestItem.AddressD, + AmountInGwei = 1 + }; + + return (tx, tx2, tx3, withdrawal); + } + + /// + /// Builds a test block via engine pipeline (fcu + getPayload), optionally modifying the BAL for error testing. + /// Caller must dispose the returned chain. + /// + private async Task<(MergeTestBlockchain chain, ExecutionPayloadV4 payload)> BuildTestBlockViaEngine( + IReleaseSpec spec, + string? expectedBlockHash = null, + string? expectedReceiptsRoot = null, + string? expectedStateRoot = null, + BalErrorKind errorKind = BalErrorKind.None) + { + MergeTestBlockchain chain = await CreateBlockchain(spec); + IEngineRpcModule rpc = chain.EngineRpcModule; + + const ulong gasPrice = 2; + const long gasLimit = 100000; + const ulong timestamp = 1000000; + const ulong slotNumber = 1; + + (Transaction tx, Transaction tx2, Transaction tx3, Withdrawal withdrawal) = BuildTestTransactionsAndWithdrawal(gasPrice, gasLimit); + + chain.TxPool.SubmitTx(tx, TxHandlingOptions.None); + chain.TxPool.SubmitTx(tx2, TxHandlingOptions.None); + chain.TxPool.SubmitTx(tx3, TxHandlingOptions.None); + + Hash256 parentHash = chain.BlockTree.HeadHash; + PayloadAttributes payloadAttributes = new() + { + Timestamp = timestamp, + PrevRandao = Keccak.Zero, + SuggestedFeeRecipient = TestItem.AddressE, + ParentBeaconBlockRoot = Keccak.Zero, + Withdrawals = [withdrawal], + SlotNumber = slotNumber + }; + + ForkchoiceStateV1 fcuState = new(parentHash, parentHash, parentHash); + Task blockImprovementWait = chain.WaitForImprovedBlock(parentHash); + ResultWrapper fcuResponse = await rpc.engine_forkchoiceUpdatedV4(fcuState, payloadAttributes); + Assert.That(fcuResponse.Result.ResultType, Is.EqualTo(ResultType.Success)); + await blockImprovementWait; + + byte[] payloadId = Bytes.FromHexString(fcuResponse.Data.PayloadId!); + ResultWrapper payloadResult = await rpc.engine_getPayloadV6(payloadId); + Assert.That(payloadResult.Data, Is.Not.Null); + ExecutionPayloadV4 payload = payloadResult.Data!.ExecutionPayload; + + if (expectedBlockHash is not null) + Assert.That(payload.BlockHash.ToString(), Is.EqualTo(expectedBlockHash), "Engine-built block hash mismatch"); + if (expectedReceiptsRoot is not null) + Assert.That(payload.ReceiptsRoot.ToString(), Is.EqualTo(expectedReceiptsRoot), "Engine-built receipts root mismatch"); + if (expectedStateRoot is not null) + Assert.That(payload.StateRoot.ToString(), Is.EqualTo(expectedStateRoot), "Engine-built state root mismatch"); + + if (errorKind is BalErrorKind.None) + return (chain, payload); + + // Apply BAL modifications for error testing + payload.ExecutionRequests = payloadResult.Data!.ExecutionRequests; + BlockDecodingResult blockResult = payload.TryGetBlock(); + Block block = blockResult.Block!; + BlockAccessList validBal = block.BlockAccessList!; + + SortedDictionary modifiedAccounts = new(); + Address senderAddress = TestItem.AddressA; + + BlockAccessList modifiedBal = CreateBlockAccessList(); + byte[] modifiedBalRlp = Rlp.Encode(modifiedBal).Bytes; + block.BlockAccessList = modifiedBal; + block.EncodedBlockAccessList = modifiedBalRlp; + block.Header.BlockAccessListHash = new Hash256(ValueKeccak.Compute(modifiedBalRlp).Bytes); + block.Header.Hash = block.Header.CalculateHash(); + + return (chain, ExecutionPayloadV4.Create(block)); + + BlockAccessList CreateBlockAccessList() + { + foreach (AccountChanges ac in validBal.AccountChanges) + { + if (errorKind is not BalErrorKind.MissingChange || ac.Address != senderAddress) + { + modifiedAccounts[ac.Address] = CloneAccountChanges(ac); + } + } + + if (errorKind is BalErrorKind.IncorrectChange) + { + modifiedAccounts[senderAddress] = CloneAccountChanges( + validBal.GetAccountChanges(senderAddress)!, + bc => bc.BlockAccessIndex == 1 ? new BalanceChange(1, bc.PostBalance + 1) : bc); + } + + if (errorKind is BalErrorKind.SurplusChange) + { + SortedList fakeNonce = new() { { 1, new NonceChange(1, 5) } }; + modifiedAccounts[TestItem.AddressF] = new AccountChanges( + TestItem.AddressF, new(), new SortedSet(), new(), fakeNonce, new()); + } + + if (errorKind is BalErrorKind.SurplusReads) + { + AccountChanges entry = modifiedAccounts[senderAddress]; + for (ulong i = 1_000_000; i < 1_000_100; i++) + entry.AddStorageRead(new UInt256(i)); + } + + BlockAccessList blockAccessList = new(modifiedAccounts); + return blockAccessList; + } + } + + private static AccountChanges CloneAccountChanges(AccountChanges ac, Func? balanceModifier = null) + { + SortedList storageChanges = new(); + foreach (SlotChanges sc in ac.StorageChanges) + { + SortedList changes = new(); + foreach (KeyValuePair kvp in sc.Changes) + changes.Add(kvp.Key, kvp.Value); + + storageChanges.Add(sc.Slot, sc with { Changes = changes }); + } + + SortedSet storageReads = new(ac.StorageReads); + + SortedList balanceChanges = new(); + foreach (BalanceChange bc in ac.BalanceChanges) + { + BalanceChange modified = balanceModifier?.Invoke(bc) ?? bc; + balanceChanges.Add(modified.BlockAccessIndex, modified); + } + + SortedList nonceChanges = new(); + foreach (NonceChange nc in ac.NonceChanges) + nonceChanges.Add(nc.BlockAccessIndex, nc); + + SortedList codeChanges = new(); + foreach (CodeChange cc in ac.CodeChanges) + codeChanges.Add(cc.BlockAccessIndex, cc); + + return new AccountChanges(ac.Address, storageChanges, storageReads, balanceChanges, nonceChanges, codeChanges); } } diff --git a/src/Nethermind/Nethermind.Optimism/OptimismTransactionProcessor.cs b/src/Nethermind/Nethermind.Optimism/OptimismTransactionProcessor.cs index 2538b74c1ef..74a89d49b10 100644 --- a/src/Nethermind/Nethermind.Optimism/OptimismTransactionProcessor.cs +++ b/src/Nethermind/Nethermind.Optimism/OptimismTransactionProcessor.cs @@ -173,7 +173,7 @@ protected override void PayFees(Transaction tx, BlockHeader header, IReleaseSpec } protected override GasConsumed Refund(Transaction tx, BlockHeader header, IReleaseSpec spec, ExecutionOptions opts, - in TransactionSubstate substate, in EthereumGasPolicy unspentGas, in UInt256 gasPrice, int codeInsertRefunds, EthereumGasPolicy floorGas) + in TransactionSubstate substate, in EthereumGasPolicy unspentGas, in UInt256 gasPrice, int codeInsertRefunds, EthereumGasPolicy floorGas, in EthereumGasPolicy intrinsicGasStandard) { // if deposit: skip refunds, skip tipping coinbase // Regolith changes this behaviour to report the actual gasUsed instead of always reporting all gas used. @@ -185,6 +185,6 @@ protected override GasConsumed Refund(Transaction tx, BlockHeader header, IRelea return gas; } - return base.Refund(tx, header, spec, opts, substate, unspentGas, gasPrice, codeInsertRefunds, floorGas); + return base.Refund(tx, header, spec, opts, substate, unspentGas, gasPrice, codeInsertRefunds, floorGas, intrinsicGasStandard); } } diff --git a/src/Nethermind/Nethermind.Specs.Test/ChainSpecStyle/ChainSpecBasedSpecProviderTests.cs b/src/Nethermind/Nethermind.Specs.Test/ChainSpecStyle/ChainSpecBasedSpecProviderTests.cs index 130b9a4c967..fc365c1872b 100644 --- a/src/Nethermind/Nethermind.Specs.Test/ChainSpecStyle/ChainSpecBasedSpecProviderTests.cs +++ b/src/Nethermind/Nethermind.Specs.Test/ChainSpecStyle/ChainSpecBasedSpecProviderTests.cs @@ -799,7 +799,7 @@ public void Eip2200_is_not_set_correctly_indirectly_after_disabling_eip1283() } [Test] - public void Eip150_and_Eip2537_fork_by_block_number() + public void Max_code_size_forks_by_block_number() { ChainSpec chainSpec = new() { @@ -828,7 +828,7 @@ public void Eip150_and_Eip2537_fork_by_block_number() } [Test] - public void Eip150_and_Eip2537_fork_by_timestamp() + public void Max_code_size_forks_by_timestamp() { ChainSpec chainSpec = new() { @@ -836,6 +836,7 @@ public void Eip150_and_Eip2537_fork_by_timestamp() { MaxCodeSizeTransitionTimestamp = 10, Eip2537TransitionTimestamp = 20, + Eip7954TransitionTimestamp = 30, MaxCodeSize = 1 }, EngineChainSpecParametersProvider = TestChainSpecParametersProvider.NethDev @@ -853,6 +854,11 @@ public void Eip150_and_Eip2537_fork_by_timestamp() Assert.That(provider.GetSpec((100, 19)).IsEip2537Enabled, Is.False); Assert.That(provider.GetSpec((100, 20)).IsEip2537Enabled, Is.True); Assert.That(provider.GetSpec((100, 21)).IsEip2537Enabled, Is.True); + Assert.That(provider.GetSpec((100, 29)).IsEip7954Enabled, Is.False); + Assert.That(provider.GetSpec((100, 29)).MaxCodeSize, Is.EqualTo(1)); + Assert.That(provider.GetSpec((100, 30)).IsEip7954Enabled, Is.True); + Assert.That(provider.GetSpec((100, 30)).MaxCodeSize, Is.EqualTo(CodeSizeConstants.MaxCodeSizeEip7954)); + Assert.That(provider.GetSpec((100, 30)).MaxInitCodeSize, Is.EqualTo(2L * CodeSizeConstants.MaxCodeSizeEip7954)); } } diff --git a/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs b/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs index c44b8dc8645..c47b18d5417 100644 --- a/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Specs.Test/OverridableReleaseSpec.cs @@ -115,15 +115,17 @@ public class OverridableReleaseSpec(IReleaseSpec spec) : IReleaseSpec public bool IsEip6110Enabled { get; set; } = spec.IsEip6110Enabled; public Address? DepositContractAddress { get; set; } = spec.DepositContractAddress; public bool IsEip7594Enabled { get; set; } = spec.IsEip7594Enabled; - Array? IReleaseSpec.EvmInstructionsNoTrace { get => spec.EvmInstructionsNoTrace; set => spec.EvmInstructionsNoTrace = value; } - Array? IReleaseSpec.EvmInstructionsTraced { get => spec.EvmInstructionsTraced; set => spec.EvmInstructionsTraced = value; } + Array? IReleaseSpec.EvmInstructionsNoTrace { get => field ?? spec.EvmInstructionsNoTrace; set; } + Array? IReleaseSpec.EvmInstructionsTraced { get => field ?? spec.EvmInstructionsTraced; set; } public bool IsEip7939Enabled { get; set; } = spec.IsEip7939Enabled; public bool IsEip7907Enabled { get; set; } = spec.IsEip7907Enabled; public bool IsRip7728Enabled { get; set; } = spec.IsRip7728Enabled; public bool IsEip7928Enabled { get; set; } = spec.IsEip7928Enabled; + public bool IsEip8037Enabled { get; set; } = spec.IsEip8037Enabled; public bool IsEip7708Enabled { get; set; } = spec.IsEip7708Enabled; public bool IsEip7778Enabled { get; set; } = spec.IsEip7778Enabled; public bool IsEip7843Enabled => spec.IsEip7843Enabled; + public bool IsEip7954Enabled { get; set; } = spec.IsEip7954Enabled; 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 ac61770fb59..68b3c0389da 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainParameters.cs @@ -175,9 +175,11 @@ public class ChainParameters #endregion public ulong? Rip7728TransitionTimestamp { get; set; } + public ulong? Eip8037TransitionTimestamp { get; set; } public ulong? Eip7928TransitionTimestamp { get; set; } public ulong? Eip7708TransitionTimestamp { get; set; } public ulong? Eip8024TransitionTimestamp { get; set; } public ulong? Eip7843TransitionTimestamp { get; set; } + public ulong? Eip7954TransitionTimestamp { get; set; } } diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs index 9cec9811900..8982eeb3469 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecBasedSpecProvider.cs @@ -294,6 +294,7 @@ protected virtual ReleaseSpec CreateReleaseSpec(ChainSpec chainSpec, long releas releaseSpec.IsEip7939Enabled = (chainSpec.Parameters.Eip7939TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; releaseSpec.IsRip7728Enabled = (chainSpec.Parameters.Rip7728TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; + releaseSpec.IsEip8037Enabled = (chainSpec.Parameters.Eip8037TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; releaseSpec.IsEip7778Enabled = (chainSpec.Parameters.Eip7778TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; releaseSpec.IsEip7928Enabled = (chainSpec.Parameters.Eip7928TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; @@ -301,6 +302,12 @@ protected virtual ReleaseSpec CreateReleaseSpec(ChainSpec chainSpec, long releas releaseSpec.IsEip7708Enabled = (chainSpec.Parameters.Eip7708TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; + releaseSpec.IsEip7954Enabled = (chainSpec.Parameters.Eip7954TransitionTimestamp ?? ulong.MaxValue) <= releaseStartTimestamp; + if (releaseSpec.IsEip7954Enabled && !releaseSpec.IsEip7907Enabled) + { + releaseSpec.MaxCodeSize = CodeSizeConstants.MaxCodeSizeEip7954; + } + 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 ca2ca7bb98b..073d41ff0aa 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/ChainSpecLoader.cs @@ -201,12 +201,14 @@ bool GetForInnerPathExistence(KeyValuePair o) => Eip7778TransitionTimestamp = chainSpecJson.Params.Eip7778TransitionTimestamp, Rip7728TransitionTimestamp = chainSpecJson.Params.Rip7728TransitionTimestamp, + Eip8037TransitionTimestamp = chainSpecJson.Params.Eip8037TransitionTimestamp, Eip7928TransitionTimestamp = chainSpecJson.Params.Eip7928TransitionTimestamp, Eip7708TransitionTimestamp = chainSpecJson.Params.Eip7708TransitionTimestamp, Eip8024TransitionTimestamp = chainSpecJson.Params.Eip8024TransitionTimestamp, Eip7843TransitionTimestamp = chainSpecJson.Params.Eip7843TransitionTimestamp, + Eip7954TransitionTimestamp = chainSpecJson.Params.Eip7954TransitionTimestamp, }; chainSpec.Parameters.Eip152Transition ??= GetTransitionForExpectedPricing("blake2_f", "price.blake2_f.gas_per_round", 1); diff --git a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs index d2f6772949b..538ca967971 100644 --- a/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs +++ b/src/Nethermind/Nethermind.Specs/ChainSpecStyle/Json/ChainSpecParamsJson.cs @@ -176,10 +176,12 @@ public class ChainSpecParamsJson public ulong? Eip7594TransitionTimestamp { get; set; } public ulong? Eip7939TransitionTimestamp { get; set; } public ulong? Rip7728TransitionTimestamp { get; set; } + public ulong? Eip8037TransitionTimestamp { get; set; } public ulong? Eip7778TransitionTimestamp { get; set; } public ulong? Eip7928TransitionTimestamp { get; set; } public ulong? Eip7708TransitionTimestamp { get; set; } public ulong? Eip8024TransitionTimestamp { get; set; } public ulong? Eip7843TransitionTimestamp { get; set; } + public ulong? Eip7954TransitionTimestamp { get; set; } } diff --git a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs index 06ac11b2671..0129235a309 100644 --- a/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs +++ b/src/Nethermind/Nethermind.Specs/Forks/25_Amsterdam.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Threading; +using Nethermind.Core; using Nethermind.Core.Specs; namespace Nethermind.Specs.Forks; @@ -9,17 +10,22 @@ namespace Nethermind.Specs.Forks; public class Amsterdam : BPO5 { private static IReleaseSpec _instance; + private static IReleaseSpec _noEip8037Instance; public Amsterdam() { Name = "Amsterdam"; + IsEip8037Enabled = true; IsEip7778Enabled = true; IsEip7928Enabled = true; IsEip7708Enabled = true; IsEip8024Enabled = true; IsEip7843Enabled = true; + IsEip7954Enabled = true; + MaxCodeSize = CodeSizeConstants.MaxCodeSizeEip7954; Released = false; } public new static IReleaseSpec Instance => LazyInitializer.EnsureInitialized(ref _instance, static () => new Amsterdam()); + public static IReleaseSpec NoEip8037Instance => LazyInitializer.EnsureInitialized(ref _noEip8037Instance, static () => new Amsterdam { IsEip8037Enabled = false }); } diff --git a/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs b/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs index 85b73d30713..0a858c9898d 100644 --- a/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Specs/ReleaseSpec.cs @@ -167,10 +167,12 @@ public virtual FrozenSet BuildPrecompilesCache() } public bool IsEip7928Enabled { get; set; } + public bool IsEip8037Enabled { get; set; } public bool IsEip7778Enabled { get; set; } public bool IsEip7843Enabled { get; set; } public bool IsEip7708Enabled { get; set; } + public bool IsEip7954Enabled { get; set; } private ReleaseSpec? _systemSpec; diff --git a/src/Nethermind/Nethermind.State/ParallelWorldState.cs b/src/Nethermind/Nethermind.State/ParallelWorldState.cs index 55b66dcefa8..7929c143234 100644 --- a/src/Nethermind/Nethermind.State/ParallelWorldState.cs +++ b/src/Nethermind/Nethermind.State/ParallelWorldState.cs @@ -46,9 +46,6 @@ public override void AddToBalance(Address address, in UInt256 balanceChange, IRe } } - public override bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balanceChange, IReleaseSpec spec) - => AddToBalanceAndCreateIfNotExists(address, balanceChange, spec, out _); - public override bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) { bool res = _innerWorldState.AddToBalanceAndCreateIfNotExists(address, balanceChange, spec, out oldBalance); @@ -74,9 +71,6 @@ public override ReadOnlySpan Get(in StorageCell storageCell) return _innerWorldState.Get(storageCell); } - public override void IncrementNonce(Address address, UInt256 delta) - => IncrementNonce(address, delta, out _); - public override void IncrementNonce(Address address, UInt256 delta, out UInt256 oldNonce) { _innerWorldState.IncrementNonce(address, delta, out oldNonce); @@ -135,6 +129,7 @@ public bool HasCode(Address address) return _innerWorldState.HasCode(address); } + public override ref readonly ValueHash256 GetCodeHash(Address address) { AddAccountRead(address); @@ -147,9 +142,6 @@ public override ref readonly ValueHash256 GetCodeHash(Address address) return _innerWorldState.GetCode(address); } - public override void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec) - => SubtractFromBalance(address, balanceChange, spec, out _); - public override void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) { _innerWorldState.SubtractFromBalance(address, balanceChange, spec, out oldBalance); @@ -367,6 +359,7 @@ public void SetBlockAccessList(Block block, IReleaseSpec spec) // for testing internal IWorldState Inner => _innerWorldState; + private static bool HasNoChanges(in ChangeAtIndex c) => c.BalanceChange is null && c.NonceChange is null && diff --git a/src/Nethermind/Nethermind.State/StateProvider.cs b/src/Nethermind/Nethermind.State/StateProvider.cs index 5bd74371f41..53256075d8c 100644 --- a/src/Nethermind/Nethermind.State/StateProvider.cs +++ b/src/Nethermind/Nethermind.State/StateProvider.cs @@ -41,7 +41,7 @@ internal class StateProvider(ILogManager logManager) : IJournal private readonly ClockKeyCacheNonConcurrent _blockCodeInsertFilter = new(256); private readonly Dictionary _blockChanges = new(4_096); - private readonly List _keptInCache = new(); + private readonly List _keptInCache = []; private readonly ILogger _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); private Dictionary? _codeBatch; private Dictionary.AlternateLookup _codeBatchAlternate; diff --git a/src/Nethermind/Nethermind.State/WorldState.cs b/src/Nethermind/Nethermind.State/WorldState.cs index fdbebbdbbf6..ac641379967 100644 --- a/src/Nethermind/Nethermind.State/WorldState.cs +++ b/src/Nethermind/Nethermind.State/WorldState.cs @@ -210,20 +210,11 @@ public bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balance DebugGuardInScope(); return _stateProvider.AddToBalanceAndCreateIfNotExists(address, balanceChange, spec, out oldBalance); } - public bool AddToBalanceAndCreateIfNotExists(Address address, in UInt256 balanceChange, IReleaseSpec spec) - => AddToBalanceAndCreateIfNotExists(address, balanceChange, spec, out _); public void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec, out UInt256 oldBalance) { DebugGuardInScope(); _stateProvider.SubtractFromBalance(address, balanceChange, spec, out oldBalance); } - public void SubtractFromBalance(Address address, in UInt256 balanceChange, IReleaseSpec spec) - { - DebugGuardInScope(); - _stateProvider.SubtractFromBalance(address, balanceChange, spec, out _); - } - public void IncrementNonce(Address address, UInt256 delta) - => IncrementNonce(address, delta, out _); public void IncrementNonce(Address address, UInt256 delta, out UInt256 oldNonce) { DebugGuardInScope();