Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions src/Nethermind/Nethermind.Xdc.Test/ModuleTests/SyncInfoDecoderTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
93 changes: 93 additions & 0 deletions src/Nethermind/Nethermind.Xdc/RLP/SyncInfoDecoder.cs
Original file line number Diff line number Diff line change
@@ -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<SyncInfo>
{
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);
}
}