Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
df14e97
push draft implementation of QC manager and other related components
Demuirgos Sep 16, 2025
35e5254
intial draft implementation of Votes manager
Demuirgos Sep 16, 2025
6cd2824
arg null exception
ak88 Sep 17, 2025
5a97b0d
fixes
ak88 Sep 17, 2025
6bd2f9d
Merge branch 'master' into xdc/feature-quorum-cert-manager
ak88 Sep 17, 2025
c3ac617
refactor and fixes
ak88 Sep 17, 2025
926670b
refactor
ak88 Sep 17, 2025
a135ab1
refactor
ak88 Sep 17, 2025
97d0860
Change signatures
ak88 Sep 18, 2025
4c8aad3
Merge branch 'refactor/epochmanager' into xdc/feature-quorum-cert-man…
ak88 Sep 18, 2025
c1b5628
bit of refactor
ak88 Sep 18, 2025
fda1007
cleanup
ak88 Sep 18, 2025
c31de15
Merge branch 'master' into xdc/feature-quorum-cert-manager
ak88 Sep 22, 2025
df17391
merged master
ak88 Sep 23, 2025
a0cc61b
Test
ak88 Sep 24, 2025
84df34f
format
ak88 Sep 24, 2025
8fb15b9
fixes
ak88 Sep 24, 2025
7e39c8a
format
ak88 Sep 24, 2025
b7ff01a
format
ak88 Sep 25, 2025
9db59fa
persist when committing QC
ak88 Sep 25, 2025
11a3a5a
Merge branch 'master' into xdc/feature-quorum-cert-manager
ak88 Sep 25, 2025
2b042e1
merge conflicts
ak88 Sep 26, 2025
f6baab8
Merge branch 'master' into xdc/feature-votes-manager
ak88 Sep 29, 2025
90cb635
merge fixes
ak88 Sep 29, 2025
9b191f7
Merge branch 'xdc/feature-quorum-cert-manager' into xdc/feature-votes…
ak88 Sep 29, 2025
64efd63
votepool type
ak88 Sep 30, 2025
bad79f6
concurrent vote pool
ak88 Oct 1, 2025
598e63b
added log
ak88 Oct 1, 2025
5236ece
isigner
ak88 Oct 3, 2025
d34a2aa
comment
ak88 Oct 8, 2025
6f79d3c
comments
ak88 Oct 8, 2025
28b1e28
implement initial vote filtering
cicr99 Oct 15, 2025
da4d213
Merge branch 'master' into xdc/feature-votes-manager
cicr99 Oct 15, 2025
81a5095
refactor vote manager
cicr99 Oct 15, 2025
ff73ad9
implement XdcPool for votes and timeouts
cicr99 Oct 15, 2025
40aca69
ensure valid votes before processing qc
cicr99 Oct 16, 2025
dbdc9ad
format
cicr99 Oct 17, 2025
d55c1e9
implement pool for timeouts and votes
cicr99 Oct 21, 2025
d0b54c4
Merge branch 'feature/xdc-pool' into xdc/feature-votes-manager
cicr99 Oct 21, 2025
4c87803
add tests for vote handling
cicr99 Oct 23, 2025
f2e9609
Merge branch 'master' into xdc/feature-votes-manager
cicr99 Oct 23, 2025
a99cd99
fix errors after merge
cicr99 Oct 23, 2025
382b0bb
format
ak88 Oct 24, 2025
12460d3
fix for QC manager
ak88 Oct 24, 2025
ff68753
format
ak88 Oct 24, 2025
800ce2d
refactors and add tests
cicr99 Oct 24, 2025
c2fb333
Merge branch 'master' into xdc/feature-votes-manager
cicr99 Oct 24, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,24 @@ public XdcBlockHeaderBuilder WithExtraConsensusData(ExtraFieldsV2 extraFieldsV2)
return this;
}

public new XdcBlockHeaderBuilder WithParentHash(Hash256 parentHash)
{
XdcTestObjectInternal.ParentHash = parentHash;
return this;
}

public new XdcBlockHeaderBuilder WithBaseFee(UInt256 baseFee)
{
TestObjectInternal.BaseFeePerGas = baseFee;
return this;
}

public new XdcBlockHeaderBuilder WithNumber(long blockNumber)
{
TestObjectInternal.Number = blockNumber;
return this;
}

