From d55c1e987ecb9e7431617f0e2f55b5e1cb5a0c30 Mon Sep 17 00:00:00 2001 From: cicr99 Date: Tue, 21 Oct 2025 10:12:55 +0200 Subject: [PATCH 1/8] implement pool for timeouts and votes --- .../Nethermind.Xdc/RLP/VoteDecoder.cs | 11 +++ .../Nethermind.Xdc/Types/Timeout.cs | 4 +- src/Nethermind/Nethermind.Xdc/Types/Vote.cs | 8 +- src/Nethermind/Nethermind.Xdc/XdcPool.cs | 81 +++++++++++++++++++ 4 files changed, 100 insertions(+), 4 deletions(-) create mode 100644 src/Nethermind/Nethermind.Xdc/XdcPool.cs 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/Types/Timeout.cs b/src/Nethermind/Nethermind.Xdc/Types/Timeout.cs index 218b7735ba95..85a6606d52a0 100644 --- a/src/Nethermind/Nethermind.Xdc/Types/Timeout.cs +++ b/src/Nethermind/Nethermind.Xdc/Types/Timeout.cs @@ -9,8 +9,9 @@ namespace Nethermind.Xdc.Types; -public class Timeout(ulong round, Signature? signature, ulong gapNumber) +public class Timeout(ulong round, Signature? signature, ulong gapNumber) : IXdcPoolItem { + private readonly TimeoutDecoder _decoder = new(); private Address signer; public ulong Round { get; set; } = round; @@ -20,4 +21,5 @@ public class Timeout(ulong round, Signature? signature, ulong gapNumber) 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/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/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(); +} From af1b1b57943874c90496b30f4b4984515f66d560 Mon Sep 17 00:00:00 2001 From: cicr99 Date: Mon, 13 Oct 2025 10:05:05 +0200 Subject: [PATCH 2/8] implement timeout handler --- .../TimeoutCertificateManagerTests.cs | 7 +- .../Nethermind.Xdc/ISyncInfoManager.cs | 2 +- .../ITimeoutCertificateManager.cs | 2 +- .../Nethermind.Xdc/Spec/XdcReleaseSpec.cs | 4 +- .../TimeoutCertificateManager.cs | 118 +++++++++++++++++- .../Types/TimeoutCertificate.cs | 2 - src/Nethermind/Nethermind.Xdc/XdcContext.cs | 22 ++-- 7 files changed, 134 insertions(+), 23 deletions(-) diff --git a/src/Nethermind/Nethermind.Xdc.Test/TimeoutCertificateManagerTests.cs b/src/Nethermind/Nethermind.Xdc.Test/TimeoutCertificateManagerTests.cs index d7323e9f22e6..c726d828cc64 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; @@ -136,10 +136,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..a7c69b4b573d 100644 --- a/src/Nethermind/Nethermind.Xdc/ITimeoutCertificateManager.cs +++ b/src/Nethermind/Nethermind.Xdc/ITimeoutCertificateManager.cs @@ -9,7 +9,7 @@ namespace Nethermind.Xdc; public interface ITimeoutCertificateManager { void HandleTimeout(Timeout timeout); - void OnCountdownTimer(DateTime time); + void OnCountdownTimer(); void ProcessTimeoutCertificate(TimeoutCertificate timeoutCertificate); bool VerifyTimeoutCertificate(TimeoutCertificate timeoutCertificate, out string errorMessage); } 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..d536a3d452b2 100644 --- a/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs +++ b/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs @@ -3,41 +3,87 @@ 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 HashSet _timeouts = new HashSet(); public void HandleTimeout(Timeout timeout) { - throw new NotImplementedException(); + if (timeout.Round != _ctx.CurrentRound) + { + throw new CertificateValidationException(CertificateType.TimeoutCertificate, CertificateValidationFailure.InvalidRound); + } + + _timeouts.Add(timeout); + + BlockHeader header = _blockTree.Head?.Header; + if (header is not XdcBlockHeader xdcHeader) + throw new ArgumentException($"Only type of {nameof(XdcBlockHeader)} is allowed"); + EpochSwitchInfo epochSwitchInfo = _epochSwitchManager.GetEpochSwitchInfo(xdcHeader, xdcHeader.Hash); + if (epochSwitchInfo is null) + { + throw new ConsensusHeaderDataExtractionException(nameof(EpochSwitchInfo)); + } + + IXdcReleaseSpec spec = _specProvider.GetXdcSpec(xdcHeader, timeout.Round); + var certThreshold = spec.CertThreshold; + if (_timeouts.Count >= epochSwitchInfo.Masternodes.Length * certThreshold) + { + // The original code passes the collected timeouts instead of using the global variable, check rase condition? + OnTimeoutPoolThresholdReached(timeout); + } } - public void OnCountdownTimer(DateTime time) + private void OnTimeoutPoolThresholdReached(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) @@ -98,11 +144,73 @@ public bool VerifyTimeoutCertificate(TimeoutCertificate timeoutCertificate, out return true; } + public void OnCountdownTimer() + { + if (!AllowedToSend()) + return; + + SendTimeout(); + _ctx.TimeoutCounter++; + + var xdcHeader = (XdcBlockHeader)_blockTree.Head?.Header; + IXdcReleaseSpec spec = _specProvider.GetXdcSpec(xdcHeader!, _ctx.CurrentRound); + + if (_ctx.TimeoutCounter % spec.TimeoutSyncThreshold == 0) + { + SyncInfo syncInfo = _syncInfoManager.GetSyncInfo(); + //TODO: Broadcast syncInfo + } + } + + 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.SetSigner(_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; + foreach (Address masternode in epochSwitchInfo.Masternodes) + if (masternode == _signer.Address) return true; + return false; + } + 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; } + } 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/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; From 968d5a815ca9337c2b3e8ada2f02778da2e1ee87 Mon Sep 17 00:00:00 2001 From: cicr99 Date: Tue, 21 Oct 2025 11:08:05 +0200 Subject: [PATCH 3/8] use XdcPool for collecting timeouts --- .../Nethermind.Xdc/TimeoutCertificateManager.cs | 11 +++++------ src/Nethermind/Nethermind.Xdc/Types/Timeout.cs | 4 +--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs b/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs index d536a3d452b2..71a972bc52ec 100644 --- a/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs +++ b/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs @@ -31,7 +31,7 @@ public class TimeoutCertificateManager(XdcContext context, ISnapshotManager snap private EthereumEcdsa _ethereumEcdsa = new EthereumEcdsa(0); private static readonly TimeoutDecoder _timeoutDecoder = new(); - private HashSet _timeouts = new HashSet(); + private XdcPool _timeouts = new (); public void HandleTimeout(Timeout timeout) { @@ -40,7 +40,7 @@ public void HandleTimeout(Timeout timeout) throw new CertificateValidationException(CertificateType.TimeoutCertificate, CertificateValidationFailure.InvalidRound); } - _timeouts.Add(timeout); + var count = _timeouts.Add(timeout); BlockHeader header = _blockTree.Head?.Header; if (header is not XdcBlockHeader xdcHeader) @@ -53,7 +53,7 @@ public void HandleTimeout(Timeout timeout) IXdcReleaseSpec spec = _specProvider.GetXdcSpec(xdcHeader, timeout.Round); var certThreshold = spec.CertThreshold; - if (_timeouts.Count >= epochSwitchInfo.Masternodes.Length * certThreshold) + if (count >= epochSwitchInfo.Masternodes.Length * certThreshold) { // The original code passes the collected timeouts instead of using the global variable, check rase condition? OnTimeoutPoolThresholdReached(timeout); @@ -62,7 +62,7 @@ public void HandleTimeout(Timeout timeout) private void OnTimeoutPoolThresholdReached(Timeout timeout) { - Signature[] signatures = _timeouts.Select(t => t.Signature).ToArray(); + Signature[] signatures = _timeouts.GetItems(timeout).Select(t => t.Signature).ToArray(); var timeoutCertificate = new TimeoutCertificate(timeout.Round, signatures, timeout.GapNumber); @@ -186,7 +186,7 @@ private void SendTimeout() ValueHash256 msgHash = ComputeTimeoutMsgHash(_ctx.CurrentRound, gapNumber); Signature signedHash = _signer.Sign(msgHash); var timeoutMsg = new Timeout(_ctx.CurrentRound, signedHash, gapNumber); - timeoutMsg.SetSigner(_signer.Address); + timeoutMsg.Signer = _signer.Address; HandleTimeout(timeoutMsg); @@ -213,4 +213,3 @@ internal static ValueHash256 ComputeTimeoutMsgHash(ulong round, ulong gap) } } - diff --git a/src/Nethermind/Nethermind.Xdc/Types/Timeout.cs b/src/Nethermind/Nethermind.Xdc/Types/Timeout.cs index 85a6606d52a0..b1e5efeea4a0 100644 --- a/src/Nethermind/Nethermind.Xdc/Types/Timeout.cs +++ b/src/Nethermind/Nethermind.Xdc/Types/Timeout.cs @@ -12,14 +12,12 @@ namespace Nethermind.Xdc.Types; public class Timeout(ulong round, Signature? signature, ulong gapNumber) : IXdcPoolItem { private readonly TimeoutDecoder _decoder = new(); - private Address signer; 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)); } From 5ecbd8e351a447026beb92a9bf02ed2d4aa098ec Mon Sep 17 00:00:00 2001 From: cicr99 Date: Tue, 21 Oct 2025 11:35:03 +0200 Subject: [PATCH 4/8] implement checks before timeout handling and refactors --- .../ITimeoutCertificateManager.cs | 4 +- .../TimeoutCertificateManager.cs | 65 ++++++++++++++----- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/src/Nethermind/Nethermind.Xdc/ITimeoutCertificateManager.cs b/src/Nethermind/Nethermind.Xdc/ITimeoutCertificateManager.cs index a7c69b4b573d..8cee540ade3c 100644 --- a/src/Nethermind/Nethermind.Xdc/ITimeoutCertificateManager.cs +++ b/src/Nethermind/Nethermind.Xdc/ITimeoutCertificateManager.cs @@ -3,12 +3,12 @@ // 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); + Task HandleTimeout(Timeout timeout); void OnCountdownTimer(); void ProcessTimeoutCertificate(TimeoutCertificate timeoutCertificate); bool VerifyTimeoutCertificate(TimeoutCertificate timeoutCertificate, out string errorMessage); diff --git a/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs b/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs index 71a972bc52ec..448aca828d85 100644 --- a/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs +++ b/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs @@ -33,36 +33,37 @@ public class TimeoutCertificateManager(XdcContext context, ISnapshotManager snap private static readonly TimeoutDecoder _timeoutDecoder = new(); private XdcPool _timeouts = new (); - public void HandleTimeout(Timeout timeout) + public Task HandleTimeout(Timeout timeout) { if (timeout.Round != _ctx.CurrentRound) { - throw new CertificateValidationException(CertificateType.TimeoutCertificate, CertificateValidationFailure.InvalidRound); + // Not interested in processing timeout for round different from the current one + return Task.CompletedTask; } var count = _timeouts.Add(timeout); + var collectedTimeouts = _timeouts.GetItems(timeout); - BlockHeader header = _blockTree.Head?.Header; - if (header is not XdcBlockHeader xdcHeader) - throw new ArgumentException($"Only type of {nameof(XdcBlockHeader)} is allowed"); + var xdcHeader = _blockTree.Head?.Header as XdcBlockHeader; EpochSwitchInfo epochSwitchInfo = _epochSwitchManager.GetEpochSwitchInfo(xdcHeader, xdcHeader.Hash); if (epochSwitchInfo is null) { - throw new ConsensusHeaderDataExtractionException(nameof(EpochSwitchInfo)); + // Failed to get epoch switch info, cannot process timeout + return Task.CompletedTask; } IXdcReleaseSpec spec = _specProvider.GetXdcSpec(xdcHeader, timeout.Round); var certThreshold = spec.CertThreshold; if (count >= epochSwitchInfo.Masternodes.Length * certThreshold) { - // The original code passes the collected timeouts instead of using the global variable, check rase condition? - OnTimeoutPoolThresholdReached(timeout); + OnTimeoutPoolThresholdReached(collectedTimeouts, timeout); } + return Task.CompletedTask; } - private void OnTimeoutPoolThresholdReached(Timeout timeout) + private void OnTimeoutPoolThresholdReached(IEnumerable timeouts, Timeout timeout) { - Signature[] signatures = _timeouts.GetItems(timeout).Select(t => t.Signature).ToArray(); + Signature[] signatures = timeouts.Select(t => t.Signature).ToArray(); var timeoutCertificate = new TimeoutCertificate(timeout.Round, signatures, timeout.GapNumber); @@ -106,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) @@ -152,7 +151,7 @@ public void OnCountdownTimer() SendTimeout(); _ctx.TimeoutCounter++; - var xdcHeader = (XdcBlockHeader)_blockTree.Head?.Header; + var xdcHeader = _blockTree.Head?.Header as XdcBlockHeader; IXdcReleaseSpec spec = _specProvider.GetXdcSpec(xdcHeader!, _ctx.CurrentRound); if (_ctx.TimeoutCounter % spec.TimeoutSyncThreshold == 0) @@ -162,6 +161,40 @@ public void OnCountdownTimer() } } + 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; @@ -200,9 +233,7 @@ private bool AllowedToSend() EpochSwitchInfo epochSwitchInfo = _epochSwitchManager.GetEpochSwitchInfo(currentHeader, currentHeader.Hash); if (epochSwitchInfo is null) return false; - foreach (Address masternode in epochSwitchInfo.Masternodes) - if (masternode == _signer.Address) return true; - return false; + return epochSwitchInfo.Masternodes.Any(x => x == _signer.Address); } internal static ValueHash256 ComputeTimeoutMsgHash(ulong round, ulong gap) From 3ecc51a494c5672845326c2b02c3a441d29436f1 Mon Sep 17 00:00:00 2001 From: cicr99 Date: Tue, 21 Oct 2025 11:46:46 +0200 Subject: [PATCH 5/8] update tc manager in tests --- .../TimeoutCertificateManagerTests.cs | 17 ++++++++++++++--- .../Nethermind.Xdc/TimeoutCertificateManager.cs | 4 ++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Nethermind/Nethermind.Xdc.Test/TimeoutCertificateManagerTests.cs b/src/Nethermind/Nethermind.Xdc.Test/TimeoutCertificateManagerTests.cs index c726d828cc64..bcd71a6aa408 100644 --- a/src/Nethermind/Nethermind.Xdc.Test/TimeoutCertificateManagerTests.cs +++ b/src/Nethermind/Nethermind.Xdc.Test/TimeoutCertificateManagerTests.cs @@ -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)); } diff --git a/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs b/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs index 448aca828d85..5d6eac3323fb 100644 --- a/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs +++ b/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs @@ -31,7 +31,7 @@ public class TimeoutCertificateManager(XdcContext context, ISnapshotManager snap private EthereumEcdsa _ethereumEcdsa = new EthereumEcdsa(0); private static readonly TimeoutDecoder _timeoutDecoder = new(); - private XdcPool _timeouts = new (); + private XdcPool _timeouts = new(); public Task HandleTimeout(Timeout timeout) { @@ -183,7 +183,7 @@ public Task OnReceiveTimeout(Timeout timeout) private bool FilterTimeout(Timeout timeout) { - if(timeout.Round < _ctx.CurrentRound) return false; + if (timeout.Round < _ctx.CurrentRound) return false; Snapshot snapshot = _snapshotManager.GetSnapshotByGapNumber(_blockTree, timeout.GapNumber); if (snapshot is null || snapshot.NextEpochCandidates.Length == 0) return false; From fd1b0d5d08947d60d01a62c7612e970fc46b5149 Mon Sep 17 00:00:00 2001 From: cicr99 Date: Tue, 21 Oct 2025 12:03:45 +0200 Subject: [PATCH 6/8] expose OnReceiveTimeout method in interface for tc manager --- src/Nethermind/Nethermind.Xdc/ITimeoutCertificateManager.cs | 1 + src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Xdc/ITimeoutCertificateManager.cs b/src/Nethermind/Nethermind.Xdc/ITimeoutCertificateManager.cs index 8cee540ade3c..1be500848244 100644 --- a/src/Nethermind/Nethermind.Xdc/ITimeoutCertificateManager.cs +++ b/src/Nethermind/Nethermind.Xdc/ITimeoutCertificateManager.cs @@ -8,6 +8,7 @@ namespace Nethermind.Xdc; public interface ITimeoutCertificateManager { + Task OnReceiveTimeout(Timeout timeout); Task HandleTimeout(Timeout timeout); void OnCountdownTimer(); void ProcessTimeoutCertificate(TimeoutCertificate timeoutCertificate); diff --git a/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs b/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs index 5d6eac3323fb..cb040b20ca14 100644 --- a/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs +++ b/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs @@ -233,7 +233,7 @@ private bool AllowedToSend() EpochSwitchInfo epochSwitchInfo = _epochSwitchManager.GetEpochSwitchInfo(currentHeader, currentHeader.Hash); if (epochSwitchInfo is null) return false; - return epochSwitchInfo.Masternodes.Any(x => x == _signer.Address); + return epochSwitchInfo.Masternodes.Contains(_signer.Address); } internal static ValueHash256 ComputeTimeoutMsgHash(ulong round, ulong gap) From 9e83cc057aa00cac10de81aeaf3d140eae4b805d Mon Sep 17 00:00:00 2001 From: ak88 Date: Tue, 21 Oct 2025 17:34:28 +0200 Subject: [PATCH 7/8] bit of optimization --- .../Nethermind.Xdc/TimeoutCertificateManager.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs b/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs index cb040b20ca14..e5884c303778 100644 --- a/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs +++ b/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs @@ -41,7 +41,7 @@ public Task HandleTimeout(Timeout timeout) return Task.CompletedTask; } - var count = _timeouts.Add(timeout); + _timeouts.Add(timeout); var collectedTimeouts = _timeouts.GetItems(timeout); var xdcHeader = _blockTree.Head?.Header as XdcBlockHeader; @@ -54,7 +54,7 @@ public Task HandleTimeout(Timeout timeout) IXdcReleaseSpec spec = _specProvider.GetXdcSpec(xdcHeader, timeout.Round); var certThreshold = spec.CertThreshold; - if (count >= epochSwitchInfo.Masternodes.Length * certThreshold) + if (collectedTimeouts.Count >= epochSwitchInfo.Masternodes.Length * certThreshold) { OnTimeoutPoolThresholdReached(collectedTimeouts, timeout); } @@ -238,9 +238,10 @@ private bool AllowedToSend() 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(); } } From 73add9bc581399038f29952c95b5b6f039b5356a Mon Sep 17 00:00:00 2001 From: ak88 Date: Tue, 21 Oct 2025 17:36:06 +0200 Subject: [PATCH 8/8] format --- src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs b/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs index e5884c303778..7180bf921f5d 100644 --- a/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs +++ b/src/Nethermind/Nethermind.Xdc/TimeoutCertificateManager.cs @@ -238,7 +238,7 @@ private bool AllowedToSend() internal static ValueHash256 ComputeTimeoutMsgHash(ulong round, ulong gap) { - Timeout timeout = new (round, null, gap); + Timeout timeout = new(round, null, gap); KeccakRlpStream stream = new KeccakRlpStream(); _timeoutDecoder.Encode(stream, timeout, RlpBehaviors.ForSealing); return stream.GetValueHash();