From be2f7885765d14a487e748b1233255405a27f556 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 07:18:39 +0000 Subject: [PATCH 1/2] Initial plan From 1c1f0226169c14202af945716604e0cb360f03a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 29 Mar 2026 07:27:58 +0000 Subject: [PATCH 2/2] Add masternode validation guards and new XDC consensus tests Agent-Logs-Url: https://github.com/Deejae69/nethermind/sessions/bee3779a-8489-45aa-bf61-77b1b0ebf51f Co-authored-by: Deejae69 <179696940+Deejae69@users.noreply.github.com> --- .../SnapshotManagerTests.cs | 32 +++++++++ .../XdcSealValidatorTests.cs | 65 +++++++++++++++++++ .../Nethermind.Xdc/SnapshotManager.cs | 8 +++ src/Nethermind/Nethermind.Xdc/XdcHotStuff.cs | 2 + .../Nethermind.Xdc/XdcSealValidator.cs | 12 +++- 5 files changed, 117 insertions(+), 2 deletions(-) diff --git a/src/Nethermind/Nethermind.Xdc.Test/SnapshotManagerTests.cs b/src/Nethermind/Nethermind.Xdc.Test/SnapshotManagerTests.cs index 4a8516e2ddec..88cb145b1ef7 100644 --- a/src/Nethermind/Nethermind.Xdc.Test/SnapshotManagerTests.cs +++ b/src/Nethermind/Nethermind.Xdc.Test/SnapshotManagerTests.cs @@ -4,6 +4,7 @@ using FluentAssertions; using Nethermind.Blockchain; using Nethermind.Core; +using Nethermind.Core.Crypto; using Nethermind.Core.Specs; using Nethermind.Core.Test.Builders; using Nethermind.Db; @@ -154,6 +155,37 @@ public void GetSnapshot_DifferentBlockNumbers_ReturnsSnapshotFromCorrectGapNumbe result.Should().BeEquivalentTo(snapshot); } + [Test] + public void CalculateNextEpochMasternodes_AllPenalized_Throws() + { + // Arrange + IXdcReleaseSpec releaseSpec = Substitute.For(); + releaseSpec.EpochLength.Returns(900); + releaseSpec.Gap.Returns(450); + releaseSpec.MaxMasternodes.Returns(108); + releaseSpec.SwitchBlock.Returns(0L); + + IPenaltyHandler penaltyHandler = Substitute.For(); + IBlockTree blockTree = Substitute.For(); + SnapshotManager snapshotManager = new SnapshotManager(new MemDb(), blockTree, penaltyHandler, Substitute.For(), Substitute.For()); + + const int gapBlock = 0; + XdcBlockHeader header = Build.A.XdcBlockHeader().TestObject; + Address[] candidates = [Address.FromNumber(1), Address.FromNumber(2)]; + var snapshot = new Snapshot(gapBlock, header.Hash!, candidates); + snapshotManager.StoreSnapshot(snapshot); + blockTree.FindHeader(gapBlock).Returns(header); + + // All candidates are penalized + penaltyHandler.HandlePenalties(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(candidates); + + // Act & Assert + Assert.That( + () => snapshotManager.CalculateNextEpochMasternodes(900, header.Hash!, releaseSpec), + Throws.InvalidOperationException); + } + [TestCase(450)] [TestCase(1350)] public void NewHeadBlock_(int gapNumber) diff --git a/src/Nethermind/Nethermind.Xdc.Test/XdcSealValidatorTests.cs b/src/Nethermind/Nethermind.Xdc.Test/XdcSealValidatorTests.cs index 0cc13b7f80ce..94ae8943290f 100644 --- a/src/Nethermind/Nethermind.Xdc.Test/XdcSealValidatorTests.cs +++ b/src/Nethermind/Nethermind.Xdc.Test/XdcSealValidatorTests.cs @@ -214,6 +214,71 @@ public void ValidateParams_HeaderHasDifferentSealParameters_ReturnsExpected(XdcB Assert.That(validator.ValidateParams(parent, header), Is.EqualTo(expected)); } + [Test] + public void ValidateParams_MissingEpochInfo_ReturnsFalse() + { + // Arrange - non-epoch-switch header + XdcBlockHeader parent = Build.A.XdcBlockHeader().TestObject; + + PrivateKeyGenerator keyBuilder = new PrivateKeyGenerator(); + PrivateKey[] masterSigners = Enumerable.Range(0, 108).Select(i => keyBuilder.Generate()).ToArray(); + var extraFieldsV2 = new ExtraFieldsV2(901, CreateQc(new BlockRoundInfo(Hash256.Zero, 900, 1), masterSigners, 1)); + XdcBlockHeaderBuilder headerBuilder = Build.A.XdcBlockHeader() + .WithExtraFieldsV2(extraFieldsV2) + .WithValidators(Array.Empty()); + headerBuilder.WithAuthor(masterSigners[1].Address); + headerBuilder.WithParent(parent); + XdcBlockHeader header = headerBuilder.TestObject; + + ISpecProvider specProvider = Substitute.For(); + IXdcReleaseSpec releaseSpec = Substitute.For(); + releaseSpec.EpochLength.Returns(900); + specProvider.GetSpec(Arg.Any()).Returns(releaseSpec); + + IEpochSwitchManager epochSwitchManager = Substitute.For(); + epochSwitchManager.IsEpochSwitchAtBlock(Arg.Any()).Returns(false); + // Simulate missing epoch info + epochSwitchManager.GetEpochSwitchInfo(Arg.Any()).Returns((EpochSwitchInfo?)null); + + XdcSealValidator validator = new XdcSealValidator(Substitute.For(), epochSwitchManager, specProvider); + + // Act & Assert + Assert.That(validator.ValidateParams(parent, header), Is.False); + } + + [Test] + public void ValidateParams_EmptyMasternodes_ReturnsFalse() + { + // Arrange - non-epoch-switch header + XdcBlockHeader parent = Build.A.XdcBlockHeader().TestObject; + + PrivateKeyGenerator keyBuilder = new PrivateKeyGenerator(); + PrivateKey[] masterSigners = Enumerable.Range(0, 108).Select(i => keyBuilder.Generate()).ToArray(); + var extraFieldsV2 = new ExtraFieldsV2(901, CreateQc(new BlockRoundInfo(Hash256.Zero, 900, 1), masterSigners, 1)); + XdcBlockHeaderBuilder headerBuilder = Build.A.XdcBlockHeader() + .WithExtraFieldsV2(extraFieldsV2) + .WithValidators(Array.Empty()); + headerBuilder.WithAuthor(masterSigners[1].Address); + headerBuilder.WithParent(parent); + XdcBlockHeader header = headerBuilder.TestObject; + + ISpecProvider specProvider = Substitute.For(); + IXdcReleaseSpec releaseSpec = Substitute.For(); + releaseSpec.EpochLength.Returns(900); + specProvider.GetSpec(Arg.Any()).Returns(releaseSpec); + + IEpochSwitchManager epochSwitchManager = Substitute.For(); + epochSwitchManager.IsEpochSwitchAtBlock(Arg.Any()).Returns(false); + // Epoch info exists but masternodes list is empty + epochSwitchManager.GetEpochSwitchInfo(Arg.Any()) + .Returns(new EpochSwitchInfo([], [], [], new BlockRoundInfo(Hash256.Zero, 0, 0))); + + XdcSealValidator validator = new XdcSealValidator(Substitute.For(), epochSwitchManager, specProvider); + + // Act & Assert + Assert.That(validator.ValidateParams(parent, header), Is.False); + } + private static QuorumCertificate CreateQc(BlockRoundInfo roundInfo, PrivateKey[] keys, ulong gapNumber) { EthereumEcdsa ecdsa = new EthereumEcdsa(0); diff --git a/src/Nethermind/Nethermind.Xdc/SnapshotManager.cs b/src/Nethermind/Nethermind.Xdc/SnapshotManager.cs index 05ff2b963925..186424b8ac13 100644 --- a/src/Nethermind/Nethermind.Xdc/SnapshotManager.cs +++ b/src/Nethermind/Nethermind.Xdc/SnapshotManager.cs @@ -112,9 +112,17 @@ public void StoreSnapshot(Snapshot snapshot) .Take(maxMasternodes) // enforce max cap .ToArray(); + EnsureMasternodesAvailable(candidates); + return (candidates, penalties); } + private static void EnsureMasternodesAvailable(Address[] candidates) + { + if (candidates.Length == 0) + throw new InvalidOperationException("No masternodes available after applying penalties."); + } + private void OnNewHeadBlock(object? sender, BlockEventArgs e) { UpdateMasterNodes((XdcBlockHeader)e.Block.Header); diff --git a/src/Nethermind/Nethermind.Xdc/XdcHotStuff.cs b/src/Nethermind/Nethermind.Xdc/XdcHotStuff.cs index 0c3ef38be0eb..164313dab205 100644 --- a/src/Nethermind/Nethermind.Xdc/XdcHotStuff.cs +++ b/src/Nethermind/Nethermind.Xdc/XdcHotStuff.cs @@ -426,6 +426,8 @@ public Address GetLeaderAddress(XdcBlockHeader currentHead, ulong round, IXdcRel else { var epochSwitchInfo = _epochSwitchManager.GetEpochSwitchInfo(currentHead); + if (epochSwitchInfo is null || epochSwitchInfo.Masternodes is null || epochSwitchInfo.Masternodes.Length == 0) + throw new InvalidOperationException($"No masternode data available for block #{currentHead.Number}."); currentMasternodes = epochSwitchInfo.Masternodes; } diff --git a/src/Nethermind/Nethermind.Xdc/XdcSealValidator.cs b/src/Nethermind/Nethermind.Xdc/XdcSealValidator.cs index 297a3b165d89..f4052c0419a8 100644 --- a/src/Nethermind/Nethermind.Xdc/XdcSealValidator.cs +++ b/src/Nethermind/Nethermind.Xdc/XdcSealValidator.cs @@ -93,10 +93,18 @@ public bool ValidateParams(BlockHeader parent, BlockHeader header, out string er return false; } //TODO get masternodes from snapshot - EpochSwitchInfo epochSwitchInfo = epochSwitchManager.GetEpochSwitchInfo(xdcHeader); + EpochSwitchInfo? epochSwitchInfo = epochSwitchManager.GetEpochSwitchInfo(xdcHeader); + if (epochSwitchInfo is null) + { + error = "Epoch switch information is missing."; + return false; + } masternodes = epochSwitchInfo.Masternodes; if (masternodes is null || masternodes.Length == 0) - throw new InvalidOperationException($"Snap shot returned no master nodes for header \n{xdcHeader.ToString()}"); + { + error = "No masternodes available in epoch switch info."; + return false; + } } ulong currentLeaderIndex = (xdcHeader.ExtraConsensusData.BlockRound % (ulong)xdcSpec.EpochLength % (ulong)masternodes.Length);