public new XdcBlockHeaderBuilder WithHash(Hash256 hash256)
{
TestObjectInternal.Hash = hash256;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public void VerifyCertificate_CertificateIsNull_ThrowsArgumentNullException()
Substitute.For<IBlockTree>(),
Substitute.For<IDb>(),
Substitute.For<ISpecProvider>(),
Substitute.For<IEpochSwitchManager>());
Substitute.For<IEpochSwitchManager>(),
new BlockInfoValidator());

Assert.That(() => quorumCertificateManager.VerifyCertificate(null!, Build.A.XdcBlockHeader().TestObject, out _), Throws.ArgumentNullException);
}
Expand All @@ -43,7 +44,8 @@ public void VerifyCertificate_HeaderIsNull_ThrowsArgumentNullException()
Substitute.For<IBlockTree>(),
Substitute.For<IDb>(),
Substitute.For<ISpecProvider>(),
Substitute.For<IEpochSwitchManager>());
Substitute.For<IEpochSwitchManager>(),
new BlockInfoValidator());

Assert.That(() => quorumCertificateManager.VerifyCertificate(Build.A.QuorumCertificate().TestObject, null!, out _), Throws.ArgumentNullException);
}
Expand Down Expand Up @@ -97,7 +99,8 @@ public void VerifyCertificate_QcWithDifferentParameters_ReturnsExpected(QuorumCe
Substitute.For<IBlockTree>(),
Substitute.For<IDb>(),
specProvider,
epochSwitchManager);
epochSwitchManager,
new BlockInfoValidator());

Assert.That(quorumCertificateManager.VerifyCertificate(quorumCert, xdcBlockHeaderBuilder.TestObject, out _), Is.EqualTo(expected));
}
Expand Down
247 changes: 247 additions & 0 deletions src/Nethermind/Nethermind.Xdc.Test/VotesManagerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Nethermind.Blockchain;
using Nethermind.Consensus;
using Nethermind.Core;
using Nethermind.Core.Crypto;
using Nethermind.Core.Specs;
using Nethermind.Core.Test.Builders;
using Nethermind.Crypto;
using Nethermind.Serialization.Rlp;
using Nethermind.Xdc.Spec;
using Nethermind.Xdc.Types;
using NSubstitute;
using NUnit.Framework;

namespace Nethermind.Xdc.Test;

