Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/Nethermind/Nethermind.Xdc.Test/SnapshotManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -154,6 +155,37 @@ public void GetSnapshot_DifferentBlockNumbers_ReturnsSnapshotFromCorrectGapNumbe
result.Should().BeEquivalentTo(snapshot);
}

[Test]
public void CalculateNextEpochMasternodes_AllPenalized_Throws()
{
// Arrange
IXdcReleaseSpec releaseSpec = Substitute.For<IXdcReleaseSpec>();
releaseSpec.EpochLength.Returns(900);
releaseSpec.Gap.Returns(450);
releaseSpec.MaxMasternodes.Returns(108);
releaseSpec.SwitchBlock.Returns(0L);

IPenaltyHandler penaltyHandler = Substitute.For<IPenaltyHandler>();
IBlockTree blockTree = Substitute.For<IBlockTree>();
SnapshotManager snapshotManager = new SnapshotManager(new MemDb(), blockTree, penaltyHandler, Substitute.For<IMasternodeVotingContract>(), Substitute.For<ISpecProvider>());

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<long>(), Arg.Any<Hash256>(), Arg.Any<Address[]>())
.Returns(candidates);

// Act & Assert
Assert.That(
() => snapshotManager.CalculateNextEpochMasternodes(900, header.Hash!, releaseSpec),
Throws.InvalidOperationException);
}

[TestCase(450)]
[TestCase(1350)]
public void NewHeadBlock_(int gapNumber)
Expand Down
65 changes: 65 additions & 0 deletions src/Nethermind/Nethermind.Xdc.Test/XdcSealValidatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<byte>());
headerBuilder.WithAuthor(masterSigners[1].Address);
headerBuilder.WithParent(parent);
XdcBlockHeader header = headerBuilder.TestObject;

ISpecProvider specProvider = Substitute.For<ISpecProvider>();
IXdcReleaseSpec releaseSpec = Substitute.For<IXdcReleaseSpec>();
releaseSpec.EpochLength.Returns(900);
specProvider.GetSpec(Arg.Any<ForkActivation>()).Returns(releaseSpec);

IEpochSwitchManager epochSwitchManager = Substitute.For<IEpochSwitchManager>();
epochSwitchManager.IsEpochSwitchAtBlock(Arg.Any<XdcBlockHeader>()).Returns(false);
// Simulate missing epoch info
epochSwitchManager.GetEpochSwitchInfo(Arg.Any<XdcBlockHeader>()).Returns((EpochSwitchInfo?)null);

XdcSealValidator validator = new XdcSealValidator(Substitute.For<ISnapshotManager>(), 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<byte>());
headerBuilder.WithAuthor(masterSigners[1].Address);
headerBuilder.WithParent(parent);
XdcBlockHeader header = headerBuilder.TestObject;

ISpecProvider specProvider = Substitute.For<ISpecProvider>();
IXdcReleaseSpec releaseSpec = Substitute.For<IXdcReleaseSpec>();
releaseSpec.EpochLength.Returns(900);
specProvider.GetSpec(Arg.Any<ForkActivation>()).Returns(releaseSpec);

IEpochSwitchManager epochSwitchManager = Substitute.For<IEpochSwitchManager>();
epochSwitchManager.IsEpochSwitchAtBlock(Arg.Any<XdcBlockHeader>()).Returns(false);
// Epoch info exists but masternodes list is empty
epochSwitchManager.GetEpochSwitchInfo(Arg.Any<XdcBlockHeader>())
.Returns(new EpochSwitchInfo([], [], [], new BlockRoundInfo(Hash256.Zero, 0, 0)));

XdcSealValidator validator = new XdcSealValidator(Substitute.For<ISnapshotManager>(), 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);
Expand Down
8 changes: 8 additions & 0 deletions src/Nethermind/Nethermind.Xdc/SnapshotManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/Nethermind/Nethermind.Xdc/XdcHotStuff.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
12 changes: 10 additions & 2 deletions src/Nethermind/Nethermind.Xdc/XdcSealValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down