diff --git a/.github/workflows/nethermind-tests-checked.yml b/.github/workflows/nethermind-tests-checked.yml
index 18830556fba5..39d31b11abba 100644
--- a/.github/workflows/nethermind-tests-checked.yml
+++ b/.github/workflows/nethermind-tests-checked.yml
@@ -187,6 +187,7 @@ jobs:
env:
TEST_CHUNK: ${{ matrix.test.chunk }}
DOTNET_EnableHWIntrinsic: ${{ matrix.variant.hw-intrinsic }}
+ TEST_SKIP_HEAVY: 1
run: |
dotnet test --project ${{ matrix.test.project }}.csproj -c release \
${{ matrix.variant.dotnet-args }}
diff --git a/.github/workflows/nethermind-tests-flat.yml b/.github/workflows/nethermind-tests-flat.yml
index 85728a30d005..1b6e834a47fe 100644
--- a/.github/workflows/nethermind-tests-flat.yml
+++ b/.github/workflows/nethermind-tests-flat.yml
@@ -96,6 +96,7 @@ jobs:
working-directory: src/Nethermind/${{ matrix.project }}
env:
TEST_CHUNK: ${{ matrix.chunk }}
+ TEST_SKIP_HEAVY: 1
run: |
dotnet test --project ${{ matrix.project }}.csproj -c release
diff --git a/.github/workflows/nethermind-tests.yml b/.github/workflows/nethermind-tests.yml
index e01f1a83bf66..a1ee7729fed3 100644
--- a/.github/workflows/nethermind-tests.yml
+++ b/.github/workflows/nethermind-tests.yml
@@ -282,6 +282,7 @@ jobs:
working-directory: src/Nethermind/${{ matrix.test.project }}
env:
TEST_CHUNK: ${{ matrix.test.chunk }}
+ TEST_SKIP_HEAVY: 1
run: |
set +e
dotnet test --project ${{ matrix.test.project }}.csproj -c release
diff --git a/.gitignore b/.gitignore
index 11527d8ecd5d..27aafb566091 100644
--- a/.gitignore
+++ b/.gitignore
@@ -454,3 +454,4 @@ keystore/
# Worktrees
.worktrees/
+/.dotnet-cli
diff --git a/cspell.json b/cspell.json
index d33bd8201c5f..f82e47eddfee 100644
--- a/cspell.json
+++ b/cspell.json
@@ -6,6 +6,7 @@
"ignoreRandomStrings": true,
"unknownWords": "report-common-typos",
"ignorePaths": [
+ ".gitignore",
"**/*.json",
"**/*.zst",
"**/artifacts/**",
diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/AmsterdamTestFixture.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/AmsterdamTestFixture.cs
index f02cf1d8901b..e95b54d24ccd 100644
--- a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/AmsterdamTestFixture.cs
+++ b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/AmsterdamTestFixture.cs
@@ -13,13 +13,17 @@ namespace Ethereum.Blockchain.Pyspec.Test.Amsterdam;
///
/// Generic base for Amsterdam EIP blockchain tests.
/// Wildcard is read from on .
+/// In CI, only runs on Linux x64 to stay within the job timeout budget.
///
[TestFixture]
[Parallelizable(ParallelScope.All)]
public abstract class AmsterdamBlockChainTestFixture : BlockchainTestBase
{
+ [SetUp]
+ public void SkipInCiOnSlowRunners() => CiRunnerGuard.SkipIfNotLinuxX64();
+
[TestCaseSource(nameof(LoadTests))]
- public async Task Test(BlockchainTest test) => await RunTest(test);
+ public async Task Test(BlockchainTest test) => (await RunTest(test)).Pass.Should().BeTrue();
public static IEnumerable LoadTests() =>
new TestsSourceLoader(new LoadPyspecTestsStrategy
@@ -29,6 +33,29 @@ public static IEnumerable LoadTests() =>
}, "fixtures/blockchain_tests/for_amsterdam", typeof(TSelf).GetCustomAttribute()!.Wildcard).LoadTests();
}
+///
+/// Generic base for Amsterdam EIP engine blockchain tests.
+/// Wildcard is read from on .
+/// In CI, only runs on Linux x64 to stay within the job timeout budget.
+///
+[TestFixture]
+[Parallelizable(ParallelScope.All)]
+public abstract class AmsterdamEngineBlockChainTestFixture : BlockchainTestBase
+{
+ [SetUp]
+ public void SkipInCiOnSlowRunners() => CiRunnerGuard.SkipIfNotLinuxX64();
+
+ [TestCaseSource(nameof(LoadTests))]
+ public async Task Test(BlockchainTest test) => (await RunTest(test)).Pass.Should().BeTrue();
+
+ public static IEnumerable LoadTests() =>
+ new TestsSourceLoader(new LoadPyspecTestsStrategy
+ {
+ ArchiveVersion = Constants.BalArchiveVersion,
+ ArchiveName = Constants.BalArchiveName
+ }, "fixtures/blockchain_tests_engine/for_amsterdam", typeof(TSelf).GetCustomAttribute()!.Wildcard).LoadTests();
+}
+
///
/// Generic base for Amsterdam EIP state tests.
/// Wildcard is read from on .
diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Tests.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Tests.cs
index e05cb5312747..4d7dc1dfa41f 100644
--- a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Tests.cs
+++ b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Amsterdam/Tests.cs
@@ -8,24 +8,45 @@ namespace Ethereum.Blockchain.Pyspec.Test.Amsterdam;
[EipWildcard("eip7708_eth_transfer_logs")]
public class Eip7708BlockChainTests : AmsterdamBlockChainTestFixture;
+[EipWildcard("eip7708_eth_transfer_logs")]
+public class Eip7708EngineBlockChainTests : AmsterdamEngineBlockChainTestFixture;
+
[EipWildcard("eip7778_block_gas_accounting_without_refunds")]
public class Eip7778BlockChainTests : AmsterdamBlockChainTestFixture;
+[EipWildcard("eip7778_block_gas_accounting_without_refunds")]
+public class Eip7778EngineBlockChainTests : AmsterdamEngineBlockChainTestFixture;
+
[EipWildcard("eip7843_slotnum")]
public class Eip7843BlockChainTests : AmsterdamBlockChainTestFixture;
+[EipWildcard("eip7843_slotnum")]
+public class Eip7843EngineBlockChainTests : AmsterdamEngineBlockChainTestFixture;
+
[EipWildcard("eip7928_block_level_access_lists")]
public class Eip7928BlockChainTests : AmsterdamBlockChainTestFixture;
+[EipWildcard("eip7928_block_level_access_lists")]
+public class Eip7928EngineBlockChainTests : AmsterdamEngineBlockChainTestFixture;
+
[EipWildcard("eip7954_increase_max_contract_size")]
public class Eip7954BlockChainTests : AmsterdamBlockChainTestFixture;
+[EipWildcard("eip7954_increase_max_contract_size")]
+public class Eip7954EngineBlockChainTests : AmsterdamEngineBlockChainTestFixture;
+
[EipWildcard("eip8024_dupn_swapn_exchange")]
public class Eip8024BlockChainTests : AmsterdamBlockChainTestFixture;
+[EipWildcard("eip8024_dupn_swapn_exchange")]
+public class Eip8024EngineBlockChainTests : AmsterdamEngineBlockChainTestFixture;
+
[EipWildcard("eip8037_state_creation_gas_cost_increase")]
public class Eip8037BlockChainTests : AmsterdamBlockChainTestFixture;
+[EipWildcard("eip8037_state_creation_gas_cost_increase")]
+public class Eip8037EngineBlockChainTests : AmsterdamEngineBlockChainTestFixture;
+
// State tests
[EipWildcard("eip7708_eth_transfer_logs")]
diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/PyspecTestFixture.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/PyspecTestFixture.cs
index ed80ffd0b632..fa2d26108d7b 100644
--- a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/PyspecTestFixture.cs
+++ b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/PyspecTestFixture.cs
@@ -1,7 +1,9 @@
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only
+using System;
using System.Collections.Generic;
+using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Ethereum.Test.Base;
using FluentAssertions;
@@ -25,6 +27,26 @@ public static IEnumerable LoadTests() =>
$"fixtures/blockchain_tests/for_{TestDirectoryHelper.GetDirectoryByConvention("BlockchainTests")}").LoadTests();
}
+///
+/// Generic base for pyspec engine blockchain tests using .
+/// Directory is derived by convention: strip "EngineBlockchainTests" suffix, lowercase.
+/// In CI (TEST_CHUNK set), only runs on Linux x64 to stay within the job timeout budget.
+///
+[TestFixture]
+[Parallelizable(ParallelScope.All)]
+public abstract class PyspecEngineBlockchainTestFixture : BlockchainTestBase
+{
+ [SetUp]
+ public void SkipInCiOnSlowRunners() => CiRunnerGuard.SkipIfNotLinuxX64();
+
+ [TestCaseSource(nameof(LoadTests))]
+ public async Task Test(BlockchainTest test) => (await RunTest(test)).Pass.Should().BeTrue();
+
+ public static IEnumerable LoadTests() =>
+ new TestsSourceLoader(new LoadPyspecTestsStrategy(),
+ $"fixtures/blockchain_tests_engine/for_{TestDirectoryHelper.GetDirectoryByConvention("EngineBlockchainTests")}").LoadTests();
+}
+
///
/// Generic base for pyspec state tests using .
/// Directory is derived by convention: strip "StateTests" suffix, lowercase.
@@ -40,3 +62,23 @@ public static IEnumerable LoadTests() =>
new TestsSourceLoader(new LoadPyspecTestsStrategy(),
$"fixtures/state_tests/for_{TestDirectoryHelper.GetDirectoryByConvention("StateTests")}").LoadTests();
}
+
+///
+/// Skips heavy tests in CI on runners that are too slow or running variant builds.
+/// Only active when TEST_CHUNK is set (CI). Local runs always execute.
+/// Set TEST_SKIP_HEAVY=1 to unconditionally skip (used by checked/no-intrinsics variants).
+///
+internal static class CiRunnerGuard
+{
+ private static readonly bool s_isCi = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TEST_CHUNK"));
+ private static readonly bool s_isLinuxX64 = OperatingSystem.IsLinux() && RuntimeInformation.ProcessArchitecture == Architecture.X64;
+ private static readonly bool s_skipHeavy = Environment.GetEnvironmentVariable("TEST_SKIP_HEAVY") == "1";
+
+ public static void SkipIfNotLinuxX64()
+ {
+ if (s_skipHeavy)
+ Assert.Ignore("Skipped — TEST_SKIP_HEAVY is set");
+ if (s_isCi && !s_isLinuxX64)
+ Assert.Ignore("Skipped in CI — engine/Amsterdam tests only run on Linux x64");
+ }
+}
diff --git a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Tests.cs b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Tests.cs
index 07ef7b7c1924..09eff275184e 100644
--- a/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Tests.cs
+++ b/src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Tests.cs
@@ -27,6 +27,16 @@ public class PragueBlockchainTests : PyspecBlockchainTestFixture;
+// Engine blockchain tests — only forks with meaningful Engine API differences
+// (e.g. blobs, execution requests, BAL). Regular BlockchainTests cover earlier forks.
+// Directory derived from class name by convention (strip "EngineBlockchainTests", lowercase)
+
+public class CancunEngineBlockchainTests : PyspecEngineBlockchainTestFixture;
+
+public class PragueEngineBlockchainTests : PyspecEngineBlockchainTestFixture;
+
+public class OsakaEngineBlockchainTests : PyspecEngineBlockchainTestFixture;
+
// State tests - directory derived from class name by convention (strip "StateTests", lowercase)
public class FrontierStateTests : PyspecStateTestFixture;
diff --git a/src/Nethermind/Ethereum.Test.Base/BlockchainTestBase.cs b/src/Nethermind/Ethereum.Test.Base/BlockchainTestBase.cs
index 61e58cb87e1e..0cf6031197f3 100644
--- a/src/Nethermind/Ethereum.Test.Base/BlockchainTestBase.cs
+++ b/src/Nethermind/Ethereum.Test.Base/BlockchainTestBase.cs
@@ -32,10 +32,14 @@
using Nethermind.Evm.State;
using Nethermind.Init.Modules;
using NUnit.Framework;
-using Nethermind.Merge.Plugin.Data;
-using Nethermind.Merge.Plugin;
using Nethermind.JsonRpc;
+using Nethermind.JsonRpc.Modules;
+using Nethermind.Merge.Plugin;
+using Nethermind.Merge.Plugin.Data;
+using Nethermind.TxPool;
+using Nethermind.Serialization.Json;
using System.Reflection;
+using System.Text.Json;
namespace Ethereum.Test.Base;
@@ -49,15 +53,9 @@ public abstract class BlockchainTestBase
static BlockchainTestBase()
{
DifficultyCalculator = new DifficultyCalculatorWrapper();
- _logManager ??= LimboLogs.Instance;
_logger = _logManager.GetClassLogger();
}
- [SetUp]
- public void Setup()
- {
- }
-
private class DifficultyCalculatorWrapper : IDifficultyCalculator
{
public IDifficultyCalculator? Wrapped { get; set; }
@@ -86,11 +84,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).
+ // EIP-7928 introduces BlockAccessListHash in the block header, which must be computed
+ // during genesis processing. Without target fork rules at genesis, the hash field is missing
+ // and the genesis block header doesn't match the pyspec fixture expectation.
bool genesisUsesTargetFork = test.Network.IsEip7928Enabled;
- List<(ForkActivation Activation, IReleaseSpec Spec)> transitions = isEngineTest || genesisUsesTargetFork
+ List<(ForkActivation Activation, IReleaseSpec Spec)> transitions = genesisUsesTargetFork
? [((ForkActivation)0, test.Network)]
: [((ForkActivation)0, test.GenesisSpec), ((ForkActivation)1, test.Network)]; // genesis block is always initialized with Frontier
@@ -129,12 +128,16 @@ protected async Task RunTest(BlockchainTest test, Stopwatch?
}
IConfigProvider configProvider = new ConfigProvider();
+ IBlocksConfig blocksConfig = configProvider.GetConfig();
+ blocksConfig.PreWarmStateConcurrency = 0;
+ blocksConfig.PreWarmStateOnBlockProcessing = false;
ContainerBuilder containerBuilder = new ContainerBuilder()
.AddModule(new TestNethermindModule(configProvider))
.AddSingleton(specProvider)
.AddSingleton(_logManager)
.AddSingleton(rewardCalculator)
- .AddSingleton(DifficultyCalculator);
+ .AddSingleton(DifficultyCalculator)
+ .AddSingleton(NullTxPool.Instance);
if (isEngineTest)
{
@@ -150,7 +153,6 @@ protected async Task RunTest(BlockchainTest test, Stopwatch?
IBlockValidator blockValidator = container.Resolve();
blockchainProcessor.Start();
- // Register tracer if provided for blocktest tracing
if (tracer is not null)
{
blockchainProcessor.Tracers.Add(tracer);
@@ -171,9 +173,8 @@ protected async Task RunTest(BlockchainTest test, Stopwatch?
Block genesisBlock = Rlp.Decode(test.GenesisRlp.Bytes);
Assert.That(genesisBlock.Header.Hash, Is.EqualTo(new Hash256(test.GenesisBlockHeader.Hash)));
- ManualResetEvent genesisProcessed = new(false);
-
- blockTree.NewHeadBlock += (_, args) =>
+ using ManualResetEvent genesisProcessed = new(false);
+ EventHandler onNewHeadBlock = (_, args) =>
{
if (args.Block.Number == 0)
{
@@ -181,8 +182,7 @@ protected async Task RunTest(BlockchainTest test, Stopwatch?
genesisProcessed.Set();
}
};
-
- blockchainProcessor.BlockRemoved += (_, args) =>
+ EventHandler onGenesisBlockRemoved = (_, args) =>
{
if (args.ProcessingResult != ProcessingResult.Success && args.BlockHash == genesisBlock.Header.Hash)
{
@@ -191,11 +191,22 @@ protected async Task RunTest(BlockchainTest test, Stopwatch?
}
};
- blockTree.SuggestBlock(genesisBlock);
- genesisProcessed.WaitOne(_genesisProcessingTimeoutMs);
- parentHeader = genesisBlock.Header;
+ blockTree.NewHeadBlock += onNewHeadBlock;
+ blockchainProcessor.BlockRemoved += onGenesisBlockRemoved;
+
+ try
+ {
+ blockTree.SuggestBlock(genesisBlock);
+ Assert.That(genesisProcessed.WaitOne(_genesisProcessingTimeoutMs), Is.True,
+ "Timed out waiting for genesis block processing.");
+ parentHeader = genesisBlock.Header;
+ }
+ finally
+ {
+ blockTree.NewHeadBlock -= onNewHeadBlock;
+ blockchainProcessor.BlockRemoved -= onGenesisBlockRemoved;
+ }
- // Dispose genesis block's AccountChanges
genesisBlock.DisposeAccountChanges();
}
@@ -206,9 +217,11 @@ protected async Task RunTest(BlockchainTest test, Stopwatch?
}
else if (test.EngineNewPayloads is not null)
{
- // engine test
- IEngineRpcModule engineRpcModule = container.Resolve();
- await RunNewPayloads(test.EngineNewPayloads, engineRpcModule);
+ // engine test — route through JsonRpcService for realistic deserialization
+ IJsonRpcService rpcService = container.Resolve();
+ JsonRpcUrl engineUrl = new(Uri.UriSchemeHttp, "localhost", 8551, RpcEndpoint.Http, true, ["engine"]);
+ JsonRpcContext rpcContext = new(RpcEndpoint.Http, url: engineUrl);
+ await RunNewPayloads(test.EngineNewPayloads, rpcService, rpcContext, parentHeader.Hash!);
}
else
{
@@ -311,38 +324,69 @@ private static BlockHeader SuggestBlocks(BlockchainTest test, bool failOnInvalid
return parentHeader;
}
- private async static Task RunNewPayloads(TestEngineNewPayloadsJson[]? newPayloads, IEngineRpcModule engineRpcModule)
+ private static readonly Dictionary s_newPayloadParamCounts = Enumerable
+ .Range(1, EngineApiVersions.NewPayload.Latest)
+ .ToDictionary(v => v, v => (typeof(IEngineRpcModule).GetMethod($"engine_newPayloadV{v}")
+ ?? throw new NotSupportedException($"engine_newPayloadV{v} not found on IEngineRpcModule")).GetParameters().Length);
+
+ private async static Task RunNewPayloads(TestEngineNewPayloadsJson[]? newPayloads, IJsonRpcService rpcService, JsonRpcContext rpcContext, Hash256 initialHeadHash)
{
- (ExecutionPayloadV4, string[]?, string[]?, int, int)[] payloads = [.. JsonToEthereumTest.Convert(newPayloads)];
+ if (newPayloads is null || newPayloads.Length == 0) return;
- // blockchain test engine
- foreach ((ExecutionPayload executionPayload, string[]? blobVersionedHashes, string[]? validationError, int newPayloadVersion, int fcuVersion) in payloads)
+ int initialFcuVersion = int.Parse(newPayloads[0].ForkChoiceUpdatedVersion ?? EngineApiVersions.Fcu.Latest.ToString());
+ AssertRpcSuccess(await SendFcu(rpcService, rpcContext, initialFcuVersion, initialHeadHash.ToString()));
+
+ foreach (TestEngineNewPayloadsJson enginePayload in newPayloads)
{
- ResultWrapper res;
- byte[]?[] hashes = blobVersionedHashes is null ? [] : [.. blobVersionedHashes.Select(x => Bytes.FromHexString(x))];
+ int newPayloadVersion = int.Parse(enginePayload.NewPayloadVersion ?? EngineApiVersions.NewPayload.Latest.ToString());
+ int fcuVersion = int.Parse(enginePayload.ForkChoiceUpdatedVersion ?? EngineApiVersions.Fcu.Latest.ToString());
+ string? validationError = JsonToEthereumTest.ParseValidationError(enginePayload, newPayloadVersion);
- MethodInfo newPayloadMethod = engineRpcModule.GetType().GetMethod($"engine_newPayloadV{newPayloadVersion}");
- List