public class VotesManagerTests
{
public static IEnumerable<TestCaseData> HandleVoteCases()
{
var (keys, _) = MakeKeys(20);
var masternodes = keys.Select(k => k.Address).ToArray();

ulong currentRound = 1;
XdcBlockHeader header = Build.A.XdcBlockHeader()
.WithExtraConsensusData(new ExtraFieldsV2(currentRound, new QuorumCertificate(new BlockRoundInfo(Hash256.Zero, 0, 0), null, 450)))
.TestObject;
var info = new BlockRoundInfo(header.Hash!, currentRound, header.Number);

// Base case
yield return new TestCaseData(masternodes, header, currentRound, keys.Select(k => BuildSignedVote(info, 450, k)).ToArray(), info, 1);

// Not enough valid signers
var (extraKeys, _) = MakeKeys(2);
var votes = keys.Take(12).Select(k => BuildSignedVote(info, 450, k)).ToArray();
var extraVotes = extraKeys.Select(k => BuildSignedVote(info, 450, k)).ToArray();
yield return new TestCaseData(masternodes, header, currentRound, votes.Concat(extraVotes).ToArray(), info, 0);

// Wrong gap number generates different keys for the vote pool
var keysForVotes = keys.Take(14).ToArray();
var votesWithDiffGap = new List<Vote>(capacity: keysForVotes.Length);
for (var i = 0; i < keysForVotes.Length - 3; i++) votesWithDiffGap.Add(BuildSignedVote(info, 450, keysForVotes[i]));
for (var i = keysForVotes.Length - 3; i < keysForVotes.Length; i++) votesWithDiffGap.Add(BuildSignedVote(info, 451, keysForVotes[i]));
yield return new TestCaseData(masternodes, header, currentRound, votesWithDiffGap.ToArray(), info, 0);
}

[TestCaseSource(nameof(HandleVoteCases))]
public async Task HandleVote_VariousScenarios_CommitsQcExpectedTimes(Address[] masternodes, XdcBlockHeader header, ulong currentRound, Vote[] votes, BlockRoundInfo info, int expectedCalls)
{
var context = new XdcContext { CurrentRound = currentRound };
IBlockTree blockTree = Substitute.For<IBlockTree>();
blockTree.FindHeader(Arg.Any<Hash256>(), Arg.Any<long>()).Returns(header);

IEpochSwitchManager epochSwitchManager = Substitute.For<IEpochSwitchManager>();
var epochSwitchInfo = new EpochSwitchInfo(masternodes, [], [], info);
epochSwitchManager
.GetEpochSwitchInfo(header)
.Returns(epochSwitchInfo);

ISnapshotManager snapshotManager = Substitute.For<ISnapshotManager>();
IQuorumCertificateManager quorumCertificateManager = Substitute.For<IQuorumCertificateManager>();
ISpecProvider specProvider = Substitute.For<ISpecProvider>();
IXdcReleaseSpec xdcReleaseSpec = Substitute.For<IXdcReleaseSpec>();
xdcReleaseSpec.CertThreshold.Returns(0.667);
specProvider.GetSpec(Arg.Any<ForkActivation>()).Returns(xdcReleaseSpec);

ISigner signer = Substitute.For<ISigner>();
IForensicsProcessor forensicsProcessor = Substitute.For<IForensicsProcessor>();
IBlockInfoValidator blockInfoValidator = new BlockInfoValidator();

var voteManager = new VotesManager(context, blockTree, epochSwitchManager, snapshotManager, quorumCertificateManager,
specProvider, signer, forensicsProcessor, blockInfoValidator);

foreach (var v in votes)
await voteManager.HandleVote(v);

quorumCertificateManager.Received(expectedCalls).CommitCertificate(Arg.Any<QuorumCertificate>());
}

[Test]
public async Task HandleVote_HeaderMissing_ReturnsEarly()
{
var (keys, _) = MakeKeys(20);
var masternodes = keys.Select(k => k.Address).ToArray();

ulong currentRound = 1;
var context = new XdcContext { CurrentRound = currentRound };
IBlockTree blockTree = Substitute.For<IBlockTree>();
XdcBlockHeader header = Build.A.XdcBlockHeader()
.WithExtraConsensusData(new ExtraFieldsV2(currentRound, new QuorumCertificate(new BlockRoundInfo(Hash256.Zero, 0, 0), null, 450)))
.TestObject;

var info = new BlockRoundInfo(header.Hash!, currentRound, header.Number);
IEpochSwitchManager epochSwitchManager = Substitute.For<IEpochSwitchManager>();
var epochSwitchInfo = new EpochSwitchInfo(masternodes, [], [], info);
epochSwitchManager
.GetEpochSwitchInfo(header)
.Returns(epochSwitchInfo);

ISnapshotManager snapshotManager = Substitute.For<ISnapshotManager>();
IQuorumCertificateManager quorumCertificateManager = Substitute.For<IQuorumCertificateManager>();
ISpecProvider specProvider = Substitute.For<ISpecProvider>();
IXdcReleaseSpec xdcReleaseSpec = Substitute.For<IXdcReleaseSpec>();
xdcReleaseSpec.CertThreshold.Returns(0.667);
specProvider.GetSpec(Arg.Any<ForkActivation>()).Returns(xdcReleaseSpec);

ISigner signer = Substitute.For<ISigner>();
IForensicsProcessor forensicsProcessor = Substitute.For<IForensicsProcessor>();
IBlockInfoValidator blockInfoValidator = new BlockInfoValidator();

var voteManager = new VotesManager(context, blockTree, epochSwitchManager, snapshotManager, quorumCertificateManager,
specProvider, signer, forensicsProcessor, blockInfoValidator);

var keysForVotes = keys.ToArray();
for (var i = 0; i < keysForVotes.Length - 1; i++)
await voteManager.HandleVote(BuildSignedVote(info, gap: 450, keysForVotes[i]));

quorumCertificateManager.DidNotReceive().CommitCertificate(Arg.Any<QuorumCertificate>());

// Now insert header and send one more
blockTree.FindHeader(header.Hash!, Arg.Any<long>()).Returns(header);
await voteManager.HandleVote(BuildSignedVote(info, 450, keysForVotes[keysForVotes.Length - 1]));

quorumCertificateManager.Received(1).CommitCertificate(Arg.Any<QuorumCertificate>());
}

[TestCase(5UL, 5UL, 5UL, false)] // Current round already voted
[TestCase(5UL, 4UL, 4UL, false)] // Current round different from blockInfoRound
[TestCase(5UL, 4UL, 5UL, true)] // No LockQc
public void VerifyVotingRules_FirstChecks_ReturnsExpected(ulong currentRound, ulong highestVotedRound, ulong blockInfoRound, bool expected)
{
var ctx = new XdcContext { CurrentRound = currentRound, HighestVotedRound = highestVotedRound };
VotesManager votesManager = BuildVoteManager(ctx);

var blockInfo = new BlockRoundInfo(Hash256.Zero, blockInfoRound, 100);
var qc = new QuorumCertificate(blockInfo, null, 0);

Assert.That(votesManager.VerifyVotingRules(blockInfo, qc), Is.EqualTo(expected));
}

[Test]
public void VerifyVotingRules_QcNewerThanLockQc_ReturnsTrue()
{
var lockQc = new QuorumCertificate(new BlockRoundInfo(Hash256.Zero, 4, 99), null, 0);
var ctx = new XdcContext { CurrentRound = 5, HighestVotedRound = 4, LockQC = lockQc };
VotesManager votesManager = BuildVoteManager(ctx);

var blockInfo = new BlockRoundInfo(Hash256.Zero, 5, 100);
var qc = new QuorumCertificate(blockInfo, null, 0);

Assert.That(votesManager.VerifyVotingRules(blockInfo, qc), Is.True);
}

public static IEnumerable<TestCaseData> ExtendingFromAncestorCases()
{
XdcBlockHeader[] headers = GenerateBlockHeaders(3, 99);
IBlockTree blockTree = Substitute.For<IBlockTree>();
var headerByHash = headers.ToDictionary(h => h.Hash!, h => h);

XdcBlockHeader nonRelatedHeader = Build.A.XdcBlockHeader().WithNumber(99).TestObject;
nonRelatedHeader.Hash ??= nonRelatedHeader.CalculateHash().ToHash256();
headerByHash[nonRelatedHeader.Hash] = nonRelatedHeader;

blockTree.FindHeader(Arg.Any<Hash256>()).Returns(args =>
{
var hash = (Hash256)args[0];
return headerByHash.TryGetValue(hash, out var header) ? header : null;
});

var blockInfo = new BlockRoundInfo(headers[2].Hash!, 5, headers[2].Number);

var ancestorQc = new QuorumCertificate(new BlockRoundInfo(headers[0].Hash!, 3, headers[0].Number), null, 0);
yield return new TestCaseData(blockTree, ancestorQc, blockInfo, true);

var nonRelatedQc = new QuorumCertificate(new BlockRoundInfo(nonRelatedHeader.Hash, 3, nonRelatedHeader.Number), null, 0);
yield return new TestCaseData(blockTree, nonRelatedQc, blockInfo, false);
}

[TestCaseSource(nameof(ExtendingFromAncestorCases))]
public void VerifyVotingRules_CheckExtendingFromAncestor_ReturnsExpected(IBlockTree tree, QuorumCertificate lockQc, BlockRoundInfo blockInfo, bool expected)
{
var ctx = new XdcContext { CurrentRound = 5, HighestVotedRound = 4, LockQC = lockQc };
VotesManager votesManager = BuildVoteManager(ctx, tree);
var qc = new QuorumCertificate(new BlockRoundInfo(Hash256.Zero, 3, 99), null, 0);

Assert.That(votesManager.VerifyVotingRules(blockInfo, qc), Is.EqualTo(expected));
}

private static (PrivateKey[] keys, Address[] addrs) MakeKeys(int n)
{
var keyBuilder = new PrivateKeyGenerator();
PrivateKey[] keys = keyBuilder.Generate(n).ToArray();
Address[] addrs = keys.Select(k => k.Address).ToArray();
return (keys, addrs);
}

private static Vote BuildSignedVote(
BlockRoundInfo info, ulong gap, PrivateKey key)
{
var decoder = new VoteDecoder();
var ecdsa = new EthereumEcdsa(0);
var vote = new Vote(info, gap);
var stream = new KeccakRlpStream();
decoder.Encode(stream, vote, RlpBehaviors.ForSealing);
vote.Signature = ecdsa.Sign(key, stream.GetValueHash());
vote.Signer = key.Address;
return vote;
}

private static VotesManager BuildVoteManager(XdcContext ctx, IBlockTree? blockTree = null)
{
blockTree ??= Substitute.For<IBlockTree>();
IEpochSwitchManager epochSwitchManager = Substitute.For<IEpochSwitchManager>();
ISnapshotManager snapshotManager = Substitute.For<ISnapshotManager>();
IQuorumCertificateManager quorumCertificateManager = Substitute.For<IQuorumCertificateManager>();
ISpecProvider specProvider = Substitute.For<ISpecProvider>();
ISigner signer = Substitute.For<ISigner>();
IForensicsProcessor forensicsProcessor = Substitute.For<IForensicsProcessor>();
IBlockInfoValidator blockInfoValidator = Substitute.For<IBlockInfoValidator>();

return new VotesManager(ctx, blockTree, epochSwitchManager, snapshotManager, quorumCertificateManager,
specProvider, signer, forensicsProcessor, blockInfoValidator);
}

private static XdcBlockHeader[] GenerateBlockHeaders(int n, long blockNumber)
{
var headers = new XdcBlockHeader[n];
var parentHash = Hash256.Zero;
var number = blockNumber;
for (int i = 0; i < n; i++, number++)
{
headers[i] = Build.A.XdcBlockHeader()
.WithNumber(number)
.WithParentHash(parentHash)
.TestObject;
parentHash = headers[i].CalculateHash().ToHash256();
}

return headers;
}
}
19 changes: 19 additions & 0 deletions src/Nethermind/Nethermind.Xdc/BlockInfoValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using Nethermind.Xdc.Types;

