diff --git a/src/Nethermind/Nethermind.Consensus.Test/GenesisLoaderTests.cs b/src/Nethermind/Nethermind.Consensus.Test/GenesisLoaderTests.cs new file mode 100644 index 000000000000..9c41299d4ec6 --- /dev/null +++ b/src/Nethermind/Nethermind.Consensus.Test/GenesisLoaderTests.cs @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Threading; +using FluentAssertions; +using Nethermind.Blockchain; +using Nethermind.Consensus.Processing; +using Nethermind.Core; +using Nethermind.Core.Test.Builders; +using Nethermind.Evm.State; +using Nethermind.Logging; +using Nethermind.State; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Consensus.Test; + +[TestFixture] +public class GenesisLoaderTests +{ + [Test] + public void Load_ShouldFlushCacheAfterSuccessfulGenesisProcessing() + { + // Arrange + Block genesisBlock = Build.A.Block.Genesis.TestObject; + + IGenesisBuilder genesisBuilder = Substitute.For(); + genesisBuilder.Build().Returns(genesisBlock); + + IStateReader stateReader = Substitute.For(); + + IBlockTree blockTree = Substitute.For(); + blockTree.When(x => x.SuggestBlock(Arg.Any())).Do(_ => + { + // Simulate block processing by triggering NewHeadBlock event + blockTree.NewHeadBlock += Raise.EventWith(blockTree, new BlockEventArgs(genesisBlock)); + }); + + IWorldState worldState = Substitute.For(); + IDisposable scopeDisposable = Substitute.For(); + worldState.BeginScope(IWorldState.PreGenesis).Returns(scopeDisposable); + + IWorldStateManager worldStateManager = Substitute.For(); + + IBlockchainProcessor blockchainProcessor = Substitute.For(); + + GenesisLoader.Config config = new(null, TimeSpan.FromSeconds(10)); + ILogManager logManager = LimboLogs.Instance; + + GenesisLoader loader = new( + genesisBuilder, + stateReader, + blockTree, + worldState, + worldStateManager, + blockchainProcessor, + config, + logManager + ); + + // Act + loader.Load(); + + // Assert - verify FlushCache was called + worldStateManager.Received(1).FlushCache(Arg.Any()); + } + + [Test] + public void Load_ShouldNotFlushCache_WhenGenesisProcessingTimesOut() + { + // Arrange + Block genesisBlock = Build.A.Block.Genesis.TestObject; + + IGenesisBuilder genesisBuilder = Substitute.For(); + genesisBuilder.Build().Returns(genesisBlock); + + IStateReader stateReader = Substitute.For(); + + IBlockTree blockTree = Substitute.For(); + blockTree.When(x => x.SuggestBlock(Arg.Any())).Do(_ => + { + // Do nothing - simulate timeout + }); + + IWorldState worldState = Substitute.For(); + IDisposable scopeDisposable = Substitute.For(); + worldState.BeginScope(IWorldState.PreGenesis).Returns(scopeDisposable); + + IWorldStateManager worldStateManager = Substitute.For(); + + IBlockchainProcessor blockchainProcessor = Substitute.For(); + + GenesisLoader.Config config = new(null, TimeSpan.FromMilliseconds(100)); + ILogManager logManager = LimboLogs.Instance; + + GenesisLoader loader = new( + genesisBuilder, + stateReader, + blockTree, + worldState, + worldStateManager, + blockchainProcessor, + config, + logManager + ); + + // Act & Assert - expect timeout exception + Assert.Throws(() => loader.Load()); + + // Verify FlushCache was NOT called since genesis processing failed + worldStateManager.DidNotReceive().FlushCache(Arg.Any()); + } + + [Test] + public void Load_ShouldNotFlushCache_WhenGenesisBlockIsInvalid() + { + // Arrange + Block genesisBlock = Build.A.Block.Genesis.TestObject; + + IGenesisBuilder genesisBuilder = Substitute.For(); + genesisBuilder.Build().Returns(genesisBlock); + + IStateReader stateReader = Substitute.For(); + + IBlockTree blockTree = Substitute.For(); + + IWorldState worldState = Substitute.For(); + IDisposable scopeDisposable = Substitute.For(); + worldState.BeginScope(IWorldState.PreGenesis).Returns(scopeDisposable); + + IWorldStateManager worldStateManager = Substitute.For(); + + IBlockchainProcessor blockchainProcessor = Substitute.For(); + blockTree.When(x => x.SuggestBlock(Arg.Any())).Do(_ => + { + // Simulate invalid block by triggering InvalidBlock event + blockchainProcessor.InvalidBlock += Raise.EventWith( + blockchainProcessor, + new IBlockchainProcessor.InvalidBlockEventArgs { InvalidBlock = genesisBlock }); + }); + + GenesisLoader.Config config = new(null, TimeSpan.FromSeconds(10)); + ILogManager logManager = LimboLogs.Instance; + + GenesisLoader loader = new( + genesisBuilder, + stateReader, + blockTree, + worldState, + worldStateManager, + blockchainProcessor, + config, + logManager + ); + + // Act & Assert - expect InvalidBlockException + Assert.Throws(() => loader.Load()); + + // Verify FlushCache was NOT called since genesis was invalid + worldStateManager.DidNotReceive().FlushCache(Arg.Any()); + } + + [Test] + public void Load_ShouldFlushCacheAfterScopeExit() + { + // Arrange + Block genesisBlock = Build.A.Block.Genesis.TestObject; + + IGenesisBuilder genesisBuilder = Substitute.For(); + genesisBuilder.Build().Returns(genesisBlock); + + IStateReader stateReader = Substitute.For(); + + IBlockTree blockTree = Substitute.For(); + bool scopeExited = false; + blockTree.When(x => x.SuggestBlock(Arg.Any())).Do(_ => + { + // Simulate block processing by triggering NewHeadBlock event + blockTree.NewHeadBlock += Raise.EventWith(blockTree, new BlockEventArgs(genesisBlock)); + }); + + IWorldState worldState = Substitute.For(); + IDisposable scopeDisposable = Substitute.For(); + scopeDisposable.When(x => x.Dispose()).Do(_ => scopeExited = true); + worldState.BeginScope(IWorldState.PreGenesis).Returns(scopeDisposable); + + IWorldStateManager worldStateManager = Substitute.For(); + worldStateManager.When(x => x.FlushCache(Arg.Any())).Do(_ => + { + // Verify that scope was exited before FlushCache was called + scopeExited.Should().BeTrue("FlushCache should be called after scope exit"); + }); + + IBlockchainProcessor blockchainProcessor = Substitute.For(); + + GenesisLoader.Config config = new(null, TimeSpan.FromSeconds(10)); + ILogManager logManager = LimboLogs.Instance; + + GenesisLoader loader = new( + genesisBuilder, + stateReader, + blockTree, + worldState, + worldStateManager, + blockchainProcessor, + config, + logManager + ); + + // Act + loader.Load(); + + // Assert - verify FlushCache was called after scope exit + worldStateManager.Received(1).FlushCache(Arg.Any()); + } +} diff --git a/src/Nethermind/Nethermind.Consensus/Processing/GenesisLoader.cs b/src/Nethermind/Nethermind.Consensus/Processing/GenesisLoader.cs index c743aefa99ef..fd369d528e87 100644 --- a/src/Nethermind/Nethermind.Consensus/Processing/GenesisLoader.cs +++ b/src/Nethermind/Nethermind.Consensus/Processing/GenesisLoader.cs @@ -18,6 +18,7 @@ public class GenesisLoader( IStateReader stateReader, IBlockTree blockTree, IWorldState worldState, + IWorldStateManager worldStateManager, IBlockchainProcessor blockchainProcessor, GenesisLoader.Config genesisConfig, ILogManager logManager @@ -29,6 +30,12 @@ public record Config(Hash256? ExpectedGenesisHash, TimeSpan GenesisTimeout); ILogger _logger = logManager.GetClassLogger(); public void Load() + { + DoLoad(); + worldStateManager.FlushCache(CancellationToken.None); + } + + private void DoLoad() { using var _ = worldState.BeginScope(IWorldState.PreGenesis);