diff --git a/src/Nethermind/Nethermind.Xdc.Test/SnapshotManagerTests.cs b/src/Nethermind/Nethermind.Xdc.Test/SnapshotManagerTests.cs index 4a8516e2ddec..b60ef7aeb7f8 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; @@ -12,6 +13,7 @@ using Nethermind.Xdc.Types; using NSubstitute; using NUnit.Framework; +using System; namespace Nethermind.Xdc.Test; @@ -174,4 +176,31 @@ public void NewHeadBlock_(int gapNumber) blockTree.NewHeadBlock += Raise.EventWith(new BlockEventArgs(new Block(header))); snapshotManager.GetSnapshotByGapNumber(header.Number)!.HeaderHash.Should().Be(header.Hash!); } + + [Test] + public void CalculateNextEpochMasternodes_AllPenalized_Throws() + { + IXdcReleaseSpec releaseSpec = Substitute.For(); + releaseSpec.EpochLength.Returns(900); + releaseSpec.Gap.Returns(450); + releaseSpec.MaxMasternodes.Returns(100); + releaseSpec.SwitchBlock.Returns(0); + + IPenaltyHandler penaltyHandler = Substitute.For(); + IBlockTree blockTree = Substitute.For(); + ISpecProvider specProvider = Substitute.For(); + IMasternodeVotingContract votingContract = Substitute.For(); + SnapshotManager snapshotManager = new SnapshotManager(new MemDb(), blockTree, penaltyHandler, votingContract, specProvider); + + XdcBlockHeader header = Build.A.XdcBlockHeader().WithNumber(0).TestObject; + blockTree.FindHeader(0).Returns(header); + Snapshot snapshot = new Snapshot(0, header.Hash!, new[] { TestItem.AddressA, TestItem.AddressB }); + snapshotManager.StoreSnapshot(snapshot); + + penaltyHandler.HandlePenalties(Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(snapshot.NextEpochCandidates); + + Assert.Throws(() => + snapshotManager.CalculateNextEpochMasternodes(2, header.ParentHash ?? Hash256.Zero, releaseSpec)); + } } diff --git a/src/Nethermind/Nethermind.Xdc.Test/XdcSealValidatorTests.cs b/src/Nethermind/Nethermind.Xdc.Test/XdcSealValidatorTests.cs index 0cc13b7f80ce..a36768b3400e 100644 --- a/src/Nethermind/Nethermind.Xdc.Test/XdcSealValidatorTests.cs +++ b/src/Nethermind/Nethermind.Xdc.Test/XdcSealValidatorTests.cs @@ -214,6 +214,70 @@ public void ValidateParams_HeaderHasDifferentSealParameters_ReturnsExpected(XdcB Assert.That(validator.ValidateParams(parent, header), Is.EqualTo(expected)); } + [Test] + public void ValidateParams_MissingEpochInfo_ReturnsFalse() + { + XdcBlockHeader parent = Build.A.XdcBlockHeader().TestObject; + + PrivateKeyGenerator keyBuilder = new PrivateKeyGenerator(); + PrivateKey[] masterSigners = Enumerable.Range(0, 3).Select(_ => keyBuilder.Generate()).ToArray(); + QuorumCertificate qc = CreateQc(new BlockRoundInfo(Hash256.Zero, 1, 1), masterSigners, 1); + ExtraFieldsV2 extraFields = new ExtraFieldsV2(2, qc); + + XdcBlockHeaderBuilder headerBuilder = Build.A.XdcBlockHeader(); + headerBuilder.WithParent(parent); + headerBuilder.WithExtraFieldsV2(extraFields); + XdcBlockHeader header = headerBuilder.TestObject; + header.Validators = Array.Empty(); + header.Penalties = Array.Empty(); + + IXdcReleaseSpec releaseSpec = Substitute.For(); + releaseSpec.EpochLength.Returns(900); + ISpecProvider specProvider = Substitute.For(); + specProvider.GetSpec(Arg.Any()).Returns(releaseSpec); + + IEpochSwitchManager epochSwitchManager = Substitute.For(); + epochSwitchManager.IsEpochSwitchAtBlock(header).Returns(false); + epochSwitchManager.GetEpochSwitchInfo(header).Returns((EpochSwitchInfo?)null); + + XdcSealValidator validator = new XdcSealValidator(Substitute.For(), epochSwitchManager, specProvider); + + Assert.That(validator.ValidateParams(parent, header, out var error), Is.False); + Assert.That(error, Does.Contain("Epoch switch info")); + } + + [Test] + public void ValidateParams_EmptyMasternodes_ReturnsFalse() + { + XdcBlockHeader parent = Build.A.XdcBlockHeader().TestObject; + + PrivateKeyGenerator keyBuilder = new PrivateKeyGenerator(); + PrivateKey[] masterSigners = Enumerable.Range(0, 3).Select(_ => keyBuilder.Generate()).ToArray(); + QuorumCertificate qc = CreateQc(new BlockRoundInfo(Hash256.Zero, 1, 1), masterSigners, 1); + ExtraFieldsV2 extraFields = new ExtraFieldsV2(2, qc); + + XdcBlockHeaderBuilder headerBuilder = Build.A.XdcBlockHeader(); + headerBuilder.WithParent(parent); + headerBuilder.WithExtraFieldsV2(extraFields); + XdcBlockHeader header = headerBuilder.TestObject; + header.Validators = Array.Empty(); + header.Penalties = Array.Empty(); + + IXdcReleaseSpec releaseSpec = Substitute.For(); + releaseSpec.EpochLength.Returns(900); + ISpecProvider specProvider = Substitute.For(); + specProvider.GetSpec(Arg.Any()).Returns(releaseSpec); + + IEpochSwitchManager epochSwitchManager = Substitute.For(); + epochSwitchManager.IsEpochSwitchAtBlock(header).Returns(false); + epochSwitchManager.GetEpochSwitchInfo(header).Returns(new EpochSwitchInfo(Array.Empty
(), [], [], new BlockRoundInfo(Hash256.Zero, 1, 1))); + + XdcSealValidator validator = new XdcSealValidator(Substitute.For(), epochSwitchManager, specProvider); + + Assert.That(validator.ValidateParams(parent, header, out var error), Is.False); + Assert.That(error, Does.Contain("Snapshot returned no master nodes")); + } + 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..e4e566acb60f 100644 --- a/src/Nethermind/Nethermind.Xdc/SnapshotManager.cs +++ b/src/Nethermind/Nethermind.Xdc/SnapshotManager.cs @@ -99,9 +99,11 @@ public void StoreSnapshot(Snapshot snapshot) if (candidates.Length > maxMasternodes) { Array.Resize(ref candidates, maxMasternodes); + EnsureMasternodesAvailable(candidates, blockNumber); return (candidates, []); } + EnsureMasternodesAvailable(candidates, blockNumber); return (candidates, []); } @@ -112,9 +114,18 @@ public void StoreSnapshot(Snapshot snapshot) .Take(maxMasternodes) // enforce max cap .ToArray(); + EnsureMasternodesAvailable(candidates, blockNumber); return (candidates, penalties); } + private static void EnsureMasternodesAvailable(Address[] masternodes, long blockNumber) + { + if (masternodes.Length == 0) + { + throw new InvalidOperationException($"No masternodes available for block #{blockNumber} 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..ad055354b463 100644 --- a/src/Nethermind/Nethermind.Xdc/XdcHotStuff.cs +++ b/src/Nethermind/Nethermind.Xdc/XdcHotStuff.cs @@ -426,7 +426,12 @@ public Address GetLeaderAddress(XdcBlockHeader currentHead, ulong round, IXdcRel else { var epochSwitchInfo = _epochSwitchManager.GetEpochSwitchInfo(currentHead); - currentMasternodes = epochSwitchInfo.Masternodes; + currentMasternodes = epochSwitchInfo?.Masternodes; + } + + if (currentMasternodes is null || currentMasternodes.Length == 0) + { + throw new InvalidOperationException($"No masternodes available for leader selection at round {round}."); } int currentLeaderIndex = ((int)round % spec.EpochLength % currentMasternodes.Length); diff --git a/src/Nethermind/Nethermind.Xdc/XdcSealValidator.cs b/src/Nethermind/Nethermind.Xdc/XdcSealValidator.cs index 297a3b165d89..c46d162c8457 100644 --- a/src/Nethermind/Nethermind.Xdc/XdcSealValidator.cs +++ b/src/Nethermind/Nethermind.Xdc/XdcSealValidator.cs @@ -93,10 +93,19 @@ 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 info not found for header."; + 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 = $"Snapshot returned no master nodes for header \n{xdcHeader}"; + return false; + } } ulong currentLeaderIndex = (xdcHeader.ExtraConsensusData.BlockRound % (ulong)xdcSpec.EpochLength % (ulong)masternodes.Length);