Skip to content
Merged
Show file tree
Hide file tree
Changes from 42 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 @@ -90,6 +90,12 @@ public XdcBlockHeaderBuilder WithExtraConsensusData(ExtraFieldsV2 extraFieldsV2)
return this;
}

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

public new XdcBlockHeaderBuilder WithHash(Hash256 hash256)
{
TestObjectInternal.Hash = hash256;
Expand Down
151 changes: 151 additions & 0 deletions src/Nethermind/Nethermind.Xdc.Test/VotesManagerTests.cs
Comment thread
cicr99 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// 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> VoteCases()
{
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(VoteCases))]
public async Task VoteHandler_ExpectedCallsToCommitQc(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>()).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>();

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

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

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

[Test]
public async Task VoteHandler_Returns_Early_When_Header_Missing()
Comment thread
cicr99 marked this conversation as resolved.
Outdated
{
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>();

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

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!).Returns(header);
await voteManager.HandleVote(BuildSignedVote(info, 450, keysForVotes[keysForVotes.Length - 1]));

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

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;
}
}
13 changes: 13 additions & 0 deletions src/Nethermind/Nethermind.Xdc/BlockInfoValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using Nethermind.Xdc.Types;

namespace Nethermind.Xdc;

public static class BlockInfoValidator
Comment thread
cicr99 marked this conversation as resolved.
Outdated
{
public static 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
7 changes: 1 addition & 6 deletions src/Nethermind/Nethermind.Xdc/IVotesManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,13 @@
// SPDX-License-Identifier: LGPL-3.0-only

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

namespace Nethermind.Xdc;
internal interface IVotesManager
{
Task CastVote(BlockRoundInfo blockInfo);
Task HandleVote(Vote vote);
Task VerifyVotes(List<Vote> votes, XdcBlockHeader header);
Task OnReceiveVote(Vote vote);
bool VerifyVotingRules(BlockRoundInfo blockInfo, QuorumCertificate qc);
List<Vote> GetVotes();
}
24 changes: 3 additions & 21 deletions src/Nethermind/Nethermind.Xdc/QuorumCertificateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,16 @@
// SPDX-License-Identifier: LGPL-3.0-only

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

using Microsoft.Extensions.Logging;
using Nethermind.Blockchain;
using Nethermind.Config;
using Nethermind.Core;
using Nethermind.Core.Crypto;
using Nethermind.Core.Specs;
using Nethermind.Crypto;
using Nethermind.Db;
using Nethermind.Logging;
using Nethermind.Serialization.Rlp;
using Nethermind.Xdc;
using Nethermind.Xdc.Errors;
using Nethermind.Xdc.Spec;
using Nethermind.Xdc.Types;
Expand Down Expand Up @@ -61,7 +54,7 @@ public void CommitCertificate(QuorumCertificate qc)
if (proposedBlockHeader is null)
throw new InvalidBlockException(proposedBlockHeader, "Proposed block header not found in chain");

//TODO this could be wrong way of fetching spec if a release spec is defined on a round basis
//TODO this could be wrong way of fetching spec if a release spec is defined on a round basis
IXdcReleaseSpec spec = _specProvider.GetXdcSpec(proposedBlockHeader);
Comment thread
cicr99 marked this conversation as resolved.
Outdated

//Can only look for a QC in proposed block after the switch block
Expand Down Expand Up @@ -107,7 +100,7 @@ private void SaveQc(QuorumCertificate qc, long key)
private bool CommitBlock(IBlockTree chain, XdcBlockHeader proposedBlockHeader, ulong proposedRound, QuorumCertificate proposedQuorumCert)
{
IXdcReleaseSpec spec = _specProvider.GetXdcSpec(proposedBlockHeader);
//Can only commit a QC if the proposed block is at least 2 blocks after the switch block, since we want to check grand parent of proposed QC
//Can only commit a QC if the proposed block is at least 2 blocks after the switch block, since we want to check grandparent of proposed QC
if ((proposedBlockHeader.Number - 2) <= spec.SwitchBlock)
return false;

Expand Down Expand Up @@ -196,7 +189,7 @@ public bool VerifyCertificate(QuorumCertificate qc, XdcBlockHeader parentHeader,
return false;
}

if (!ValidateBlockInfo(qc, parentHeader))
if (!BlockInfoValidator.ValidateBlockInfo(qc.ProposedBlockInfo, parentHeader))
{
error = "QC block data does not match header data.";
return false;
Expand All @@ -205,15 +198,4 @@ public bool VerifyCertificate(QuorumCertificate qc, XdcBlockHeader parentHeader,
error = null;
return true;
}

private bool ValidateBlockInfo(QuorumCertificate qc, XdcBlockHeader parentHeader)
{
if (qc.ProposedBlockInfo.BlockNumber != parentHeader.Number)
return false;
if (qc.ProposedBlockInfo.Hash != parentHeader.Hash)
return false;
if (qc.ProposedBlockInfo.Round != parentHeader.ExtraConsensusData.CurrentRound)
return false;
return true;
}
}
1 change: 1 addition & 0 deletions src/Nethermind/Nethermind.Xdc/Spec/XdcReleaseSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class XdcReleaseSpec : ReleaseSpec, IXdcReleaseSpec
public int MinimumSigningTx { get; set; } // Signing txs that a node needs to produce to get out of penalty, after `LimitPenaltyEpoch`
public List<V2ConfigParams> V2Configs { get; set; }


public void ApplyV2Config(ulong round)
{
V2ConfigParams configParams = GetConfigAtRound(V2Configs, round);
Expand Down
2 changes: 2 additions & 0 deletions src/Nethermind/Nethermind.Xdc/Types/Vote.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using Nethermind.Core;
using Nethermind.Core.Crypto;
using RlpBehaviors = Nethermind.Serialization.Rlp.RlpBehaviors;

Expand All @@ -12,6 +13,7 @@ public class Vote(BlockRoundInfo proposedBlockInfo, ulong gapNumber, Signature s
public BlockRoundInfo ProposedBlockInfo { get; set; } = proposedBlockInfo;
public ulong GapNumber { get; set; } = gapNumber;
public Signature? Signature { get; set; } = signature;
public Address? Signer { get; set; }

public override string ToString() =>
$"{ProposedBlockInfo.Round}:{GapNumber}:{ProposedBlockInfo.BlockNumber}";
Expand Down
14 changes: 0 additions & 14 deletions src/Nethermind/Nethermind.Xdc/Types/VoteForSign.cs

This file was deleted.

Loading