Skip to content

Commit b7cd74f

Browse files
authored
XDC Epoch switch manager (#11140)
* epoch switch manager * feat: enhance epoch switch manager with new methods and optimizations * fix typo * address review comments * address review comments * address review comments * fix: handle null cases for XdcBlockHeader in epoch switch logic * test: update assertions in SubnetEpochSwitchManagerTests for penalties validation * feat: implement GetCurrentEpochNumber method in epoch switch managers * feat: add documentation and import fix * feat: simplify ResolvePenalties method by removing IXdcReleaseSpec parameter * feat: optimize candidate selection logic in BaseEpochSwitchManager
1 parent 80bfb06 commit b7cd74f

5 files changed

Lines changed: 410 additions & 145 deletions

File tree

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
2+
// SPDX-License-Identifier: LGPL-3.0-only
3+
4+
using System;
5+
using Nethermind.Blockchain;
6+
using Nethermind.Core;
7+
using Nethermind.Core.Specs;
8+
using Nethermind.Core.Test.Builders;
9+
using Nethermind.Xdc.Spec;
10+
using Nethermind.Xdc.Types;
11+
using NSubstitute;
12+
using NUnit.Framework;
13+
14+
namespace Nethermind.Xdc.Test;
15+
16+
internal class SubnetEpochSwitchManagerTests
17+
{
18+
private IEpochSwitchManager _epochSwitchManager;
19+
private IBlockTree _tree;
20+
private ISpecProvider _config;
21+
private ISnapshotManager _snapshotManager;
22+
23+
[SetUp]
24+
public void Setup()
25+
{
26+
_tree = Substitute.For<IBlockTree>();
27+
_config = Substitute.For<ISpecProvider>();
28+
_snapshotManager = Substitute.For<ISnapshotManager>();
29+
_epochSwitchManager = new SubnetEpochSwitchManager(_config, _tree, _snapshotManager);
30+
}
31+
32+
[TestCase(20L, 10, true)]
33+
[TestCase(5L, 10, false)]
34+
public void IsEpochSwitchAtBlock_BlockNumberBased(long blockNumber, int epochLength, bool expected)
35+
{
36+
XdcReleaseSpec releaseSpec = new()
37+
{
38+
EpochLength = epochLength,
39+
V2Configs = [new V2ConfigParams()]
40+
};
41+
_config.GetSpec(Arg.Any<ForkActivation>()).Returns(releaseSpec);
42+
43+
XdcSubnetBlockHeaderBuilder builder = Build.A.XdcSubnetBlockHeader();
44+
builder.WithNumber(blockNumber);
45+
XdcSubnetBlockHeader header = builder.TestObject;
46+
47+
Assert.That(_epochSwitchManager.IsEpochSwitchAtBlock(header), Is.EqualTo(expected));
48+
}
49+
50+
[TestCase(9L, 10, true)] // parent.Number + 1 = 10, 10 % 10 == 0
51+
[TestCase(5L, 10, false)] // parent.Number + 1 = 6
52+
public void IsEpochSwitchAtRound_DerivedFromParentBlockNumber(long parentNumber, int epochLength, bool expected)
53+
{
54+
XdcReleaseSpec releaseSpec = new()
55+
{
56+
EpochLength = epochLength,
57+
V2Configs = [new V2ConfigParams()]
58+
};
59+
_config.GetSpec(Arg.Any<ForkActivation>()).Returns(releaseSpec);
60+
61+
XdcSubnetBlockHeaderBuilder builder = Build.A.XdcSubnetBlockHeader();
62+
builder.WithNumber(parentNumber);
63+
XdcSubnetBlockHeader parent = builder.TestObject;
64+
65+
// currentRound is deliberately varied — subnet ignores it
66+
Assert.That(_epochSwitchManager.IsEpochSwitchAtRound(0, parent), Is.EqualTo(expected));
67+
Assert.That(_epochSwitchManager.IsEpochSwitchAtRound(999, parent), Is.EqualTo(expected));
68+
Assert.That(_epochSwitchManager.IsEpochSwitchAtRound(ulong.MaxValue, parent), Is.EqualTo(expected));
69+
}
70+
71+
[Test]
72+
public void GetEpochSwitchInfo_PenaltiesFromSubnetSnapshot_NotFromHeader()
73+
{
74+
Address[] snapshotPenalties = [TestItem.AddressC];
75+
Address[] headerPenalties = [TestItem.AddressD]; // deliberately different
76+
Address[] masterNodes = [TestItem.AddressA, TestItem.AddressB];
77+
78+
XdcReleaseSpec releaseSpec = new()
79+
{
80+
EpochLength = 10,
81+
Gap = 5,
82+
SwitchBlock = 0,
83+
GenesisMasterNodes = masterNodes,
84+
V2Configs = [new V2ConfigParams()]
85+
};
86+
_config.GetSpec(Arg.Any<ForkActivation>()).Returns(releaseSpec);
87+
88+
// Block 0 is an epoch switch (0 % 10 == 0), so no parent-walk needed
89+
XdcSubnetBlockHeaderBuilder builder = Build.A.XdcSubnetBlockHeader();
90+
builder.WithNumber(0);
91+
builder.WithHash(TestItem.KeccakA);
92+
builder.WithPenalties(headerPenalties);
93+
XdcSubnetBlockHeader header = builder.TestObject;
94+
95+
SubnetSnapshot subnetSnapshot = new(header.Number, header.Hash!, [.. masterNodes], snapshotPenalties);
96+
_snapshotManager.GetSnapshotByBlockNumber(header.Number, Arg.Any<IXdcReleaseSpec>()).Returns(subnetSnapshot);
97+
98+
EpochSwitchInfo? result = _epochSwitchManager.GetEpochSwitchInfo(header);
99+
100+
Assert.That(result, Is.Not.Null);
101+
// Penalties must come from SubnetSnapshot, NOT from header
102+
Assert.That(result!.Penalties, Is.EquivalentTo(snapshotPenalties));
103+
Assert.That(result.Penalties, Is.Not.EquivalentTo(headerPenalties));
104+
}
105+
106+
[Test]
107+
public void GetEpochSwitchInfo_NonSubnetSnapshot_Throws()
108+
{
109+
Address[] masterNodes = [TestItem.AddressA, TestItem.AddressB];
110+
111+
XdcReleaseSpec releaseSpec = new()
112+
{
113+
EpochLength = 10,
114+
Gap = 5,
115+
SwitchBlock = 0,
116+
GenesisMasterNodes = masterNodes,
117+
V2Configs = [new V2ConfigParams()]
118+
};
119+
_config.GetSpec(Arg.Any<ForkActivation>()).Returns(releaseSpec);
120+
121+
XdcSubnetBlockHeaderBuilder builder = Build.A.XdcSubnetBlockHeader();
122+
builder.WithNumber(0);
123+
builder.WithHash(TestItem.KeccakA);
124+
XdcSubnetBlockHeader header = builder.TestObject;
125+
126+
Snapshot baseSnapshot = new(header.Number, header.Hash!, masterNodes);
127+
_snapshotManager.GetSnapshotByBlockNumber(header.Number, Arg.Any<IXdcReleaseSpec>()).Returns(baseSnapshot);
128+
129+
Assert.That(() => _epochSwitchManager.GetEpochSwitchInfo(header), Throws.InstanceOf<ArgumentException>());
130+
}
131+
132+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
2+
// SPDX-License-Identifier: LGPL-3.0-only
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using Nethermind.Blockchain;
7+
using Nethermind.Core;
8+
using Nethermind.Core.Caching;
9+
using Nethermind.Core.Crypto;
10+
using Nethermind.Core.Specs;
11+
using Nethermind.Xdc.Spec;
12+
using Nethermind.Xdc.Types;
13+
14+
namespace Nethermind.Xdc;
15+
16+
internal abstract class BaseEpochSwitchManager(ISpecProvider xdcSpecProvider, IBlockTree tree, ISnapshotManager snapshotManager) : IEpochSwitchManager
17+
{
18+
protected ISpecProvider XdcSpecProvider { get; } = xdcSpecProvider;
19+
protected IBlockTree Tree { get; } = tree;
20+
protected ISnapshotManager SnapshotManager { get; } = snapshotManager;
21+
protected LruCache<ValueHash256, EpochSwitchInfo> EpochSwitches { get; } = new(XdcConstants.InMemoryEpochs, nameof(EpochSwitches));
22+
23+
public abstract bool IsEpochSwitchAtBlock(XdcBlockHeader header);
24+
25+
public abstract bool IsEpochSwitchAtRound(ulong currentRound, XdcBlockHeader parent);
26+
27+
public abstract BlockRoundInfo? GetBlockByEpochNumber(ulong targetEpoch);
28+
29+
public EpochSwitchInfo? GetEpochSwitchInfo(XdcBlockHeader header)
30+
{
31+
Hash256 headerHash = header.Hash;
32+
if (EpochSwitches.TryGet(headerHash, out EpochSwitchInfo epochSwitchInfo))
33+
{
34+
return epochSwitchInfo;
35+
}
36+
37+
IXdcReleaseSpec xdcSpec = XdcSpecProvider.GetXdcSpec(header);
38+
39+
while (!IsEpochSwitchAtBlock(header))
40+
{
41+
header = (XdcBlockHeader)(Tree.FindHeader(header.ParentHash!) ?? throw new InvalidOperationException($"Parent block {header.ParentHash} not found while walking to epoch switch"));
42+
}
43+
44+
Address[] masterNodes;
45+
46+
if (header.Number == xdcSpec.SwitchBlock)
47+
{
48+
masterNodes = xdcSpec.GenesisMasterNodes;
49+
}
50+
else
51+
{
52+
if (header.ExtraConsensusData is null)
53+
{
54+
return null;
55+
}
56+
57+
masterNodes = header.ValidatorsAddress is null
58+
? throw new InvalidOperationException($"ValidatorsAddress is null on epoch-switch block {header.Number}")
59+
: [.. header.ValidatorsAddress.Value];
60+
}
61+
62+
Snapshot snap = SnapshotManager.GetSnapshotByBlockNumber(header.Number, xdcSpec);
63+
if (snap is null)
64+
{
65+
return null;
66+
}
67+
68+
Address[] penalties = ResolvePenalties(header, snap);
69+
Address[] candidates = snap.NextEpochCandidates;
70+
71+
Address[] standbyNodes = [];
72+
73+
if (masterNodes.Length != candidates.Length)
74+
{
75+
HashSet<Address> excluded = new(masterNodes);
76+
excluded.UnionWith(penalties);
77+
78+
List<Address> result = new();
79+
foreach (Address candidate in candidates)
80+
{
81+
if (excluded.Add(candidate))
82+
result.Add(candidate);
83+
}
84+
standbyNodes = result.ToArray();
85+
}
86+
87+
epochSwitchInfo = new EpochSwitchInfo(masterNodes, standbyNodes, penalties, new BlockRoundInfo(header.Hash, header.ExtraConsensusData?.BlockRound ?? 0, header.Number));
88+
89+
if (header.ExtraConsensusData?.QuorumCert is not null)
90+
{
91+
epochSwitchInfo.EpochSwitchParentBlockInfo = header.ExtraConsensusData.QuorumCert.ProposedBlockInfo;
92+
}
93+
94+
EpochSwitches.Set(headerHash, epochSwitchInfo);
95+
return epochSwitchInfo;
96+
}
97+
98+
protected abstract Address[] ResolvePenalties(XdcBlockHeader header, Snapshot snapshot);
99+
100+
public EpochSwitchInfo? GetEpochSwitchInfo(Hash256 hash)
101+
{
102+
if (EpochSwitches.TryGet(hash, out EpochSwitchInfo epochSwitchInfo))
103+
{
104+
return epochSwitchInfo;
105+
}
106+
107+
XdcBlockHeader? h = (XdcBlockHeader?)Tree.FindHeader(hash);
108+
if (h is null) return null;
109+
110+
return GetEpochSwitchInfo(h);
111+
}
112+
113+
protected abstract ulong GetCurrentEpochNumber(EpochSwitchInfo epochSwitchInfo, IXdcReleaseSpec xdcSpec);
114+
115+
public EpochSwitchInfo? GetEpochSwitchInfo(ulong round)
116+
{
117+
XdcBlockHeader? headOfChainHeader = (XdcBlockHeader?)Tree.Head?.Header;
118+
if (headOfChainHeader is null) return null;
119+
120+
EpochSwitchInfo epochSwitchInfo = GetEpochSwitchInfo(headOfChainHeader);
121+
if (epochSwitchInfo is null)
122+
{
123+
return null;
124+
}
125+
126+
IXdcReleaseSpec xdcSpec = XdcSpecProvider.GetXdcSpec(headOfChainHeader);
127+
128+
ulong epochRound = epochSwitchInfo.EpochSwitchBlockInfo.Round;
129+
ulong tempTCEpoch = GetCurrentEpochNumber(epochSwitchInfo, xdcSpec);
130+
131+
BlockRoundInfo epochBlockInfo = new(epochSwitchInfo.EpochSwitchBlockInfo.Hash, epochRound, epochSwitchInfo.EpochSwitchBlockInfo.BlockNumber);
132+
133+
while (epochBlockInfo.Round > round)
134+
{
135+
tempTCEpoch--;
136+
epochBlockInfo = GetBlockByEpochNumber(tempTCEpoch);
137+
if (epochBlockInfo is null)
138+
{
139+
return null;
140+
}
141+
}
142+
143+
return GetEpochSwitchInfo(epochBlockInfo.Hash);
144+
}
145+
146+
147+
public EpochSwitchInfo? GetTimeoutCertificateEpochInfo(TimeoutCertificate timeoutCert) => GetEpochSwitchInfo(timeoutCert.Round);
148+
}

0 commit comments

Comments
 (0)