diff --git a/src/Nethermind/Nethermind.Xdc.Test/ModuleTests/SyncInfoDecoderTests.cs b/src/Nethermind/Nethermind.Xdc.Test/ModuleTests/SyncInfoDecoderTests.cs new file mode 100644 index 000000000000..db199e8b09e4 --- /dev/null +++ b/src/Nethermind/Nethermind.Xdc.Test/ModuleTests/SyncInfoDecoderTests.cs @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using FluentAssertions; +using Nethermind.Core.Crypto; +using Nethermind.Serialization.Rlp; +using Nethermind.Xdc.RLP; +using Nethermind.Xdc.Types; +using NUnit.Framework; +using System.Collections; + +namespace Nethermind.Xdc.Test; + +[TestFixture, Parallelizable(ParallelScope.All)] +public class SyncInfoDecoderTests +{ + public static IEnumerable SyncInfoCases + { + get + { + yield return new TestCaseData( + new SyncInfo( + new QuorumCertificate( + new BlockRoundInfo(Hash256.Zero, 1, 1), + [new Signature(new byte[64], 0), new Signature(new byte[64], 0)], + 0 + ), + new TimeoutCertificate( + 1, + [new Signature(new byte[64], 0), new Signature(new byte[64], 0)], + 0 + ) + ), + true + ); + + yield return new TestCaseData( + new SyncInfo( + new QuorumCertificate( + new BlockRoundInfo(Hash256.Zero, 1, 1), + [], + 0 + ), + new TimeoutCertificate(1, [], 0) + ), + false + ); + + yield return new TestCaseData( + new SyncInfo( + new QuorumCertificate( + new BlockRoundInfo(Hash256.Zero, ulong.MaxValue, long.MaxValue), + [], + ulong.MaxValue + ), + new TimeoutCertificate(ulong.MaxValue, [], ulong.MaxValue) + ), + true + ); + } + } + + [TestCaseSource(nameof(SyncInfoCases))] + public void EncodeDecode_RoundTrip_Matches_AllFields(SyncInfo syncInfo, bool useRlpStream) + { + var decoder = new SyncInfoDecoder(); + + Rlp encoded = decoder.Encode(syncInfo); + var stream = new RlpStream(encoded.Bytes); + SyncInfo decoded; + + if (useRlpStream) + { + decoded = decoder.Decode(stream); + } + else + { + Rlp.ValueDecoderContext decoderContext = new Rlp.ValueDecoderContext(stream.Data.AsSpan()); + decoded = decoder.Decode(ref decoderContext); + } + + decoded.Should().BeEquivalentTo(syncInfo); + } + + [Test] + public void Encode_UseBothRlpStreamAndValueDecoderContext_IsEquivalentAfterReencoding() + { + SyncInfo syncInfo = new( + new QuorumCertificate( + new BlockRoundInfo(Hash256.Zero, 1, 1), + [new Signature(new byte[64], 0), new Signature(new byte[64], 0), new Signature(new byte[64], 0)], + 0 + ), + new TimeoutCertificate( + 1, + [new Signature(new byte[64], 0), new Signature(new byte[64], 0)], + 0 + ) + ); + + SyncInfoDecoder decoder = new(); + RlpStream stream = new RlpStream(decoder.GetLength(syncInfo)); + decoder.Encode(stream, syncInfo); + stream.Position = 0; + + // Decode with RlpStream + SyncInfo decodedStream = decoder.Decode(stream); + stream.Position = 0; + + // Decode with ValueDecoderContext + Rlp.ValueDecoderContext decoderContext = new Rlp.ValueDecoderContext(stream.Data.AsSpan()); + SyncInfo decodedContext = decoder.Decode(ref decoderContext); + + // Both should be equivalent to original + decodedStream.Should().BeEquivalentTo(syncInfo); + decodedContext.Should().BeEquivalentTo(syncInfo); + decodedStream.Should().BeEquivalentTo(decodedContext); + } + + [Test] + public void TotalLength_Equals_GetLength() + { + SyncInfo syncInfo = new( + new QuorumCertificate( + new BlockRoundInfo(Hash256.Zero, 42, 42), + [new Signature(new byte[64], 0)], + 10 + ), + new TimeoutCertificate( + 41, + [new Signature(new byte[64], 1)], + 10 + ) + ); + + var decoder = new SyncInfoDecoder(); + Rlp encoded = decoder.Encode(syncInfo); + + int expectedTotal = decoder.GetLength(syncInfo, RlpBehaviors.None); + Assert.That(encoded.Bytes.Length, Is.EqualTo(expectedTotal), + "Encoded total length should match GetLength()."); + } + + [Test] + public void Encode_Null_ReturnsEmptySequence() + { + var decoder = new SyncInfoDecoder(); + + Rlp encoded = decoder.Encode(null!); + + Assert.That(encoded, Is.EqualTo(Rlp.OfEmptySequence)); + } + + [Test] + public void Decode_Null_ReturnsNull() + { + var decoder = new SyncInfoDecoder(); + var stream = new RlpStream(Rlp.OfEmptySequence.Bytes); + + SyncInfo decoded = decoder.Decode(stream); + + Assert.That(decoded, Is.Null); + } + + [Test] + public void Decode_EmptyByteArray_ValueDecoderContext_ReturnsNull() + { + var decoder = new SyncInfoDecoder(); + Rlp.ValueDecoderContext decoderContext = new Rlp.ValueDecoderContext(Rlp.OfEmptySequence.Bytes); + + SyncInfo decoded = decoder.Decode(ref decoderContext); + + Assert.That(decoded, Is.Null); + } +} diff --git a/src/Nethermind/Nethermind.Xdc/RLP/SyncInfoDecoder.cs b/src/Nethermind/Nethermind.Xdc/RLP/SyncInfoDecoder.cs new file mode 100644 index 000000000000..0ebaf9d39ad8 --- /dev/null +++ b/src/Nethermind/Nethermind.Xdc/RLP/SyncInfoDecoder.cs @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Serialization.Rlp; +using Nethermind.Xdc.RLP; +using Nethermind.Xdc.Types; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Nethermind.Xdc; + +internal class SyncInfoDecoder : RlpValueDecoder +{ + private readonly QuorumCertificateDecoder _quorumCertificateDecoder = new(); + private readonly TimeoutCertificateDecoder _timeoutCertificateDecoder = new(); + + protected override SyncInfo DecodeInternal(ref Rlp.ValueDecoderContext decoderContext, RlpBehaviors rlpBehaviors = RlpBehaviors.None) + { + if (decoderContext.IsNextItemNull()) + return null; + + int sequenceLength = decoderContext.ReadSequenceLength(); + int endPosition = decoderContext.Position + sequenceLength; + + QuorumCertificate highestQuorumCert = _quorumCertificateDecoder.Decode(ref decoderContext, rlpBehaviors); + TimeoutCertificate highestTimeoutCert = _timeoutCertificateDecoder.Decode(ref decoderContext, rlpBehaviors); + + if ((rlpBehaviors & RlpBehaviors.AllowExtraBytes) != RlpBehaviors.AllowExtraBytes) + { + decoderContext.Check(endPosition); + } + + return new SyncInfo(highestQuorumCert, highestTimeoutCert); + } + + protected override SyncInfo DecodeInternal(RlpStream rlpStream, RlpBehaviors rlpBehaviors = RlpBehaviors.None) + { + if (rlpStream.IsNextItemNull()) + return null; + + int sequenceLength = rlpStream.ReadSequenceLength(); + int endPosition = rlpStream.Position + sequenceLength; + + QuorumCertificate highestQuorumCert = _quorumCertificateDecoder.Decode(rlpStream, rlpBehaviors); + TimeoutCertificate highestTimeoutCert = _timeoutCertificateDecoder.Decode(rlpStream, rlpBehaviors); + + if ((rlpBehaviors & RlpBehaviors.AllowExtraBytes) != RlpBehaviors.AllowExtraBytes) + { + rlpStream.Check(endPosition); + } + + return new SyncInfo(highestQuorumCert, highestTimeoutCert); + } + + public Rlp Encode(SyncInfo 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 override void Encode(RlpStream stream, SyncInfo item, RlpBehaviors rlpBehaviors = RlpBehaviors.None) + { + if (item is null) + { + stream.EncodeNullObject(); + return; + } + + stream.StartSequence(GetContentLength(item, rlpBehaviors)); + _quorumCertificateDecoder.Encode(stream, item.HighestQuorumCert, rlpBehaviors); + _timeoutCertificateDecoder.Encode(stream, item.HighestTimeoutCert, rlpBehaviors); + } + + public override int GetLength(SyncInfo item, RlpBehaviors rlpBehaviors = RlpBehaviors.None) + { + return Rlp.LengthOfSequence(GetContentLength(item, rlpBehaviors)); + } + + public int GetContentLength(SyncInfo item, RlpBehaviors rlpBehaviors = RlpBehaviors.None) + { + if (item is null) + return 0; + + return _quorumCertificateDecoder.GetLength(item.HighestQuorumCert, rlpBehaviors) + + _timeoutCertificateDecoder.GetLength(item.HighestTimeoutCert, rlpBehaviors); + } +}