Skip to content

Commit bb16311

Browse files
ak88batrrclaude[bot]
authored
Feature/subnet seal validator (#11294)
* subnet block building * coding style * Apply suggestions from code review Co-authored-by: batrr <45632769+batrr@users.noreply.github.com> * seal validator * test change * fixes * removed usings * fixes * Update src/Nethermind/Nethermind.Xdc/XdcSealValidator.cs Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> * fixed check on penalties * format * preserve seal error * compare list order ignore order * tests * format --------- Co-authored-by: batrr <45632769+batrr@users.noreply.github.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
1 parent 1c8c785 commit bb16311

8 files changed

Lines changed: 464 additions & 50 deletions

File tree

src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcSubnetBlockHeaderBuilder.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,35 @@ public class XdcSubnetBlockHeaderBuilder : XdcBlockHeaderBuilder
1414

1515
public new XdcSubnetBlockHeader TestObject => (XdcSubnetBlockHeader)base.TestObject;
1616

17+
public new XdcSubnetBlockHeaderBuilder WithParent(BlockHeader parentHeader)
18+
{
19+
base.WithParent(parentHeader);
20+
return this;
21+
}
22+
23+
public new XdcSubnetBlockHeaderBuilder WithNumber(long blockNumber)
24+
{
25+
base.WithNumber(blockNumber);
26+
return this;
27+
}
28+
29+
public new XdcSubnetBlockHeaderBuilder WithTimestamp(ulong timestamp)
30+
{
31+
base.WithTimestamp(timestamp);
32+
return this;
33+
}
34+
35+
public new XdcSubnetBlockHeaderBuilder WithMixHash(Hash256 mixHash)
36+
{
37+
base.WithMixHash(mixHash);
38+
return this;
39+
}
40+
41+
public new XdcSubnetBlockHeaderBuilder WithGeneratedExtraConsensusData(int signatureNumber = 72)
42+
{
43+
base.WithGeneratedExtraConsensusData(signatureNumber);
44+
return this;
45+
}
1746

1847
public XdcSubnetBlockHeaderBuilder() =>
1948
TestObjectInternal = new XdcSubnetBlockHeader(
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
2+
// SPDX-License-Identifier: LGPL-3.0-only
3+
4+
using Nethermind.Core;
5+
using Nethermind.Core.Crypto;
6+
using Nethermind.Core.Specs;
7+
using Nethermind.Core.Test.Builders;
8+
using Nethermind.Xdc.Spec;
9+
using Nethermind.Xdc.Types;
10+
using NSubstitute;
11+
using NUnit.Framework;
12+
using System;
13+
14+
namespace Nethermind.Xdc.Test;
15+
16+
[Parallelizable(ParallelScope.All)]
17+
public class XdcSubnetSealValidatorTests
18+
{
19+
private const int Epoch = 900;
20+
private const int Gap = 450;
21+
22+
[Test]
23+
public void EpochSwitch_PenaltiesInSnapshotButNotInHeader_HeaderIsStillValid()
24+
{
25+
(IXdcReleaseSpec _, ISpecProvider specProvider) = CreateSubnetSpec();
26+
Address[] candidates = [Address.FromNumber(1)];
27+
Address[] penalties = [Address.FromNumber(2)];
28+
ISubnetMasternodesCalculator calculator = Substitute.For<ISubnetMasternodesCalculator>();
29+
calculator.CalculateNextEpochMasternodes(Arg.Any<long>(), Arg.Any<Hash256>(), Arg.Any<IXdcReleaseSpec>()).Returns((candidates, penalties));
30+
XdcSubnetSealValidator validator = CreateValidator(specProvider, calculator, CreateEpochSwitchManager(true));
31+
XdcSubnetBlockHeader parent = BuildParentHeader(899);
32+
XdcSubnetBlockHeader header = BuildSubnetHeader(parent, 900, 110,
33+
b => b.WithValidators(candidates)
34+
.WithAuthor(candidates[0]));
35+
36+
bool ok = validator.ValidateParams(parent, header, out string? error);
37+
38+
Assert.That(ok, Is.True);
39+
}
40+
41+
[Test]
42+
public void NonGapPlusOne_WithNextValidators_Invalid()
43+
{
44+
(IXdcReleaseSpec _, ISpecProvider specProvider) = CreateSubnetSpec();
45+
XdcSubnetSealValidator validator = CreateValidator(specProvider);
46+
47+
XdcSubnetBlockHeader parent = BuildParentHeader(901);
48+
XdcSubnetBlockHeader header = BuildSubnetHeader(parent, 902, 110,
49+
b => b.WithNextValidators([Address.FromNumber(1)]));
50+
51+
bool ok = validator.ValidateParams(parent, header, out string? error);
52+
53+
Assert.That(ok, Is.False);
54+
Assert.That(error, Does.Contain("NextValidators"));
55+
}
56+
57+
[Test]
58+
public void NonGapPlusOne_WithPenalties_Invalid()
59+
{
60+
(IXdcReleaseSpec _, ISpecProvider specProvider) = CreateSubnetSpec();
61+
XdcSubnetSealValidator validator = CreateValidator(specProvider);
62+
63+
XdcSubnetBlockHeader parent = BuildParentHeader(901);
64+
XdcSubnetBlockHeader header = BuildSubnetHeader(parent, 902, 110,
65+
b => b.WithPenalties([Address.FromNumber(1)]));
66+
67+
bool ok = validator.ValidateParams(parent, header, out string? error);
68+
69+
Assert.That(ok, Is.False);
70+
Assert.That(error, Does.Contain("Penalties"));
71+
}
72+
73+
[Test]
74+
public void GapPlusOne_NextValidatorsMismatch_Invalid()
75+
{
76+
(IXdcReleaseSpec _, ISpecProvider specProvider) = CreateSubnetSpec();
77+
Address[] candidates = [Address.FromNumber(1), Address.FromNumber(2)];
78+
79+
ISubnetMasternodesCalculator calculator = Substitute.For<ISubnetMasternodesCalculator>();
80+
calculator.GetNextEpochCandidatesAndPenalties(Arg.Any<Hash256>()).Returns((candidates, Array.Empty<Address>()));
81+
82+
XdcSubnetSealValidator validator = CreateValidator(specProvider, masternodesCalculator: calculator);
83+
84+
XdcSubnetBlockHeader parent = BuildParentHeader(450);
85+
XdcSubnetBlockHeader header = BuildSubnetHeader(parent, 451, 110,
86+
b => b.WithNextValidators([Address.FromNumber(99)])); // wrong candidates
87+
88+
bool ok = validator.ValidateParams(parent, header, out string? error);
89+
90+
Assert.That(ok, Is.False);
91+
Assert.That(error, Does.Contain("NextValidators"));
92+
}
93+
94+
[Test]
95+
public void GapPlusOne_PenaltiesMismatch_Invalid()
96+
{
97+
(IXdcReleaseSpec _, ISpecProvider specProvider) = CreateSubnetSpec();
98+
Address[] candidates = [Address.FromNumber(1)];
99+
Address[] penalties = [Address.FromNumber(2)];
100+
101+
ISubnetMasternodesCalculator calculator = Substitute.For<ISubnetMasternodesCalculator>();
102+
calculator.GetNextEpochCandidatesAndPenalties(Arg.Any<Hash256>()).Returns((candidates, penalties));
103+
104+
XdcSubnetSealValidator validator = CreateValidator(specProvider, masternodesCalculator: calculator);
105+
106+
XdcSubnetBlockHeader parent = BuildParentHeader(450);
107+
// NextValidators matches but Penalties on header is empty while snapshot expects [addr2]
108+
XdcSubnetBlockHeader header = BuildSubnetHeader(parent, 451, 110,
109+
b => b.WithNextValidators(candidates));
110+
111+
bool ok = validator.ValidateParams(parent, header, out string? error);
112+
113+
Assert.That(ok, Is.False);
114+
Assert.That(error, Does.Contain("Penalties"));
115+
}
116+
117+
[Test]
118+
public void ListsAreEqual_BothEmpty_ReturnsTrue()
119+
{
120+
Address[] a = [];
121+
Address[] b = [];
122+
123+
Assert.That(a.ListsAreEqual(b), Is.True);
124+
}
125+
126+
[Test]
127+
public void ListsAreEqual_SameElementsSameOrder_ReturnsTrue()
128+
{
129+
Address[] a = [Address.FromNumber(1), Address.FromNumber(2)];
130+
Address[] b = [Address.FromNumber(1), Address.FromNumber(2)];
131+
132+
Assert.That(a.ListsAreEqual(b), Is.True);
133+
}
134+
135+
[Test]
136+
public void ListsAreEqual_SameElementsDifferentOrder_ReturnsTrue()
137+
{
138+
Address[] a = [Address.FromNumber(1), Address.FromNumber(2)];
139+
Address[] b = [Address.FromNumber(2), Address.FromNumber(1)];
140+
141+
Assert.That(a.ListsAreEqual(b), Is.True);
142+
}
143+
144+
[Test]
145+
public void ListsAreEqual_DifferentCounts_ReturnsFalse()
146+
{
147+
Address[] a = [Address.FromNumber(1)];
148+
Address[] b = [Address.FromNumber(1), Address.FromNumber(2)];
149+
150+
Assert.That(a.ListsAreEqual(b), Is.False);
151+
}
152+
153+
[Test]
154+
public void ListsAreEqual_SameCountDifferentElements_ReturnsFalse()
155+
{
156+
Address[] a = [Address.FromNumber(1), Address.FromNumber(2)];
157+
Address[] b = [Address.FromNumber(1), Address.FromNumber(3)];
158+
159+
Assert.That(a.ListsAreEqual(b), Is.False);
160+
}
161+
162+
[Test]
163+
public void GapPlusOne_MatchingSnapshot_Valid()
164+
{
165+
(IXdcReleaseSpec _, ISpecProvider specProvider) = CreateSubnetSpec();
166+
Address[] candidates = [Address.FromNumber(1)];
167+
Address[] penalties = [Address.FromNumber(2)];
168+
169+
ISubnetMasternodesCalculator calculator = Substitute.For<ISubnetMasternodesCalculator>();
170+
// Empty penalties so header's empty Penalties field also matches
171+
calculator.GetNextEpochCandidatesAndPenalties(Arg.Any<Hash256>()).Returns((candidates, penalties));
172+
173+
XdcSubnetSealValidator validator = CreateValidator(specProvider, masternodesCalculator: calculator);
174+
175+
XdcSubnetBlockHeader parent = BuildParentHeader(450);
176+
XdcSubnetBlockHeader header = BuildSubnetHeader(parent, 451, 110,
177+
b => b.WithNextValidators(candidates).WithPenalties(penalties));
178+
179+
bool ok = validator.ValidateParams(parent, header, out string? error);
180+
181+
Assert.That(ok, Is.True, error);
182+
}
183+
184+
private static (IXdcReleaseSpec Spec, ISpecProvider SpecProvider) CreateSubnetSpec()
185+
{
186+
IXdcReleaseSpec releaseSpec = Substitute.For<IXdcReleaseSpec>();
187+
releaseSpec.EpochLength.Returns(Epoch);
188+
releaseSpec.Gap.Returns(Gap);
189+
releaseSpec.MinePeriod.Returns(10);
190+
releaseSpec.GasLimitBoundDivisor.Returns(1024);
191+
releaseSpec.When(x => x.ApplyV2Config(Arg.Any<ulong>())).Do(_ => { });
192+
193+
ISpecProvider specProvider = Substitute.For<ISpecProvider>();
194+
specProvider.GetSpec(Arg.Any<ForkActivation>()).Returns(releaseSpec);
195+
return (releaseSpec, specProvider);
196+
}
197+
198+
// Returns a mock where IsEpochSwitchAtBlock → isEpochSwitch, and
199+
// GetEpochSwitchInfo → masternodes=[Address.Zero] so the leader check passes
200+
// when header.Author == Address.Zero (set in BuildSubnetHeader).
201+
private static IEpochSwitchManager CreateEpochSwitchManager(bool isEpochSwitch = false)
202+
{
203+
IEpochSwitchManager esm = Substitute.For<IEpochSwitchManager>();
204+
esm.IsEpochSwitchAtBlock(Arg.Any<XdcBlockHeader>()).Returns(isEpochSwitch);
205+
esm.GetEpochSwitchInfo(Arg.Any<XdcBlockHeader>())
206+
.Returns(new EpochSwitchInfo([Address.Zero], [], [], new BlockRoundInfo(Hash256.Zero, 0, 0)));
207+
return esm;
208+
}
209+
210+
private static XdcSubnetSealValidator CreateValidator(
211+
ISpecProvider specProvider,
212+
ISubnetMasternodesCalculator? masternodesCalculator = null,
213+
IEpochSwitchManager? epochSwitchManager = null) => new(
214+
masternodesCalculator ?? Substitute.For<ISubnetMasternodesCalculator>(),
215+
epochSwitchManager ?? CreateEpochSwitchManager(),
216+
specProvider);
217+
218+
private static XdcSubnetBlockHeader BuildParentHeader(long number)
219+
{
220+
XdcSubnetBlockHeaderBuilder b = Build.A.XdcSubnetBlockHeader();
221+
b.WithNumber(number);
222+
b.WithTimestamp(100);
223+
b.WithMixHash(Hash256.Zero);
224+
b.WithValidators(Array.Empty<Address>());
225+
b.WithPenalties(Array.Empty<byte>());
226+
b.WithNextValidators(Array.Empty<byte>());
227+
return b.TestObject;
228+
}
229+
230+
private static XdcSubnetBlockHeader BuildSubnetHeader(
231+
BlockHeader parent,
232+
long number,
233+
ulong timestamp,
234+
Action<XdcSubnetBlockHeaderBuilder>? configure = null)
235+
{
236+
XdcSubnetBlockHeaderBuilder b = Build.A.XdcSubnetBlockHeader();
237+
b.WithParent(parent);
238+
b.WithNumber(number);
239+
b.WithTimestamp(timestamp);
240+
// BlockRound=2 > QC.ProposedBlockInfo.Round=1 passes the base-class round check
241+
b.WithExtraConsensusData(new ExtraFieldsV2(2, new QuorumCertificate(new BlockRoundInfo(Hash256.Zero, 1, 1), [], 450)));
242+
b.WithMixHash(Hash256.Zero);
243+
b.WithValidators(Array.Empty<Address>());
244+
b.WithPenalties(Array.Empty<byte>());
245+
b.WithNextValidators(Array.Empty<byte>());
246+
// currentLeaderIndex = BlockRound(2) % EpochLength(900) % masternodes.Length(1) = 0
247+
// masternodes[0] = Address.Zero in CreateEpochSwitchManager → must match header.Author
248+
b.WithAuthor(Address.Zero);
249+
configure?.Invoke(b);
250+
return b.TestObject;
251+
}
252+
}

src/Nethermind/Nethermind.Xdc/MasternodesCalculator.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,7 @@ internal class MasternodesCalculator(ISnapshotManager snapshotManager, IPenaltyH
1515
public (Address[] Masternodes, Address[] PenalizedNodes) CalculateNextEpochMasternodes(long blockNumber, Hash256 parentHash, IXdcReleaseSpec spec)
1616
{
1717
int maxMasternodes = spec.MaxMasternodes;
18-
Snapshot previousSnapshot = snapshotManager.GetSnapshotByBlockNumber(blockNumber, spec);
19-
20-
if (previousSnapshot is null)
21-
throw new InvalidOperationException($"No snapshot found for header #{blockNumber}");
22-
18+
Snapshot previousSnapshot = snapshotManager.GetSnapshotByBlockNumber(blockNumber, spec) ?? throw new InvalidOperationException($"No snapshot found for header #{blockNumber}");
2319
Address[] candidates = previousSnapshot.NextEpochCandidates;
2420

2521
if (blockNumber == spec.SwitchBlock + 1)

src/Nethermind/Nethermind.Xdc/XdcExtensions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Nethermind.Xdc.Spec;
1010
using Nethermind.Xdc.Types;
1111
using System;
12+
using System.Collections.Generic;
1213

1314
namespace Nethermind.Xdc;
1415

@@ -106,4 +107,12 @@ public static bool IsGapPlusOne(this XdcSubnetBlockHeader header, IXdcReleaseSpe
106107
return true;
107108
return (header.Number % spec.EpochLength) == (spec.EpochLength - spec.Gap + 1);
108109
}
110+
111+
/// <summary>
112+
/// Compares two lists of addresses for equality, ignoring order since the order of masternodes in XDC header validation does not matter.
113+
/// </summary>
114+
/// <param name="a"></param>
115+
/// <param name="b"></param>
116+
/// <returns>Returns <see cref="true"/> if the lists contain the same addresses, ignoring order; otherwise, <see cref="false"/>.</returns>
117+
public static bool ListsAreEqual(this IList<Address> a, IList<Address> b) => a.Count == b.Count && new HashSet<Address>(a).SetEquals(b);
109118
}

src/Nethermind/Nethermind.Xdc/XdcHeaderValidator.cs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@
1414

1515
namespace Nethermind.Xdc;
1616

17-
public class XdcHeaderValidator(IBlockTree blockTree, IQuorumCertificateManager quorumCertificateManager, ISealValidator sealValidator, ISpecProvider specProvider, ILogManager? logManager = null) : HeaderValidator(blockTree, sealValidator, specProvider, logManager)
17+
public class XdcHeaderValidator(
18+
IBlockTree blockTree,
19+
IQuorumCertificateManager quorumCertificateManager,
20+
ISealValidator sealValidator,
21+
ISpecProvider specProvider,
22+
ILogManager? logManager = null) : HeaderValidator(blockTree, sealValidator, specProvider, logManager)
1823
{
1924
protected override bool Validate<TOrphaned>(BlockHeader header, BlockHeader parent, bool isUncle, out string? error)
2025
{
@@ -85,14 +90,19 @@ protected override bool ValidateSeal(BlockHeader header, BlockHeader parent, boo
8590
return false;
8691
}
8792

88-
if (_sealValidator is XdcSealValidator xdcSealValidator ?
89-
!xdcSealValidator.ValidateParams(parent, header, out error) :
90-
!_sealValidator.ValidateParams(parent, header, isUncle))
93+
if (_sealValidator is XdcSealValidator xdcSealValidator)
9194
{
92-
error = "Invalid consensus data in header.";
93-
return false;
95+
if (!xdcSealValidator.ValidateParams(parent, header, out error))
96+
return false;
97+
}
98+
else
99+
{
100+
if (!_sealValidator.ValidateParams(parent, header, isUncle))
101+
{
102+
error = "Invalid consensus data in header.";
103+
return false;
104+
}
94105
}
95-
96106
return true;
97107
}
98108

0 commit comments

Comments
 (0)