diff --git a/src/Nethermind/Directory.Packages.props b/src/Nethermind/Directory.Packages.props
index 8a9ba749697..becdae36af0 100644
--- a/src/Nethermind/Directory.Packages.props
+++ b/src/Nethermind/Directory.Packages.props
@@ -36,6 +36,7 @@
+
diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp.Generator/Nethermind.Serialization.FluentRlp.Generator.csproj b/src/Nethermind/Nethermind.Serialization.FluentRlp.Generator/Nethermind.Serialization.FluentRlp.Generator.csproj
new file mode 100644
index 00000000000..f8d13657465
--- /dev/null
+++ b/src/Nethermind/Nethermind.Serialization.FluentRlp.Generator/Nethermind.Serialization.FluentRlp.Generator.csproj
@@ -0,0 +1,14 @@
+
+
+
+ netstandard2.1
+ enable
+ true
+ Nethermind.Serialization.FluentRlp.Generator
+
+
+
+
+
+
+
diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp.Generator/RlpSourceGenerator.cs b/src/Nethermind/Nethermind.Serialization.FluentRlp.Generator/RlpSourceGenerator.cs
new file mode 100644
index 00000000000..e3c86607115
--- /dev/null
+++ b/src/Nethermind/Nethermind.Serialization.FluentRlp.Generator/RlpSourceGenerator.cs
@@ -0,0 +1,339 @@
+// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited
+// SPDX-License-Identifier: LGPL-3.0-only
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+using System.Text;
+
+namespace Nethermind.Serialization.FluentRlp.Generator;
+
+public enum RlpRepresentation : byte
+{
+ ///
+ /// The RLP encoding will be a sequence of RLP objects for each property.
+ ///
+ Record = 0,
+
+ ///
+ /// The RLP encoding will be equivalent to the only underlying property.
+ ///
+ Newtype = 1,
+}
+
+[AttributeUsage(AttributeTargets.Class)]
+public sealed class RlpSerializable(RlpRepresentation representation = RlpRepresentation.Record) : Attribute
+{
+ public RlpRepresentation Representation { get; } = representation;
+}
+
+///
+/// A source generator that finds all records with [RlpSerializable] attribute and
+/// generates an abstract `IRlpConverter` class with `Read` and `Write` methods.
+///
+[Generator]
+public sealed class RlpSourceGenerator : IIncrementalGenerator
+{
+ private const string Version = "0.1";
+ private const string GeneratedCodeAttribute = $"""[GeneratedCode("{nameof(RlpSourceGenerator)}", "{Version}")]""";
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var provider = context.SyntaxProvider.CreateSyntaxProvider(
+ predicate: static (s, _) => s is RecordDeclarationSyntax,
+ transform: static (ctx, _) => (RecordDeclarationSyntax)ctx.Node
+ );
+
+ var compilation = context.CompilationProvider.Combine(provider.Collect());
+
+ context.RegisterSourceOutput(compilation, Execute);
+ }
+
+ private void Execute(
+ SourceProductionContext context,
+ (Compilation Compilation, ImmutableArray RecordsDeclarationSyntaxes) p)
+ {
+ // For each record with the attribute, generate the RlpConverter class
+ foreach (var recordDecl in p.RecordsDeclarationSyntaxes)
+ {
+ // Check if the record has the [RlpSerializable] attribute
+ SemanticModel semanticModel = p.Compilation.GetSemanticModel(recordDecl.SyntaxTree);
+ ISymbol? symbol = semanticModel.GetDeclaredSymbol(recordDecl);
+ if (symbol is null) continue;
+
+ AttributeData? rlpSerializableAttribute = symbol
+ .GetAttributes()
+ .FirstOrDefault(a =>
+ a.AttributeClass?.Name == nameof(RlpSerializable) ||
+ a.AttributeClass?.ToDisplayString() == nameof(RlpSerializable));
+
+ // If not, skip the record
+ if (rlpSerializableAttribute is null) continue;
+
+ // Extract the fully qualified record name with its namespace
+ var recordName = symbol.Name;
+ var fullTypeName = symbol.ToDisplayString();
+ // TODO: Deal with missing and nested namespaces
+ var @namespace = symbol.ContainingNamespace?.ToDisplayString();
+
+ // Extract all `using` directives
+ var usingDirectives = semanticModel.SyntaxTree.GetCompilationUnitRoot()
+ .Usings
+ .Select(u => u.ToString())
+ .ToList();
+
+ // Get the `RlpRepresentation` mode
+ var representation = (RlpRepresentation)(rlpSerializableAttribute.ConstructorArguments[0].Value ?? 0);
+
+ // Gather recursively all members that are fields or primary constructor parameters
+ // so we can read them in the same order they are declared.
+ var parameters = GetRecordParameters(recordDecl);
+
+ // Ensure `Newtype` is only used in single-property records
+ if (representation == RlpRepresentation.Newtype && parameters.Count != 1)
+ {
+ var descriptor = new DiagnosticDescriptor(
+ "RLP0001",
+ $"Invalid {nameof(RlpRepresentation)}",
+ $"'{nameof(RlpRepresentation.Newtype)}' representation is only allowed for records with a single property",
+ "", DiagnosticSeverity.Error, true);
+ context.ReportDiagnostic(Diagnostic.Create(descriptor: descriptor, recordDecl.GetLocation()));
+
+ return;
+ }
+
+ // Build the converter class source
+ var generatedCode = GenerateConverterClass(@namespace, usingDirectives, fullTypeName, recordName, parameters, representation);
+
+ // Add to the compilation
+ context.AddSource($"{recordName}RlpConverter.g.cs", SourceText.From(generatedCode, Encoding.UTF8));
+ }
+ }
+
+ ///
+ /// Gathers the record’s primary constructor parameters and public fields/properties
+ /// in the order they appear in the record declaration.
+ ///
+ private static List<(string Name, TypeSyntax TypeName)> GetRecordParameters(RecordDeclarationSyntax recordDecl)
+ {
+ List<(string, TypeSyntax)> parameters = [];
+
+ // Primary constructor parameters
+ if (recordDecl.ParameterList is not null)
+ {
+ foreach (var param in recordDecl.ParameterList.Parameters)
+ {
+ var paramName = param.Identifier.Text;
+ var paramType = param.Type!;
+
+ parameters.Add((paramName, paramType));
+ }
+ }
+
+ return parameters;
+ }
+
+ private static string GenerateConverterClass(
+ string? @namespace,
+ List usingDirectives,
+ string fullTypeName,
+ string recordName,
+ List<(string Name, TypeSyntax TypeName)> parameters,
+ RlpRepresentation representation)
+ {
+ List defaultUsingDirectives =
+ [
+ "using System;",
+ "using System.CodeDom.Compiler;",
+ "using Nethermind.Serialization.FluentRlp;",
+ "using Nethermind.Serialization.FluentRlp.Instances;"
+ ];
+ IEnumerable directives = defaultUsingDirectives.Concat(usingDirectives).Distinct();
+ var usingStatements = new StringBuilder();
+ foreach (var usingDirective in directives)
+ {
+ usingStatements.AppendLine(usingDirective);
+ }
+
+ var writeCalls = new StringBuilder();
+ foreach (var (name, typeName) in parameters)
+ {
+ var writeCall = MapTypeToWriteCall(name, typeName);
+ writeCalls.AppendLine($"w.{writeCall};");
+ }
+
+ var readCalls = new StringBuilder();
+ foreach (var (name, typeName) in parameters)
+ {
+ var readCall = MapTypeToReadCall(typeName);
+ readCalls.AppendLine($"var {name} = r.{readCall};");
+ }
+
+ var constructorCall = new StringBuilder($"{fullTypeName}(");
+ for (int i = 0; i < parameters.Count; i++)
+ {
+ constructorCall.Append(parameters[i].Name);
+ if (i < parameters.Count - 1) constructorCall.Append(", ");
+ }
+ constructorCall.Append(");");
+
+ return
+ $$"""
+ //
+ #nullable enable
+ {{usingStatements}}
+ {{(@namespace is null ? "" : $"namespace {@namespace};")}}
+
+ {{GeneratedCodeAttribute}}
+ public abstract class {{recordName}}RlpConverter : IRlpConverter<{{fullTypeName}}>
+ {
+ public static void Write(ref RlpWriter w, {{fullTypeName}} value)
+ {
+ {{(representation == RlpRepresentation.Record
+ ? $$"""
+ w.WriteSequence(value, static (ref RlpWriter w, {{fullTypeName}} value) =>
+ {
+ {{writeCalls}}
+ });
+ """
+ : writeCalls)}}
+ }
+
+ public static {{fullTypeName}} Read(ref RlpReader r)
+ {
+ {{(representation == RlpRepresentation.Record
+ ? $$"""
+ return r.ReadSequence(static (scoped ref RlpReader r) =>
+ {
+ {{readCalls}}
+
+ return new {{constructorCall}}
+ });
+ """
+ : $"""
+ {readCalls}
+
+ return new {constructorCall}
+ """)}}
+ }
+ }
+
+ {{GeneratedCodeAttribute}}
+ public static class {{recordName}}Ext
+ {
+ public static {{fullTypeName}} Read{{recordName}}(this ref RlpReader reader) => {{recordName}}RlpConverter.Read(ref reader);
+ public static void Write(this ref RlpWriter writer, {{fullTypeName}} value) => {{recordName}}RlpConverter.Write(ref writer, value);
+ }
+ """;
+ }
+
+ ///
+ /// Map the type name to the appropriate Read method on the `RlpReader`
+ /// Extend this mapping for more types as needed.
+ ///
+ private static string MapTypeToReadCall(TypeSyntax syntax)
+ {
+ // Hard-coded cases
+ switch (syntax.ToString())
+ {
+ case "byte[]" or "Byte[]" or "System.Byte[]":
+ return "ReadBytes().ToArray()";
+ case "Span" or "System.Span" or "ReadOnlySpan" or "System.ReadOnlySpan":
+ return "ReadBytes()";
+ }
+
+ // Generics
+ if (syntax is GenericNameSyntax or TupleTypeSyntax or ArrayTypeSyntax)
+ {
+ var typeConstructor = syntax switch
+ {
+ GenericNameSyntax generic => generic.Identifier.ToString(),
+ TupleTypeSyntax _ => "Tuple",
+ ArrayTypeSyntax _ => "Array",
+ _ => throw new ArgumentOutOfRangeException(nameof(syntax))
+ };
+
+ var typeParameters = syntax switch
+ {
+ GenericNameSyntax generic => generic.TypeArgumentList.Arguments,
+ TupleTypeSyntax tuple => tuple.Elements.Select(e => e.Type),
+ ArrayTypeSyntax array => [array.ElementType],
+ _ => throw new ArgumentOutOfRangeException(nameof(syntax))
+ };
+
+ var sb = new StringBuilder();
+ sb.AppendLine($"Read{typeConstructor.Capitalize()}(");
+ foreach (var typeParameter in typeParameters)
+ {
+ sb.AppendLine($$"""static (scoped ref RlpReader r) => { return r.{{MapTypeToReadCall(typeParameter)}}; },""");
+ }
+ sb.Length -= 2; // Remove the trailing `,\n`
+ sb.Append(")");
+
+ return sb.ToString();
+ }
+
+ // Default
+ return $"Read{MapTypeAlias(syntax.ToString())}()";
+ }
+
+ ///
+ /// Map the type name to the appropriate Write method on the `RlpWriter`
+ /// Extend this mapping for more types as needed.
+ ///
+ private static string MapTypeToWriteCall(string? propertyName, TypeSyntax syntax)
+ {
+ // Hard-coded cases
+ switch (syntax.ToString())
+ {
+ case "byte[]" or "Byte[]" or "System.Byte[]" or "Span" or "System.Span" or "ReadOnlySpan" or "System.ReadOnlySpan":
+ return propertyName is null ? "Write(value)" : $"Write(value.{propertyName})";
+ }
+
+ // Generics
+ if (syntax is GenericNameSyntax or TupleTypeSyntax or ArrayTypeSyntax)
+ {
+ var typeParameters = syntax switch
+ {
+ GenericNameSyntax generic => generic.TypeArgumentList.Arguments,
+ TupleTypeSyntax tuple => tuple.Elements.Select(e => e.Type),
+ ArrayTypeSyntax array => [array.ElementType],
+ _ => throw new ArgumentOutOfRangeException(nameof(syntax))
+ };
+
+ var sb = new StringBuilder();
+ sb.AppendLine(propertyName is null ? "Write(value," : $"Write(value.{propertyName},");
+ foreach (var typeParameter in typeParameters)
+ {
+ sb.AppendLine($$"""static (ref RlpWriter w, {{typeParameter}} value) => { w.{{MapTypeToWriteCall(null, typeParameter)}}; },""");
+ }
+ sb.Length -= 2; // Remove the trailing `,\n`
+ sb.Append(")");
+
+ return sb.ToString();
+ }
+
+ // Default
+ return propertyName is not null ? $"Write(value.{propertyName})" : "Write(value)";
+ }
+
+ private static string MapTypeAlias(string alias) =>
+ alias switch
+ {
+ "string" => "String",
+ "short" => "Int16",
+ "int" => "Int32",
+ "long" => "Int64",
+ _ => alias
+ };
+}
+
+public static class StringExt
+{
+ public static string Capitalize(this string str) => str[0].ToString().ToUpper() + str[1..];
+}
diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/Extensions.cs b/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/Extensions.cs
new file mode 100644
index 00000000000..f2c491248ca
--- /dev/null
+++ b/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/Extensions.cs
@@ -0,0 +1,32 @@
+// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
+// SPDX-License-Identifier: LGPL-3.0-only
+
+using FluentAssertions;
+using FluentAssertions.Collections;
+
+namespace Nethermind.Serialization.FluentRlp.Test;
+
+// NOTE: `FluentAssertions` currently does not support `(ReadOnly)Span` or `(ReadOnly)Memory` assertions.
+public static class Extensions
+{
+ public static GenericCollectionAssertions Should(this ReadOnlySpan span) => span.ToArray().Should();
+ public static GenericCollectionAssertions Should(this ReadOnlyMemory memory) => memory.ToArray().Should();
+
+ public static AndConstraint> BeEquivalentTo(
+ this GenericCollectionAssertions @this,
+ ReadOnlySpan expectation,
+ string because = "",
+ params object[] becauseArgs)
+ {
+ return @this.BeEquivalentTo(expectation.ToArray(), config => config, because, becauseArgs);
+ }
+
+ public static AndConstraint> BeEquivalentTo(
+ this GenericCollectionAssertions @this,
+ ReadOnlyMemory expectation,
+ string because = "",
+ params object[] becauseArgs)
+ {
+ return @this.BeEquivalentTo(expectation.ToArray(), config => config, because, becauseArgs);
+ }
+}
diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/Nethermind.Serialization.FluentRlp.Test.csproj b/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/Nethermind.Serialization.FluentRlp.Test.csproj
new file mode 100644
index 00000000000..131fbae2d89
--- /dev/null
+++ b/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/Nethermind.Serialization.FluentRlp.Test.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net9.0
+ preview
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/RlpDerivedTest.cs b/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/RlpDerivedTest.cs
new file mode 100644
index 00000000000..98558ca22de
--- /dev/null
+++ b/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/RlpDerivedTest.cs
@@ -0,0 +1,143 @@
+// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited
+// SPDX-License-Identifier: LGPL-3.0-only
+
+using FluentAssertions;
+using Nethermind.Serialization.FluentRlp.Generator;
+using Nethermind.Serialization.FluentRlp.Instances;
+
+namespace Nethermind.Serialization.FluentRlp.Test;
+
+[RlpSerializable]
+public record Player(int Id, string Username);
+
+[RlpSerializable]
+public record PlayerWithFriends(int Id, string Username, List Friends);
+
+[RlpSerializable]
+public record PlayerWithScores(int Id, string Username, Dictionary Scores);
+
+[RlpSerializable]
+public record PlayerWithCodes(int Id, string Username, int[] Codes);
+
+[RlpSerializable]
+public record Tree(string Value, List Children);
+
+[RlpSerializable]
+public record RawData(int Tag, byte[] Data);
+
+[RlpSerializable]
+public record Integers(short A, int B, long C, Int128 D);
+
+[RlpSerializable]
+public record IntegerTuple((int, long) Values);
+
+[RlpSerializable(RlpRepresentation.Newtype)]
+public record Address(string HexString);
+
+[RlpSerializable]
+public record AccessList(List<(Address, List)> Entries);
+
+public class RlpDerivedTest
+{
+ [Test]
+ public void FlatRecord()
+ {
+ var player = new Player(Id: 42, Username: "SuperUser");
+ var rlp = Rlp.Write(player, static (ref RlpWriter w, Player player) => w.Write(player));
+
+ var decoded = Rlp.Read(rlp, static (scoped ref RlpReader r) => r.ReadPlayer());
+ decoded.Should().BeEquivalentTo(player);
+ }
+
+ [Test]
+ public void RecordWithList()
+ {
+ var player = new PlayerWithFriends(Id: 42, Username: "SuperUser", Friends: ["ana", "bob"]);
+ var rlp = Rlp.Write(player, static (ref RlpWriter w, PlayerWithFriends player) => w.Write(player));
+
+ var decoded = Rlp.Read(rlp, static (scoped ref RlpReader r) => r.ReadPlayerWithFriends());
+ decoded.Should().BeEquivalentTo(player);
+ }
+
+ [Test]
+ public void RecordWithArray()
+ {
+ var player = new PlayerWithCodes(Id: 42, Username: "SuperUser", Codes: [2, 4, 8, 16, 32, 64]);
+ var rlp = Rlp.Write(player, static (ref RlpWriter w, PlayerWithCodes player) => w.Write(player));
+
+ var decoded = Rlp.Read(rlp, static (scoped ref RlpReader r) => r.ReadPlayerWithCodes());
+ decoded.Should().BeEquivalentTo(player);
+ }
+
+ [Test]
+ public void RecordWithDictionary()
+ {
+ var player = new PlayerWithScores(Id: 42, Username: "SuperUser", Scores: new()
+ {
+ { "foo", 42 },
+ { "bar", 1337 }
+ });
+ var rlp = Rlp.Write(player, static (ref RlpWriter w, PlayerWithScores player) => w.Write(player));
+
+ var decoded = Rlp.Read(rlp, static (scoped ref RlpReader r) => r.ReadPlayerWithScores());
+ decoded.Should().BeEquivalentTo(player);
+ }
+
+ [Test]
+ public void RecordWithTuple()
+ {
+ var integerTuple = new IntegerTuple((42, 1337));
+ var rlp = Rlp.Write(integerTuple, static (ref RlpWriter w, IntegerTuple tuple) => w.Write(tuple));
+
+ var decoded = Rlp.Read(rlp, static (scoped ref RlpReader r) => r.ReadIntegerTuple());
+ decoded.Should().BeEquivalentTo(integerTuple);
+ }
+
+ [Test]
+ public void RecursiveRecord()
+ {
+ var tree = new Tree("foo",
+ [
+ new Tree("bar",
+ [new Tree("dog", [])]),
+ new Tree("qux",
+ [new Tree("cat", [])])
+ ]);
+ var rlp = Rlp.Write(tree, static (ref RlpWriter w, Tree tree) => w.Write(tree));
+
+ var decoded = Rlp.Read(rlp, static (scoped ref RlpReader r) => r.ReadTree());
+ decoded.Should().BeEquivalentTo(tree);
+ }
+
+ [Test]
+ public void NewtypeRecords()
+ {
+ var address = new Address("0x1234567890ABCDEF");
+
+ var rlp = Rlp.Write(address, static (ref RlpWriter writer, Address address)
+ => writer.Write(address));
+
+ var rlpExplicit = Rlp.Write(address, (ref RlpWriter writer, Address value)
+ => writer.Write(value.HexString));
+
+ rlp.Should().BeEquivalentTo(rlpExplicit);
+
+ var decoded = Rlp.Read(rlp, static (scoped ref RlpReader r) => r.ReadAddress());
+
+ decoded.Should().BeEquivalentTo(address);
+ }
+
+ [Test]
+ public void RecordWithNestedGenerics()
+ {
+ var accessList = new AccessList([
+ (new Address("0x1234567890ABCDEF"), [1, 1, 3, 5, 8, 13]),
+ (new Address("0xFEDCBA0987654321"), [2, 4, 6, 8, 10])
+ ]);
+
+ var rlp = Rlp.Write(accessList, (ref RlpWriter writer, AccessList value) => writer.Write(value));
+
+ var decoded = Rlp.Read(rlp, static (scoped ref RlpReader r) => r.ReadAccessList());
+ decoded.Should().BeEquivalentTo(accessList);
+ }
+}
diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/RlpReadWriteTest.cs b/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/RlpReadWriteTest.cs
new file mode 100644
index 00000000000..ac08499fe2a
--- /dev/null
+++ b/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/RlpReadWriteTest.cs
@@ -0,0 +1,494 @@
+// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited
+// SPDX-License-Identifier: LGPL-3.0-only
+
+using FluentAssertions;
+using Nethermind.Serialization.FluentRlp.Instances;
+
+namespace Nethermind.Serialization.FluentRlp.Test;
+
+public class RlpReadWriteTest
+{
+ [Test]
+ public void LongString()
+ {
+ var rlp = Rlp.Write(static (ref RlpWriter w) =>
+ {
+ var str = new string('A', 2000);
+ w.Write(str);
+ });
+
+ var decoded = Rlp.Read(rlp, (scoped ref RlpReader r) => r.ReadString());
+
+ decoded.Should().Be(new string('A', 2000));
+ }
+
+ [Test]
+ public void HeterogeneousList()
+ {
+ var rlp = Rlp.Write(static (ref RlpWriter w) =>
+ {
+ w.WriteSequence(static (ref RlpWriter w) =>
+ {
+ w.WriteSequence(static (ref RlpWriter w) => { w.Write(42); });
+ w.WriteSequence(static (ref RlpWriter w) =>
+ {
+ w.Write("dog");
+ w.Write("cat");
+ });
+ });
+ });
+
+ var decoded = Rlp.Read(rlp, (scoped ref RlpReader r) =>
+ {
+ return r.ReadSequence(static (scoped ref RlpReader r) =>
+ {
+ var _1 = r.ReadSequence(static (scoped ref RlpReader r) => r.ReadInt32());
+ var _2 = r.ReadSequence(static (scoped ref RlpReader r) =>
+ {
+ var _1 = r.ReadString();
+ var _2 = r.ReadString();
+
+ return (_1, _2);
+ });
+
+ return (_1, _2);
+ });
+ });
+
+ decoded.Should().Be((42, ("dog", "cat")));
+ }
+
+ [Test]
+ public void LongList()
+ {
+ var rlp = Rlp.Write(static (ref RlpWriter w) =>
+ {
+ w.WriteSequence(static (ref RlpWriter w) =>
+ {
+ for (int i = 0; i < 100; i++)
+ {
+ w.Write("dog");
+ }
+ });
+ });
+
+ List decoded = Rlp.Read(rlp, static (scoped ref RlpReader r) =>
+ {
+ return r.ReadSequence(static (scoped ref RlpReader r) =>
+ {
+ List result = [];
+ for (int i = 0; i < 100; i++)
+ {
+ result.Add(r.ReadString());
+ }
+
+ return result;
+ });
+ });
+
+ decoded.Count.Should().Be(100);
+ decoded.Should().AllBeEquivalentTo("dog");
+ }
+
+ [Test]
+ public void MultipleLongList()
+ {
+ var rlp = Rlp.Write(static (ref RlpWriter w) =>
+ {
+ w.WriteSequence(static (ref RlpWriter w) =>
+ {
+ for (int i = 0; i < 100; i++)
+ {
+ w.Write("dog");
+ }
+ });
+ w.WriteSequence(static (ref RlpWriter w) =>
+ {
+ for (int i = 0; i < 50; i++)
+ {
+ w.Write("cat");
+ }
+ });
+ });
+
+ var (dogs, cats) = Rlp.Read(rlp, static (scoped ref RlpReader r) =>
+ {
+ var dogs = r.ReadSequence(static (scoped ref RlpReader r) =>
+ {
+ List result = [];
+ while (r.HasNext)
+ {
+ result.Add(r.ReadString());
+ }
+
+ return result;
+ });
+ var cats = r.ReadSequence(static (scoped ref RlpReader r) =>
+ {
+ List result = [];
+ while (r.HasNext)
+ {
+ result.Add(r.ReadString());
+ }
+
+ return result;
+ });
+
+ return (dogs, cats);
+ });
+
+ dogs.Count.Should().Be(100);
+ dogs.Should().AllBeEquivalentTo("dog");
+
+ cats.Count.Should().Be(50);
+ cats.Should().AllBeEquivalentTo("cat");
+ }
+
+ [TestCase(2)]
+ public void UnknownLengthList([Values(1, 3, 5, 10, 20)] int length)
+ {
+ var rlp = Rlp.Write(length, static (ref RlpWriter root, int length) =>
+ {
+ root.WriteSequence(length, static (ref RlpWriter w, int length) =>
+ {
+ for (int i = 0; i < length; i++)
+ {
+ w.Write(42);
+ }
+ });
+ });
+
+ List decoded = Rlp.Read(rlp, static (scoped ref RlpReader r) =>
+ {
+ return r.ReadSequence(static (scoped ref RlpReader r) =>
+ {
+ List result = [];
+ while (r.HasNext)
+ {
+ result.Add(r.ReadInt32());
+ }
+
+ return result;
+ });
+ });
+
+ decoded.Count.Should().Be(length);
+ }
+
+ [Test]
+ public void InvalidObjectReading()
+ {
+ var rlp = Rlp.Write(static (ref RlpWriter w) => { w.Write(42); });
+ Action tryRead = () => Rlp.Read(rlp, static (scoped ref RlpReader r) =>
+ {
+ return r.ReadSequence(static (scoped ref RlpReader _) => null as object);
+ });
+
+ tryRead.Should().Throw();
+ }
+
+ [Test]
+ public void InvalidListReading()
+ {
+ var rlp = Rlp.Write(static (ref RlpWriter w) => { w.WriteSequence(static (ref RlpWriter _) => { }); });
+ Func tryRead = () => Rlp.Read(rlp, static (scoped ref RlpReader r) => r.ReadInt32());
+
+ tryRead.Should().Throw();
+ }
+
+ [Test]
+ public void Choice()
+ {
+ RefRlpReaderFunc intReader = static (scoped ref RlpReader r) => r.ReadInt32();
+ RefRlpReaderFunc wrappedReader = (scoped ref RlpReader r) => r.ReadSequence(intReader);
+ var intRlp = Rlp.Write(static (ref RlpWriter w) => { w.Write(42); });
+ var wrappedIntRlp = Rlp.Write(static (ref RlpWriter w) => w.WriteSequence(static (ref RlpWriter w) => { w.Write(42); }));
+
+ foreach (var rlp in (byte[][])[intRlp, wrappedIntRlp])
+ {
+ int decoded = Rlp.Read(rlp, (scoped ref RlpReader r) => r.Choice(wrappedReader, intReader));
+
+ decoded.Should().Be(42);
+ }
+ }
+
+ [Test]
+ public void ChoiceDeep()
+ {
+ RefRlpReaderFunc<(string, string, string)> readerA = static (scoped ref RlpReader r) =>
+ {
+ return r.ReadSequence(static (scoped ref RlpReader r) =>
+ {
+ var _1 = r.ReadString();
+ var _2 = r.ReadString();
+ var _3 = r.ReadString();
+
+ return (_1, _2, _3);
+ });
+ };
+ RefRlpReaderFunc<(string, string, string)> readerB = static (scoped ref RlpReader r) =>
+ {
+ return r.ReadSequence(static (scoped ref RlpReader r) =>
+ {
+ var _1 = r.ReadString();
+ var _2 = r.ReadString();
+ var _3 = r.ReadInt32();
+
+ return (_1, _2, _3.ToString());
+ });
+ };
+
+ var rlp = Rlp.Write(static (ref RlpWriter w) =>
+ {
+ w.WriteSequence(static (ref RlpWriter w) =>
+ {
+ w.Write("dog");
+ w.Write("cat");
+ w.Write(42);
+ });
+ });
+
+ var decoded = Rlp.Read(rlp, (scoped ref RlpReader r) => r.Choice(readerA, readerB));
+ decoded.Should().Be(("dog", "cat", "42"));
+ }
+
+ [Test]
+ public void OptionalStruct()
+ {
+ int? value = null;
+
+ var rlp = Rlp.Write(value, static (ref RlpWriter w, int? value) =>
+ {
+ if (value.HasValue)
+ {
+ w.Write(value.Value);
+ }
+ });
+
+ var decoded = Rlp.Read(rlp, static (scoped ref RlpReader r) =>
+ {
+ return r.Optional(static (scoped ref RlpReader r) => r.ReadInt32());
+ });
+
+ decoded.Should().Be(value);
+ }
+
+
+ [Test]
+ public void OptionalReference()
+ {
+ string? value = null;
+
+ var rlp = Rlp.Write(value, static (ref RlpWriter w, string? value) =>
+ {
+ if (value is not null)
+ {
+ w.Write(value);
+ }
+ });
+
+ var decoded = Rlp.Read(rlp, static (scoped ref RlpReader r) =>
+ {
+ return r.Optional(static (scoped ref RlpReader r) => r.ReadString());
+ });
+
+ decoded.Should().Be(value);
+ }
+
+ [Test]
+ public void OptionalDeep()
+ {
+ (string, string?, int, int?) tuple = ("dog", null, 42, null);
+
+ var rlp = Rlp.Write(tuple, static (ref RlpWriter w, (string _1, string? _2, int _3, int? _4) tuple) =>
+ {
+ w.Write(tuple._1);
+ if (tuple._2 is not null)
+ {
+ w.Write(tuple._2);
+ }
+ w.Write(tuple._3);
+ if (tuple._4.HasValue)
+ {
+ w.Write(tuple._4.Value);
+ }
+ });
+
+ var decoded = Rlp.Read(rlp, static (scoped ref RlpReader r) =>
+ {
+ var _1 = r.ReadString();
+ var _2 = r.Optional(static (scoped ref RlpReader r) => r.ReadString());
+ var _3 = r.ReadInt32();
+ var _4 = r.Optional(static (scoped ref RlpReader r) => r.ReadInt32());
+
+ return (_1, _2, _3, _4);
+ });
+
+ decoded.Should().Be(tuple);
+ }
+
+ [Test]
+ public void UserDefinedRecord()
+ {
+ List students =
+ [
+ new("Ana", 23, new Dictionary
+ {
+ { "Math", 7 },
+ { "Literature", 9 }
+ }),
+ new("Bob", 25, new Dictionary
+ {
+ { "Math", 9 },
+ { "Literature", 6 }
+ }),
+ ];
+
+ var rlp = Rlp.Write(students, static (ref RlpWriter w, List students) =>
+ {
+ w.WriteSequence(students, static (ref RlpWriter w, List students) =>
+ {
+ foreach (var student in students)
+ {
+ w.Write(student);
+ }
+ });
+ });
+
+ var decoded = Rlp.Read(rlp, static (scoped ref RlpReader r) =>
+ {
+ return r.ReadSequence(static (scoped ref RlpReader r) =>
+ {
+ List result = [];
+ while (r.HasNext)
+ {
+ result.Add(r.ReadStudent());
+ }
+
+ return result;
+ });
+ });
+
+ decoded.Should().BeEquivalentTo(students);
+ }
+
+ [Test]
+ public void ListCollection()
+ {
+ var list = new List { "cat", "dog" };
+
+ var rlp = Rlp.Write(list, static (ref RlpWriter w, List list) => w.Write(list, StringRlpConverter.Write));
+
+ var rlpExplicit = Rlp.Write(static (ref RlpWriter w) =>
+ {
+ w.WriteSequence(static (ref RlpWriter w) =>
+ {
+ w.Write("cat");
+ w.Write("dog");
+ });
+ });
+ rlpExplicit.Should().BeEquivalentTo(rlp);
+
+ var decoded = Rlp.Read(rlp, static (scoped ref RlpReader r) => r.ReadList(StringRlpConverter.Read));
+
+ list.Should().BeEquivalentTo(decoded);
+ }
+
+ [Test]
+ public void ListOfListCollection()
+ {
+ List> list = [
+ ["dog", "cat"],
+ ["foo"],
+ []
+ ];
+
+ var rlp = Rlp.Write(list, static (ref RlpWriter w, List> list) =>
+ w.Write(list, static (ref RlpWriter w, List v) =>
+ w.Write(v, StringRlpConverter.Write)));
+
+ var rlpExplicit = Rlp.Write(static (ref RlpWriter w) =>
+ {
+ w.WriteSequence(static (ref RlpWriter w) =>
+ {
+ w.WriteSequence(static (ref RlpWriter w) =>
+ {
+ w.Write("dog");
+ w.Write("cat");
+ });
+
+ w.WriteSequence(static (ref RlpWriter w) =>
+ {
+ w.Write("foo");
+ });
+
+ w.WriteSequence(static (ref RlpWriter _) =>
+ {
+ });
+ });
+ });
+ rlpExplicit.Should().BeEquivalentTo(rlp);
+
+ var decoded = Rlp.Read(rlp, static (scoped ref RlpReader r) =>
+ r.ReadList(static (scoped ref RlpReader r) =>
+ r.ReadList(StringRlpConverter.Read)));
+
+ list.Should().BeEquivalentTo(decoded);
+ }
+
+ [Test]
+ public void DictionaryCollection()
+ {
+ var dictionary = new Dictionary
+ {
+ { 1, "dog" },
+ { 2, "cat" },
+ };
+
+ var rlp = Rlp.Write(dictionary, static (ref RlpWriter w, Dictionary dictionary) =>
+ w.Write(dictionary, Int32RlpConverter.Write, StringRlpConverter.Write));
+
+ var rlpExplicit = Rlp.Write(dictionary, static (ref RlpWriter w, Dictionary dictionary) =>
+ {
+ w.WriteSequence(dictionary, static (ref RlpWriter w, Dictionary dictionary) =>
+ {
+ foreach (var tuple in dictionary)
+ {
+ w.WriteSequence(tuple, static (ref RlpWriter w, KeyValuePair tuple) =>
+ {
+ w.Write(tuple.Key);
+ w.Write(tuple.Value);
+ });
+ }
+ });
+ });
+ rlp.Should().BeEquivalentTo(rlpExplicit);
+
+ var decoded = Rlp.Read(rlp, static (scoped ref RlpReader r) =>
+ r.ReadDictionary(Int32RlpConverter.Read, StringRlpConverter.Read));
+
+ decoded.Should().BeEquivalentTo(dictionary);
+ }
+
+ [Test]
+ public void TupleCollection()
+ {
+ var tuple = (42, 1337);
+
+ var rlp = Rlp.Write(tuple, static (ref RlpWriter w, (int, int) tuple)
+ => w.Write(tuple, Int32RlpConverter.Write, Int32RlpConverter.Write));
+
+ var rlpExplicit = Rlp.Write(tuple, static (ref RlpWriter w, (int, int) tuple) =>
+ {
+ w.Write(tuple.Item1);
+ w.Write(tuple.Item2);
+ });
+
+ rlp.Should().BeEquivalentTo(rlpExplicit);
+
+ var decoded = Rlp.Read(rlp, static (scoped ref RlpReader r) =>
+ r.ReadTuple(Int32RlpConverter.Read, Int32RlpConverter.Read));
+
+ decoded.Should().BeEquivalentTo(tuple);
+ }
+}
diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/RlpReaderTest.cs b/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/RlpReaderTest.cs
new file mode 100644
index 00000000000..b39ef0e895a
--- /dev/null
+++ b/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/RlpReaderTest.cs
@@ -0,0 +1,156 @@
+// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited
+// SPDX-License-Identifier: LGPL-3.0-only
+
+using FluentAssertions;
+using Nethermind.Serialization.FluentRlp.Instances;
+
+namespace Nethermind.Serialization.FluentRlp.Test;
+
+public class RlpReaderTest
+{
+ [Test]
+ public void ReadShortString()
+ {
+ byte[] source = [0x83, (byte)'d', (byte)'o', (byte)'g'];
+ string actual = Rlp.Read(source, static (scoped ref RlpReader r) => r.ReadString());
+
+ actual.Should().Be("dog");
+ }
+
+ [Test]
+ public void ReadEmptyString()
+ {
+ byte[] source = [0x80];
+ string actual = Rlp.Read(source, static (scoped ref RlpReader r) => r.ReadString());
+
+ actual.Should().Be("");
+ }
+
+ [Test]
+ public void ReadLongString()
+ {
+ byte[] source = [0xb8, 0x38, .. "Lorem ipsum dolor sit amet, consectetur adipisicing elit"u8];
+ string actual = Rlp.Read(source, static (scoped ref RlpReader r) => r.ReadString());
+
+ actual.Should().Be("Lorem ipsum dolor sit amet, consectetur adipisicing elit");
+ }
+
+ [Test]
+ public void ReadShortInteger()
+ {
+ for (int i = 0; i < 0x80; i++)
+ {
+ var integer = i;
+ byte[] source = [(byte)integer];
+ int actual = Rlp.Read(source, static (scoped ref RlpReader r) => r.ReadInt32());
+
+ actual.Should().Be(integer);
+ }
+ }
+
+ [Test]
+ public void ReadLongInteger()
+ {
+ for (int i = 0x100; i < 0xFFFF; i++)
+ {
+ var integer = i;
+ byte[] source = [0x82, (byte)((integer & 0xFF00) >> 8), (byte)((integer & 0x00FF) >> 0)];
+ int actual = Rlp.Read(source, static (scoped ref RlpReader r) => r.ReadInt32());
+
+ actual.Should().Be(integer);
+ }
+ }
+
+ [Test]
+ public void ReadStringList()
+ {
+ byte[] source = [0xc8, 0x83, .. "cat"u8, 0x83, .. "dog"u8];
+ var actual = Rlp.Read(source, static (scoped ref RlpReader r) =>
+ {
+ return r.ReadSequence(static (scoped ref RlpReader r) =>
+ {
+ var cat = r.ReadString();
+ var dog = r.ReadString();
+
+ return (cat, dog);
+ });
+ });
+
+ actual.Should().Be(("cat", "dog"));
+ }
+
+ [Test]
+ public void ReadEmptyList()
+ {
+ byte[] source = [0xc0];
+
+ var actual = Rlp.Read(source, static (scoped ref RlpReader r) =>
+ {
+ return r.ReadSequence(static (scoped ref RlpReader _) => Array.Empty