namespace Nethermind.Xdc;

public interface IBlockInfoValidator
{
bool ValidateBlockInfo(BlockRoundInfo blockInfo, XdcBlockHeader blockHeader);
}

public class BlockInfoValidator : IBlockInfoValidator
{
public bool ValidateBlockInfo(BlockRoundInfo blockInfo, XdcBlockHeader blockHeader) =>
(blockInfo.BlockNumber == blockHeader.Number)
&& (blockInfo.Hash == blockHeader.Hash)
&& (blockInfo.Round == blockHeader.ExtraConsensusData.CurrentRound);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,9 @@

using Nethermind.Blockchain;
using Nethermind.Xdc.Types;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Nethermind.Xdc.Errors;
internal class QuorumCertificateException : BlockchainException
internal class QuorumCertificateException(QuorumCertificate certificate, string message) : BlockchainException(message)
{
public QuorumCertificateException(QuorumCertificate certificate, string message) : base(message)
{
Certificate = certificate;
}

public QuorumCertificate Certificate { get; }
public QuorumCertificate Certificate { get; } = certificate;
}
15 changes: 0 additions & 15 deletions src/Nethermind/Nethermind.Xdc/IBlockInfoValidator.cs

This file was deleted.

6 changes: 0 additions & 6 deletions src/Nethermind/Nethermind.Xdc/IEpochSwitchManager.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using Nethermind.Blockchain;
using Nethermind.Xdc.Types;
using Nethermind.Core.Crypto;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Nethermind.Xdc;
public interface IEpochSwitchManager
Expand Down
2 changes: 1 addition & 1 deletion src/Nethermind/Nethermind.Xdc/IForensicsProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public interface IForensicsProcessor

Task ProcessVoteEquivocation(Vote incomingVote);

Task DetectEquivocationInVotePool(Vote vote, List<Vote> votePool);
Task DetectEquivocationInVotePool(Vote vote, IEnumerable<Vote> votePool);

Task SendVoteEquivocationProof(Vote vote1, Vote vote2, Address signer);
}
7 changes: 0 additions & 7 deletions src/Nethermind/Nethermind.Xdc/IPenaltyHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,6 @@

using Nethermind.Core;
using Nethermind.Core.Crypto;
using Nethermind.Evm.State;
using Nethermind.Xdc.Types;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Nethermind.Xdc;
public interface IPenaltyHandler
Expand Down
Loading