diff --git a/src/Nethermind/Nethermind.Xdc.Test/TimeoutCertificateManagerTests.cs b/src/Nethermind/Nethermind.Xdc.Test/TimeoutCertificateManagerTests.cs index d7323e9f22e6..bcd71a6aa408 100644 --- a/src/Nethermind/Nethermind.Xdc.Test/TimeoutCertificateManagerTests.cs +++ b/src/Nethermind/Nethermind.Xdc.Test/TimeoutCertificateManagerTests.cs @@ -11,8 +11,8 @@ using Nethermind.Core.Crypto; using Nethermind.Core.Specs; using Nethermind.Core.Test.Builders; +using Nethermind.Consensus; using Nethermind.Crypto; -using Nethermind.Xdc.RLP; using Nethermind.Xdc.Spec; using Nethermind.Xdc.Types; @@ -47,10 +47,13 @@ public void VerifyTC_SnapshotMissing_ReturnsFalse() XdcBlockHeader header = Build.A.XdcBlockHeader().TestObject; blockTree.FindHeader(Arg.Any()).Returns(header); var tcManager = new TimeoutCertificateManager( + new XdcContext(), snapshotManager, Substitute.For(), Substitute.For(), - blockTree); + blockTree, + Substitute.For(), + Substitute.For()); var ok = tcManager.VerifyTimeoutCertificate(tc, out var err); Assert.That(ok, Is.False); @@ -68,10 +71,13 @@ public void VerifyTC_EmptyCandidates_ReturnsFalse() XdcBlockHeader header = Build.A.XdcBlockHeader().TestObject; blockTree.FindHeader(Arg.Any()).Returns(header); var tcManager = new TimeoutCertificateManager( + new XdcContext(), snapshotManager, Substitute.For(), Substitute.For(), - blockTree); + blockTree, + Substitute.For(), + Substitute.For()); var ok = tcManager.VerifyTimeoutCertificate(tc, out var err); Assert.That(ok, Is.False); @@ -128,7 +134,12 @@ public void VerifyTCWithDifferentParameters_ReturnsExpected(TimeoutCertificate t blockTree.Head.Returns(new Block(header, new BlockBody())); blockTree.FindHeader(Arg.Any()).Returns(header); - var tcManager = new TimeoutCertificateManager(snapshotManager, epochSwitchManager, specProvider, blockTree); + var context = new XdcContext(); + ISyncInfoManager syncInfoManager = Substitute.For(); + ISigner signer = Substitute.For(); + + var tcManager = new TimeoutCertificateManager(context, snapshotManager, epochSwitchManager, specProvider, + blockTree, syncInfoManager, signer); Assert.That(tcManager.VerifyTimeoutCertificate(timeoutCertificate, out _), Is.EqualTo(expected)); } @@ -136,10 +147,13 @@ public void VerifyTCWithDifferentParameters_ReturnsExpected(TimeoutCertificate t private TimeoutCertificateManager BuildTimeoutCertificateManager() { return new TimeoutCertificateManager( + new XdcContext(), Substitute.For(), Substitute.For(), Substitute.For(), - Substitute.For()); + Substitute.For(), + Substitute.For(), + Substitute.For()); } private static TimeoutCertificate BuildTimeoutCertificate(PrivateKey[] keys, ulong round = 1, ulong gap = 0) diff --git a/src/Nethermind/Nethermind.Xdc/ISyncInfoManager.cs b/src/Nethermind/Nethermind.Xdc/ISyncInfoManager.cs index 4ca0423ac7eb..d1c578819cc2 100644 --- a/src/Nethermind/Nethermind.Xdc/ISyncInfoManager.cs +++ b/src/Nethermind/Nethermind.Xdc/ISyncInfoManager.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; namespace Nethermind.Xdc; -internal interface ISyncInfoManager +public interface ISyncInfoManager { void ProcessSyncInfo(SyncInfo syncInfo); bool VerifySyncInfo(SyncInfo syncInfo); diff --git a/src/Nethermind/Nethermind.Xdc/ITimeoutCertificateManager.cs b/src/Nethermind/Nethermind.Xdc/ITimeoutCertificateManager.cs index f645556f8c24..1be500848244 100644 --- a/src/Nethermind/Nethermind.Xdc/ITimeoutCertificateManager.cs +++ b/src/Nethermind/Nethermind.Xdc/ITimeoutCertificateManager.cs @@ -3,13 +3,14 @@ // SPDX-License-Identifier: LGPL-3.0-only using Nethermind.Xdc.Types; -using System; +using System.Threading.Tasks; namespace Nethermind.Xdc; public interface ITimeoutCertificateManager { - void HandleTimeout(Timeout timeout); - void OnCountdownTimer(DateTime time); + Task OnReceiveTimeout(Timeout timeout); + Task HandleTimeout(Timeout timeout); + void OnCountdownTimer(); void ProcessTimeoutCertificate(TimeoutCertificate timeoutCertificate); bool VerifyTimeoutCertificate(TimeoutCertificate timeoutCertificate, out string errorMessage); } diff --git a/src/Nethermind/Nethermind.Xdc/RLP/VoteDecoder.cs b/src/Nethermind/Nethermind.Xdc/RLP/VoteDecoder.cs index cad93402dff0..e424126dc8df 100644 --- a/src/Nethermind/Nethermind.Xdc/RLP/VoteDecoder.cs +++ b/src/Nethermind/Nethermind.Xdc/RLP/VoteDecoder.cs @@ -78,6 +78,17 @@ public void Encode(RlpStream stream, Vote item, RlpBehaviors rlpBehaviors = RlpB stream.Encode(item.GapNumber); } + public Rlp Encode(Vote item, RlpBehaviors rlpBehaviors = RlpBehaviors.None) + { + if (item is null) + return Rlp.OfEmptySequence; + + RlpStream rlpStream = new(GetLength(item, rlpBehaviors)); + Encode(rlpStream, item, rlpBehaviors); + + return new Rlp(rlpStream.Data.ToArray()); + } + public int GetLength(Vote item, RlpBehaviors rlpBehaviors) { return Rlp.LengthOfSequence(GetContentLength(item, rlpBehaviors)); diff --git a/src/Nethermind/Nethermind.Xdc/Spec/XdcReleaseSpec.cs b/src/Nethermind/Nethermind.Xdc/Spec/XdcReleaseSpec.cs index f3d2fe89828e..b7cf4afef86a 100644 --- a/src/Nethermind/Nethermind.Xdc/Spec/XdcReleaseSpec.cs +++ b/src/Nethermind/Nethermind.Xdc/Spec/XdcReleaseSpec.cs @@ -10,7 +10,7 @@ namespace Nethermind.Xdc.Spec; public class XdcReleaseSpec : ReleaseSpec, IXdcReleaseSpec { public int EpochLength { get; set; } - public long Gap { get; set; } + public int Gap { get; set; } public int SwitchEpoch { get; set; } public UInt256 SwitchBlock { get; set; } public int MaxMasternodes { get; set; } // v2 max masternodes @@ -57,7 +57,7 @@ internal static V2ConfigParams GetConfigAtRound(List list, ulong public interface IXdcReleaseSpec : IReleaseSpec { public int EpochLength { get; } - public long Gap { get; } + public int Gap { get; } public int SwitchEpoch { get; set; } public UInt256 SwitchBlock { get; set; } public int MaxMasternodes { get; set; } // v2 max masternodes diff --git a/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs b/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs index 7d0de40dcd28..7180bf921f5d 100644 --- a/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs +++ b/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs @@ -3,41 +3,88 @@ 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.Crypto; using Nethermind.Serialization.Rlp; using Nethermind.Xdc.RLP; +using Nethermind.Xdc.Errors; using Nethermind.Xdc.Types; using Nethermind.Xdc.Spec; namespace Nethermind.Xdc; -public class TimeoutCertificateManager(ISnapshotManager snapshotManager, IEpochSwitchManager epochSwitchManager, ISpecProvider specProvider, IBlockTree blockTree) : ITimeoutCertificateManager +public class TimeoutCertificateManager(XdcContext context, ISnapshotManager snapshotManager, IEpochSwitchManager epochSwitchManager, ISpecProvider specProvider, IBlockTree blockTree, ISyncInfoManager syncInfoManager, ISigner signer) : ITimeoutCertificateManager { + private XdcContext _ctx = context; private ISnapshotManager _snapshotManager = snapshotManager; private IEpochSwitchManager _epochSwitchManager = epochSwitchManager; private ISpecProvider _specProvider = specProvider; private IBlockTree _blockTree = blockTree; + private ISyncInfoManager _syncInfoManager = syncInfoManager; + private ISigner _signer = signer; + private EthereumEcdsa _ethereumEcdsa = new EthereumEcdsa(0); private static readonly TimeoutDecoder _timeoutDecoder = new(); + private XdcPool _timeouts = new(); - public void HandleTimeout(Timeout timeout) + public Task HandleTimeout(Timeout timeout) { - throw new NotImplementedException(); + if (timeout.Round != _ctx.CurrentRound) + { + // Not interested in processing timeout for round different from the current one + return Task.CompletedTask; + } + + _timeouts.Add(timeout); + var collectedTimeouts = _timeouts.GetItems(timeout); + + var xdcHeader = _blockTree.Head?.Header as XdcBlockHeader; + EpochSwitchInfo epochSwitchInfo = _epochSwitchManager.GetEpochSwitchInfo(xdcHeader, xdcHeader.Hash); + if (epochSwitchInfo is null) + { + // Failed to get epoch switch info, cannot process timeout + return Task.CompletedTask; + } + + IXdcReleaseSpec spec = _specProvider.GetXdcSpec(xdcHeader, timeout.Round); + var certThreshold = spec.CertThreshold; + if (collectedTimeouts.Count >= epochSwitchInfo.Masternodes.Length * certThreshold) + { + OnTimeoutPoolThresholdReached(collectedTimeouts, timeout); + } + return Task.CompletedTask; } - public void OnCountdownTimer(DateTime time) + private void OnTimeoutPoolThresholdReached(IEnumerable timeouts, Timeout timeout) { - throw new NotImplementedException(); + Signature[] signatures = timeouts.Select(t => t.Signature).ToArray(); + + var timeoutCertificate = new TimeoutCertificate(timeout.Round, signatures, timeout.GapNumber); + + ProcessTimeoutCertificate(timeoutCertificate); + + SyncInfo syncInfo = _syncInfoManager.GetSyncInfo(); + //TODO: Broadcast syncInfo } public void ProcessTimeoutCertificate(TimeoutCertificate timeoutCertificate) { - throw new NotImplementedException(); + if (timeoutCertificate.Round > _ctx.HighestTC.Round) + { + _ctx.HighestTC = timeoutCertificate; + } + + if (timeoutCertificate.Round >= _ctx.CurrentRound) + { + //TODO Check how this new round is set + _ctx.SetNewRound(_blockTree, timeoutCertificate.Round + 1); + } } public bool VerifyTimeoutCertificate(TimeoutCertificate timeoutCertificate, out string errorMessage) @@ -60,9 +107,7 @@ public bool VerifyTimeoutCertificate(TimeoutCertificate timeoutCertificate, out var nextEpochCandidates = new HashSet
(snapshot.NextEpochCandidates); var signatures = new HashSet(timeoutCertificate.Signatures); - BlockHeader header = _blockTree.Head?.Header; - if (header is not XdcBlockHeader xdcHeader) - throw new InvalidOperationException($"Only type of {nameof(XdcBlockHeader)} is allowed"); + var xdcHeader = _blockTree.Head?.Header as XdcBlockHeader; IXdcReleaseSpec spec = _specProvider.GetXdcSpec(xdcHeader, timeoutCertificate.Round); EpochSwitchInfo epochInfo = _epochSwitchManager.GetTimeoutCertificateEpochInfo(timeoutCertificate); if (epochInfo is null) @@ -98,11 +143,105 @@ public bool VerifyTimeoutCertificate(TimeoutCertificate timeoutCertificate, out return true; } + public void OnCountdownTimer() + { + if (!AllowedToSend()) + return; + + SendTimeout(); + _ctx.TimeoutCounter++; + + var xdcHeader = _blockTree.Head?.Header as XdcBlockHeader; + IXdcReleaseSpec spec = _specProvider.GetXdcSpec(xdcHeader!, _ctx.CurrentRound); + + if (_ctx.TimeoutCounter % spec.TimeoutSyncThreshold == 0) + { + SyncInfo syncInfo = _syncInfoManager.GetSyncInfo(); + //TODO: Broadcast syncInfo + } + } + + public Task OnReceiveTimeout(Timeout timeout) + { + var currentBlock = _blockTree.Head ?? throw new InvalidOperationException("Failed to get current block"); + var currentHeader = currentBlock.Header as XdcBlockHeader; + var currentBlockNumber = currentBlock.Number; + var epochLenth = _specProvider.GetXdcSpec(currentHeader, timeout.Round).EpochLength; + if (Math.Abs((long)timeout.GapNumber - currentBlockNumber) > 3 * epochLenth) + { + // Discarded propagated timeout, too far away + return Task.CompletedTask; + } + + if (FilterTimeout(timeout)) + { + //TODO: Broadcast Timeout + return HandleTimeout(timeout); + } + return Task.CompletedTask; + } + + private bool FilterTimeout(Timeout timeout) + { + if (timeout.Round < _ctx.CurrentRound) return false; + Snapshot snapshot = _snapshotManager.GetSnapshotByGapNumber(_blockTree, timeout.GapNumber); + if (snapshot is null || snapshot.NextEpochCandidates.Length == 0) return false; + + // Verify msg signature + ValueHash256 timeoutMsgHash = ComputeTimeoutMsgHash(timeout.Round, timeout.GapNumber); + Address signer = _ethereumEcdsa.RecoverAddress(timeout.Signature, in timeoutMsgHash); + timeout.Signer = signer; + + return snapshot.NextEpochCandidates.Contains(signer); + } + + private void SendTimeout() + { + ulong gapNumber = 0; + var currentHeader = (XdcBlockHeader)_blockTree.Head?.Header; + if (currentHeader is null) throw new InvalidOperationException("Failed to retrieve current header"); + IXdcReleaseSpec spec = _specProvider.GetXdcSpec(currentHeader, _ctx.CurrentRound); + if (_epochSwitchManager.IsEpochSwitchAtRound(_ctx.CurrentRound, currentHeader, out ulong epochNumber)) + { + ulong currentNumber = (ulong)currentHeader.Number + 1; + gapNumber = Math.Max(0, currentNumber - currentNumber % (ulong)spec.EpochLength - (ulong)spec.Gap); + } + else + { + EpochSwitchInfo epochSwitchInfo = _epochSwitchManager.GetEpochSwitchInfo(currentHeader, currentHeader.Hash); + if (epochSwitchInfo is null) + throw new ConsensusHeaderDataExtractionException(nameof(EpochSwitchInfo)); + + ulong currentNumber = (ulong)epochSwitchInfo.EpochSwitchBlockInfo.BlockNumber; + gapNumber = Math.Max(0, currentNumber - currentNumber % (ulong)spec.EpochLength - (ulong)spec.Gap); + } + + ValueHash256 msgHash = ComputeTimeoutMsgHash(_ctx.CurrentRound, gapNumber); + Signature signedHash = _signer.Sign(msgHash); + var timeoutMsg = new Timeout(_ctx.CurrentRound, signedHash, gapNumber); + timeoutMsg.Signer = _signer.Address; + + HandleTimeout(timeoutMsg); + + //TODO: Broadcast _ctx.HighestTC + } + + // Returns true if the signer is within the master node list + private bool AllowedToSend() + { + var currentHeader = (XdcBlockHeader)_blockTree.Head?.Header; + EpochSwitchInfo epochSwitchInfo = _epochSwitchManager.GetEpochSwitchInfo(currentHeader, currentHeader.Hash); + if (epochSwitchInfo is null) + return false; + return epochSwitchInfo.Masternodes.Contains(_signer.Address); + } + internal static ValueHash256 ComputeTimeoutMsgHash(ulong round, ulong gap) { - var timeout = new Timeout(round, null, gap); - Rlp encoded = _timeoutDecoder.Encode(timeout, RlpBehaviors.ForSealing); - return Keccak.Compute(encoded.Bytes).ValueHash256; + Timeout timeout = new(round, null, gap); + KeccakRlpStream stream = new KeccakRlpStream(); + _timeoutDecoder.Encode(stream, timeout, RlpBehaviors.ForSealing); + return stream.GetValueHash(); } -} +} diff --git a/src/Nethermind/Nethermind.Xdc/Types/Timeout.cs b/src/Nethermind/Nethermind.Xdc/Types/Timeout.cs index 218b7735ba95..b1e5efeea4a0 100644 --- a/src/Nethermind/Nethermind.Xdc/Types/Timeout.cs +++ b/src/Nethermind/Nethermind.Xdc/Types/Timeout.cs @@ -9,15 +9,15 @@ namespace Nethermind.Xdc.Types; -public class Timeout(ulong round, Signature? signature, ulong gapNumber) +public class Timeout(ulong round, Signature? signature, ulong gapNumber) : IXdcPoolItem { - private Address signer; + private readonly TimeoutDecoder _decoder = new(); public ulong Round { get; set; } = round; public Signature? Signature { get; set; } = signature; public ulong GapNumber { get; set; } = gapNumber; + public Address? Signer { get; set; } public override string ToString() => $"{Round}:{GapNumber}"; - - public void SetSigner(Address signer) => this.signer = signer; + public (ulong Round, Hash256 hash) PoolKey() => (Round, Keccak.Compute(_decoder.Encode(this, RlpBehaviors.ForSealing).Bytes)); } diff --git a/src/Nethermind/Nethermind.Xdc/Types/TimeoutCertificate.cs b/src/Nethermind/Nethermind.Xdc/Types/TimeoutCertificate.cs index f60eb302c7e1..9c9cebf2fe49 100644 --- a/src/Nethermind/Nethermind.Xdc/Types/TimeoutCertificate.cs +++ b/src/Nethermind/Nethermind.Xdc/Types/TimeoutCertificate.cs @@ -2,8 +2,6 @@ // SPDX-License-Identifier: LGPL-3.0-only using Nethermind.Core.Crypto; -using Nethermind.Serialization.Rlp; -using Nethermind.Xdc.RLP; namespace Nethermind.Xdc.Types; diff --git a/src/Nethermind/Nethermind.Xdc/Types/Vote.cs b/src/Nethermind/Nethermind.Xdc/Types/Vote.cs index f43bec3d0eca..cef0aeb5530a 100644 --- a/src/Nethermind/Nethermind.Xdc/Types/Vote.cs +++ b/src/Nethermind/Nethermind.Xdc/Types/Vote.cs @@ -1,18 +1,20 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Core; using Nethermind.Core.Crypto; -using System.Text.Json.Serialization; +using RlpBehaviors = Nethermind.Serialization.Rlp.RlpBehaviors; namespace Nethermind.Xdc.Types; -public class Vote(BlockRoundInfo proposedBlockInfo, ulong gapNumber, Signature signature = null) +public class Vote(BlockRoundInfo proposedBlockInfo, ulong gapNumber, Signature signature = null) : IXdcPoolItem { + private readonly VoteDecoder _decoder = new(); public BlockRoundInfo ProposedBlockInfo { get; set; } = proposedBlockInfo; public ulong GapNumber { get; set; } = gapNumber; public Signature? Signature { get; set; } = signature; public override string ToString() => $"{ProposedBlockInfo.Round}:{GapNumber}:{ProposedBlockInfo.BlockNumber}"; + + public (ulong Round, Hash256 hash) PoolKey() => (ProposedBlockInfo.Round, Keccak.Compute(_decoder.Encode(this, RlpBehaviors.ForSealing).Bytes)); } diff --git a/src/Nethermind/Nethermind.Xdc/XdcContext.cs b/src/Nethermind/Nethermind.Xdc/XdcContext.cs index 00ee0cf32a74..9240c35decb3 100644 --- a/src/Nethermind/Nethermind.Xdc/XdcContext.cs +++ b/src/Nethermind/Nethermind.Xdc/XdcContext.cs @@ -6,24 +6,26 @@ using Nethermind.Core.Crypto; using Nethermind.Xdc.Types; using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using static Org.BouncyCastle.Asn1.Cmp.Challenge; +using System.Collections.Concurrent; +using BlockInfo = Nethermind.Xdc.Types.BlockRoundInfo; +using Round = ulong; namespace Nethermind.Xdc; + public class XdcContext { + public ConcurrentDictionary Signatures { get; set; } public Address Leader { get; set; } - public int TimeoutCounter { get; set; } - public ulong CurrentRound { get; set; } - public ulong HighestSelfMindeRound { get; set; } - public ulong HighestVotedRound { get; set; } + public int TimeoutCounter { get; set; } = 0; + public Round CurrentRound { get; set; } + public Round HighestSelfMindeRound { get; set; } + public Round HighestVotedRound { get; set; } public QuorumCertificate HighestQC { get; set; } public QuorumCertificate LockQC { get; set; } public TimeoutCertificate HighestTC { get; set; } - public BlockRoundInfo HighestCommitBlock { get; set; } + public BlockInfo HighestCommitBlock { get; set; } + public SignFn SignFun { get; set; } + public bool IsInitialized { get; set; } = false; public event Action NewRoundSetEvent; diff --git a/src/Nethermind/Nethermind.Xdc/XdcPool.cs b/src/Nethermind/Nethermind.Xdc/XdcPool.cs new file mode 100644 index 000000000000..d088ee99fcf5 --- /dev/null +++ b/src/Nethermind/Nethermind.Xdc/XdcPool.cs @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Linq; +using Nethermind.Core.Collections; +using Nethermind.Core.Crypto; +using Nethermind.Core.Threading; + +namespace Nethermind.Xdc; + +public class XdcPool where T : IXdcPoolItem +{ + private readonly Dictionary<(ulong Round, Hash256 Hash), ArrayPoolList> _items = new(); + private readonly McsLock _lock = new(); + + public long Add(T item) + { + using var lockRelease = _lock.Acquire(); + { + var key = item.PoolKey(); + if (!_items.TryGetValue(key, out var list)) + { + //128 should be enough to cover all master nodes and some extras + list = new ArrayPoolList(128); + _items[key] = list; + } + if (!list.Contains(item)) + list.Add(item); + return list.Count; + } + } + + public void EndRound(ulong round) + { + using var lockRelease = _lock.Acquire(); + { + foreach (var key in _items.Keys.ToArray()) + { + if (key.Round <= round && _items.Remove(key, out ArrayPoolList list)) + { + list?.Dispose(); + } + } + } + } + + public IReadOnlyCollection GetItems(T item) + { + using var lockRelease = _lock.Acquire(); + { + var key = item.PoolKey(); + if (_items.TryGetValue(key, out ArrayPoolList list)) + { + //Allocating a new array since it goes outside the lock + return list.ToArray(); + } + return []; + } + } + + public long GetCount(T item) + { + using var lockRelease = _lock.Acquire(); + { + var key = item.PoolKey(); + if (_items.TryGetValue(key, out ArrayPoolList list)) + { + return list.Count; + } + return 0; + } + } +} + +public interface IXdcPoolItem +{ + (ulong Round, Hash256 hash) PoolKey(); +}