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()); + }); + + actual.Should().BeEmpty(); + } + + [Test] + public void ReadSpan() + { + byte[] source = [0x82, 0x04, 0x00]; + + ReadOnlySpan actual = Rlp.Read(source, static (scoped ref RlpReader r) => r.ReadBytes()); + + ReadOnlySpan expected = [0x04, 0x00]; + actual.SequenceEqual(expected).Should().BeTrue(); + } + + [Test] + public void ReadSetTheoreticalRepresentation() + { + byte[] source = [0xc7, 0xc0, 0xc1, 0xc0, 0xc3, 0xc0, 0xc1, 0xc0]; + + object[] actual = Rlp.Read(source, static (scoped ref RlpReader r) => + { + return r.ReadSequence(static (scoped ref RlpReader r) => + { + var _1 = r.ReadSequence(static (scoped ref RlpReader _) => Array.Empty()); + var _2 = r.ReadSequence(static (scoped ref RlpReader r) => + { + var _1 = r.ReadSequence(static (scoped ref RlpReader _) => Array.Empty()); + return new object[] { _1 }; + }); + var _3 = r.ReadSequence(static (scoped ref RlpReader r) => + { + var _1 = r.ReadSequence(static (scoped ref RlpReader _) => Array.Empty()); + var _2 = r.ReadSequence(static (scoped ref RlpReader r) => + { + var _1 = r.ReadSequence(static (scoped ref RlpReader _) => Array.Empty()); + return new object[] { _1 }; + }); + + return new object[] { _1, _2 }; + }); + + return new object[] { _1, _2, _3 }; + }); + }); + + actual.Should().BeEquivalentTo(new object[] + { + new object[] { }, + new object[] { new object[] { } }, + new object[] { new object[] { }, new object[] { new object[] { } } }, + }); + } + + [Test] + public void ReadTrailingBytes() + { + byte[] source = [0x83, (byte)'d', (byte)'o', (byte)'g']; + + var reader = new RlpReader(source); + _ = reader.ReadString(); + + reader.HasNext.Should().BeFalse(); + reader.BytesRead.Should().Be(source.Length); + } +} diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/RlpWriterTest.cs b/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/RlpWriterTest.cs new file mode 100644 index 00000000000..f601981bc1e --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/RlpWriterTest.cs @@ -0,0 +1,147 @@ +// 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; + +[Parallelizable(ParallelScope.All)] +public class RlpWriterTest +{ + [Test] + public void WriteShortString() + { + var serialized = Rlp.Write(static (ref RlpWriter w) => { w.Write("dog"); }); + + byte[] expected = [0x83, (byte)'d', (byte)'o', (byte)'g']; + serialized.Should().BeEquivalentTo(expected); + } + + [Test] + public void WriteEmptyString() + { + var serialized = Rlp.Write(static (ref RlpWriter w) => { w.Write(""); }); + + byte[] expected = [0x80]; + serialized.Should().BeEquivalentTo(expected); + } + + [Test] + public void WriteLongString() + { + var serialized = Rlp.Write(static (ref RlpWriter w) => + { + w.Write("Lorem ipsum dolor sit amet, consectetur adipisicing elit"); + }); + + byte[] expected = [0xb8, 0x38, .. "Lorem ipsum dolor sit amet, consectetur adipisicing elit"u8]; + serialized.Should().BeEquivalentTo(expected); + } + + [Test] + public void WriteZero() + { + var serialized = Rlp.Write(static (ref RlpWriter w) => { w.Write(0); }); + + byte[] expected = [0x80]; + serialized.Should().BeEquivalentTo(expected); + } + + [Test] + public void WriteInteger_1Component() + { + for (int i = 1; i < 0x80; i++) + { + var integer = i; + var serialized = Rlp.Write(integer, static (ref RlpWriter w, int integer) => { w.Write(integer); }); + + byte[] expected = [(byte)integer]; + serialized.Should().BeEquivalentTo(expected); + } + } + + [Test] + public void WriteInteger_2Components() + { + byte[] expected = [0x81, 0x00]; + for (int i = 0x80; i < 0x0100; i++) + { + var integer = i; + var serialized = Rlp.Write(integer, static (ref RlpWriter w, int integer) => { w.Write(integer); }); + + expected[1] = (byte)integer; + serialized.Should().BeEquivalentTo(expected); + } + } + + [Test] + public void WriteInteger_3Components() + { + byte[] expected = [0x82, 0x00, 0x00]; + for (int i = 0x100; i < 0xFFFF; i++) + { + var integer = i; + var serialized = Rlp.Write(integer, static (ref RlpWriter w, int integer) => { w.Write(integer); }); + + expected[1] = (byte)((integer & 0xFF00) >> 8); + expected[2] = (byte)((integer & 0x00FF) >> 0); + serialized.Should().BeEquivalentTo(expected); + } + } + + [Test] + public void WriteStringList() + { + var serialized = Rlp.Write(static (ref RlpWriter w) => + { + w.WriteSequence(static (ref RlpWriter w) => + { + w.Write("cat"); + w.Write("dog"); + }); + }); + + byte[] expected = [0xc8, 0x83, (byte)'c', (byte)'a', (byte)'t', 0x83, (byte)'d', (byte)'o', (byte)'g']; + serialized.Should().BeEquivalentTo(expected); + } + + [Test] + public void WriteEmptyList() + { + var serialized = Rlp.Write(static (ref RlpWriter w) => { w.WriteSequence(static (ref RlpWriter _) => { }); }); + + byte[] expected = [0xc0]; + serialized.Should().BeEquivalentTo(expected); + } + + [Test] + public void WriteSpan() + { + var serialized = Rlp.Write(static (ref RlpWriter w) => { w.Write([0x04, 0x00]); }); + + byte[] expected = [0x82, 0x04, 0x00]; + serialized.Should().BeEquivalentTo(expected); + } + + [Test] + public void WriteSetTheoreticalRepresentation() + { + var serialized = Rlp.Write(static (ref RlpWriter w) => + { + w.WriteSequence(static (ref RlpWriter w) => + { + w.WriteSequence(static (ref RlpWriter _) => { }); + w.WriteSequence(static (ref RlpWriter w) => { w.WriteSequence(static (ref RlpWriter _) => { }); }); + w.WriteSequence(static (ref RlpWriter w) => + { + w.WriteSequence(static (ref RlpWriter _) => { }); + w.WriteSequence(static (ref RlpWriter w) => { w.WriteSequence(static (ref RlpWriter _) => { }); }); + }); + }); + }); + + byte[] expected = [0xc7, 0xc0, 0xc1, 0xc0, 0xc3, 0xc0, 0xc1, 0xc0]; + serialized.Should().BeEquivalentTo(expected); + } +} diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/Student.cs b/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/Student.cs new file mode 100644 index 00000000000..a8e2b76f1fb --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.FluentRlp.Test/Student.cs @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Serialization.FluentRlp.Generator; +using Nethermind.Serialization.FluentRlp.Instances; + +namespace Nethermind.Serialization.FluentRlp.Test; + +public record Student(string Name, int Age, Dictionary Scores); + +/// +/// The following class can be automatically generated by annotating with . +/// +public abstract class StudentRlpConverter : IRlpConverter +{ + public static Student Read(ref RlpReader reader) + { + return reader.ReadSequence(static (scoped ref RlpReader r) => + { + var name = r.ReadString(); + var age = r.ReadInt32(); + var scores = r.ReadSequence(static (scoped ref RlpReader r) => + { + Dictionary scores = []; + while (r.HasNext) + { + var subject = r.ReadString(); + var score = r.ReadInt32(); + + scores[subject] = score; + } + + return scores; + }); + + return new Student(name, age, scores); + }); + } + + public static void Write(ref RlpWriter writer, Student value) + { + writer.WriteSequence(value, static (ref RlpWriter w, Student value) => + { + w.Write(value.Name); + w.Write(value.Age); + w.WriteSequence(value, (ref RlpWriter w, Student value) => + { + foreach (var (subject, score) in value.Scores) + { + w.Write(subject); + w.Write(score); + } + }); + }); + } +} + +public static class StudentExt +{ + public static Student ReadStudent(this ref RlpReader reader) => StudentRlpConverter.Read(ref reader); + public static void Write(this ref RlpWriter writer, Student value) => StudentRlpConverter.Write(ref writer, value); +} diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp/IRlpConverter.cs b/src/Nethermind/Nethermind.Serialization.FluentRlp/IRlpConverter.cs new file mode 100644 index 00000000000..5fd7a14ed04 --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.FluentRlp/IRlpConverter.cs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Serialization.FluentRlp; + +public interface IRlpConverter where T : allows ref struct +{ + public static abstract T Read(ref RlpReader reader); + public static abstract void Write(ref RlpWriter writer, T value); +} diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp/Instances/ArrayRlpConverter.cs b/src/Nethermind/Nethermind.Serialization.FluentRlp/Instances/ArrayRlpConverter.cs new file mode 100644 index 00000000000..eaa0ed50236 --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.FluentRlp/Instances/ArrayRlpConverter.cs @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; + +namespace Nethermind.Serialization.FluentRlp.Instances; + +public abstract class ArrayRlpConverter +{ + public static T[] Read(ref RlpReader reader, RefRlpReaderFunc func) + { + return reader.ReadSequence(func, static (scoped ref RlpReader r, RefRlpReaderFunc func) => + { + List result = []; + while (r.HasNext) + { + result.Add(func(ref r)); + } + + // TODO: Avoid copying + return result.ToArray(); + }); + } + + public static void Write(ref RlpWriter writer, T[] value, RefRlpWriterAction action) + { + var ctx = ValueTuple.Create(value, action); + writer.WriteSequence(ctx, static (ref RlpWriter w, (T[], RefRlpWriterAction) ctx) => + { + var (value, action) = ctx; + foreach (T v in value) + { + action(ref w, v); + } + }); + } +} + +public static class ArrayRlpConverterExt +{ + public static T[] ReadArray(this ref RlpReader reader, RefRlpReaderFunc func) + => ArrayRlpConverter.Read(ref reader, func); + + public static void Write(this ref RlpWriter writer, T[] value, RefRlpWriterAction action) + => ArrayRlpConverter.Write(ref writer, value, action); +} diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp/Instances/DictionaryRlpConverter.cs b/src/Nethermind/Nethermind.Serialization.FluentRlp/Instances/DictionaryRlpConverter.cs new file mode 100644 index 00000000000..d07b6892c09 --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.FluentRlp/Instances/DictionaryRlpConverter.cs @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; + +namespace Nethermind.Serialization.FluentRlp.Instances; + +public abstract class DictionaryRlpConverter where TKey : notnull +{ + public static Dictionary Read(ref RlpReader reader, RefRlpReaderFunc readKey, RefRlpReaderFunc readValue) + { + var ctx = ValueTuple.Create(readKey, readValue); + return reader.ReadSequence(ctx, static (scoped ref RlpReader r, (RefRlpReaderFunc, RefRlpReaderFunc) ctx) => + { + Dictionary result = []; + while (r.HasNext) + { + + (TKey key, TValue value) = r.ReadSequence(ctx, static (scoped ref RlpReader r, (RefRlpReaderFunc, RefRlpReaderFunc) ctx) => + { + var (readKey, readValue) = ctx; + TKey key = readKey(ref r); + TValue value = readValue(ref r); + + return (key, value); + }); + + result.Add(key, value); + } + + return result; + }); + } + + public static void Write(ref RlpWriter writer, Dictionary value, RefRlpWriterAction writeKey, RefRlpWriterAction writeValue) + { + var ctx = ValueTuple.Create(value, writeKey, writeValue); + writer.WriteSequence(ctx, static (ref RlpWriter w, (Dictionary, RefRlpWriterAction, RefRlpWriterAction) ctx) => + { + var (dictionary, writeKey, writeValue) = ctx; + foreach (var kp in dictionary) + { + var innerCtx = ValueTuple.Create(kp, writeKey, writeValue); + w.WriteSequence(innerCtx, static (ref RlpWriter w, (KeyValuePair, RefRlpWriterAction, RefRlpWriterAction) ctx) => + { + var ((k, v), writeKey, writeValue) = ctx; + writeKey(ref w, k); + writeValue(ref w, v); + }); + } + }); + } +} + +public static class DictionaryRlpConverterExt +{ + public static Dictionary ReadDictionary( + this ref RlpReader reader, + RefRlpReaderFunc readKey, + RefRlpReaderFunc readValue + ) where TKey : notnull + => DictionaryRlpConverter.Read(ref reader, readKey, readValue); + + public static void Write( + this ref RlpWriter writer, + Dictionary value, + RefRlpWriterAction writeKey, + RefRlpWriterAction writeValue + ) where TKey : notnull + => DictionaryRlpConverter.Write(ref writer, value, writeKey, writeValue); +} diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp/Instances/IntegerRlpConverter.cs b/src/Nethermind/Nethermind.Serialization.FluentRlp/Instances/IntegerRlpConverter.cs new file mode 100644 index 00000000000..ee67bc98074 --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.FluentRlp/Instances/IntegerRlpConverter.cs @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; + +namespace Nethermind.Serialization.FluentRlp.Instances; + +public abstract class Int16RlpConverter : IRlpConverter +{ + public static Int16 Read(ref RlpReader reader) => reader.ReadInteger(); + + public static void Write(ref RlpWriter writer, Int16 value) => writer.Write(value); +} + +public abstract class Int32RlpConverter : IRlpConverter +{ + public static Int32 Read(ref RlpReader reader) => reader.ReadInteger(); + + public static void Write(ref RlpWriter writer, Int32 value) => writer.Write(value); +} + +public abstract class Int64RlpConverter : IRlpConverter +{ + public static Int64 Read(ref RlpReader reader) => reader.ReadInteger(); + + public static void Write(ref RlpWriter writer, Int64 value) => writer.Write(value); +} + +public abstract class Int128RlpConverter : IRlpConverter +{ + public static Int128 Read(ref RlpReader reader) => reader.ReadInteger(); + + public static void Write(ref RlpWriter writer, Int128 value) => writer.Write(value); +} + +public abstract class UInt16RlpConverter : IRlpConverter +{ + public static UInt16 Read(ref RlpReader reader) => reader.ReadInteger(); + + public static void Write(ref RlpWriter writer, UInt16 value) => writer.Write(value); +} + +public abstract class UInt32RlpConverter : IRlpConverter +{ + public static UInt32 Read(ref RlpReader reader) => reader.ReadInteger(); + + public static void Write(ref RlpWriter writer, UInt32 value) => writer.Write(value); +} + +public abstract class UInt64RlpConverter : IRlpConverter +{ + public static UInt64 Read(ref RlpReader reader) => reader.ReadInteger(); + + public static void Write(ref RlpWriter writer, UInt64 value) => writer.Write(value); +} + +public abstract class UInt128RlpConverter : IRlpConverter +{ + public static UInt128 Read(ref RlpReader reader) => reader.ReadInteger(); + + public static void Write(ref RlpWriter writer, UInt128 value) => writer.Write(value); +} + +// NOTE: No need for `Write` overloads since they're covered by generic primitives +// `Read` methods are provided for a consistent API (instead of using generics primitives) +public static class IntegerRlpConverterExt +{ + public static Int16 ReadInt16(this ref RlpReader reader) => reader.ReadInteger(); + public static Int32 ReadInt32(this ref RlpReader reader) => reader.ReadInteger(); + public static Int64 ReadInt64(this ref RlpReader reader) => reader.ReadInteger(); + public static Int128 ReadInt128(this ref RlpReader reader) => reader.ReadInteger(); + public static UInt16 ReadUInt16(this ref RlpReader reader) => reader.ReadInteger(); + public static UInt64 UInt64(this ref RlpReader reader) => reader.ReadInteger(); + public static UInt64 ReadUInt64(this ref RlpReader reader) => reader.ReadInteger(); + public static UInt128 UReadInt128(this ref RlpReader reader) => reader.ReadInteger(); +} diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp/Instances/ListRlpConverter.cs b/src/Nethermind/Nethermind.Serialization.FluentRlp/Instances/ListRlpConverter.cs new file mode 100644 index 00000000000..5e6db5b22be --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.FluentRlp/Instances/ListRlpConverter.cs @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; + +namespace Nethermind.Serialization.FluentRlp.Instances; + +public abstract class ListRlpConverter +{ + public static List Read(ref RlpReader reader, RefRlpReaderFunc func) + { + return reader.ReadSequence(func, static (scoped ref RlpReader r, RefRlpReaderFunc func) => + { + List result = []; + while (r.HasNext) + { + result.Add(func(ref r)); + } + + return result; + }); + } + + public static void Write(ref RlpWriter writer, List value, RefRlpWriterAction action) + { + var ctx = ValueTuple.Create(value, action); + writer.WriteSequence(ctx, static (ref RlpWriter w, (List, RefRlpWriterAction) ctx) => + { + var (value, action) = ctx; + foreach (T v in value) + { + action(ref w, v); + } + }); + } +} + +public static class ListRlpConverterExt +{ + public static List ReadList(this ref RlpReader reader, RefRlpReaderFunc func) + => ListRlpConverter.Read(ref reader, func); + + public static void Write(this ref RlpWriter writer, List value, RefRlpWriterAction action) + => ListRlpConverter.Write(ref writer, value, action); +} diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp/Instances/StringRlpConverter.cs b/src/Nethermind/Nethermind.Serialization.FluentRlp/Instances/StringRlpConverter.cs new file mode 100644 index 00000000000..dcc1158e6e3 --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.FluentRlp/Instances/StringRlpConverter.cs @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Buffers; +using System.Text; + +namespace Nethermind.Serialization.FluentRlp.Instances; + +public abstract class StringRlpConverter : IRlpConverter +{ + private const int MaxStackSize = 256; + private static readonly Encoding Encoding = Encoding.UTF8; + + public static string Read(ref RlpReader reader) + { + ReadOnlySpan obj = reader.ReadBytes(); + return Encoding.GetString(obj); + } + + public static void Write(ref RlpWriter writer, string value) + { + ReadOnlySpan charSpan = value.AsSpan(); + var valueByteLength = Encoding.GetMaxByteCount(charSpan.Length); + + byte[]? sharedBuffer = null; + try + { + Span buffer = valueByteLength <= MaxStackSize + ? stackalloc byte[valueByteLength] + : sharedBuffer = ArrayPool.Shared.Rent(valueByteLength); + + var bytes = Encoding.GetBytes(charSpan, buffer); + + writer.Write(buffer[..bytes]); + } + finally + { + if (sharedBuffer is not null) ArrayPool.Shared.Return(sharedBuffer); + } + } +} + +public static class StringRlpConverterExt +{ + public static string ReadString(this ref RlpReader reader) => StringRlpConverter.Read(ref reader); + public static void Write(this ref RlpWriter writer, string value) => StringRlpConverter.Write(ref writer, value); +} diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp/Instances/TupleRlpConverter.cs b/src/Nethermind/Nethermind.Serialization.FluentRlp/Instances/TupleRlpConverter.cs new file mode 100644 index 00000000000..e0a20154092 --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.FluentRlp/Instances/TupleRlpConverter.cs @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Serialization.FluentRlp.Instances; + +public abstract class TupleRlpConverter +{ + public static (T1, T2) Read(ref RlpReader reader, RefRlpReaderFunc read1, RefRlpReaderFunc read2) + { + T1 _1 = read1(ref reader); + T2 _2 = read2(ref reader); + + return (_1, _2); + } + + public static void Write(ref RlpWriter writer, (T1, T2) value, RefRlpWriterAction write1, RefRlpWriterAction write2) + { + write1(ref writer, value.Item1); + write2(ref writer, value.Item2); + } +} + +public static class TupleRlpConverterExt +{ + public static (T1, T2) ReadTuple( + this ref RlpReader reader, + RefRlpReaderFunc read1, + RefRlpReaderFunc read2 + ) => TupleRlpConverter.Read(ref reader, read1, read2); + + public static void Write( + this ref RlpWriter writer, + (T1, T2) value, + RefRlpWriterAction write1, + RefRlpWriterAction write2 + ) => TupleRlpConverter.Write(ref writer, value, write1, write2); +} diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp/Int32Primitive.cs b/src/Nethermind/Nethermind.Serialization.FluentRlp/Int32Primitive.cs new file mode 100644 index 00000000000..f47c8700ce2 --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.FluentRlp/Int32Primitive.cs @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Buffers.Binary; + +namespace Nethermind.Serialization.FluentRlp; + +internal static class Int32Primitive +{ + /// + /// Reads a from the beginning of a read-only span of bytes, as big endian. + /// + /// The read-only span to read. + /// The big endian value. + /// The span is padded with leading `0`s as needed. + public static int Read(ReadOnlySpan source) + { + Span buffer = stackalloc byte[sizeof(Int32)]; + source.CopyTo(buffer[^source.Length..]); + return BinaryPrimitives.ReadInt32BigEndian(buffer); + } +} diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp/Nethermind.Serialization.FluentRlp.csproj b/src/Nethermind/Nethermind.Serialization.FluentRlp/Nethermind.Serialization.FluentRlp.csproj new file mode 100644 index 00000000000..f3ba37c6fa3 --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.FluentRlp/Nethermind.Serialization.FluentRlp.csproj @@ -0,0 +1,8 @@ + + + + enable + true + + + diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp/README.md b/src/Nethermind/Nethermind.Serialization.FluentRlp/README.md new file mode 100644 index 00000000000..2a5a75aff0f --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.FluentRlp/README.md @@ -0,0 +1,9 @@ +# FluentRlp + +Declarative RLP encoding a decoding with support for extensibility through manually written `IRlpConverter`s and automatically generated through attributes. + +## TODO + +- Add support more instances for base types +- Alternative API for writing based on `Async` and `Stream` +- Support for parameterizable names when using attributes diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp/Rlp.cs b/src/Nethermind/Nethermind.Serialization.FluentRlp/Rlp.cs new file mode 100644 index 00000000000..e918ebdbd33 --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.FluentRlp/Rlp.cs @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Buffers; +using System.Diagnostics; + +namespace Nethermind.Serialization.FluentRlp; + +public static class Rlp +{ + public static byte[] Write(RefRlpWriterAction action) + => Write(action, static (ref RlpWriter w, RefRlpWriterAction action) => action(ref w)); + + public static byte[] Write(TContext ctx, RefRlpWriterAction action) + where TContext : allows ref struct + { + var lengthWriter = RlpWriter.LengthWriter(); + action(ref lengthWriter, ctx); + var bufferWriter = new FixedArrayBufferWriter(lengthWriter.Length); + var contentWriter = RlpWriter.ContentWriter(bufferWriter); + action(ref contentWriter, ctx); + + return bufferWriter.Buffer; + } + + public static T Read(ReadOnlySpan source, RefRlpReaderFunc func) + where T : allows ref struct + { + var reader = new RlpReader(source); + T result = func(ref reader); + if (reader.HasNext) throw new RlpReaderException("RLP has trailing bytes"); + return result; + } +} + +/// +/// The existing performs various bound checks and supports resizing buffers +/// which we don't need for our use case. +/// +internal class FixedArrayBufferWriter : IBufferWriter +{ + private readonly T[] _buffer; + private int _index; + + public T[] Buffer => _buffer; + + /// + /// Creates an instance of an , in which data can be written to, + /// with a fixed capacity specified. + /// + /// The capacity of the underlying buffer. + public FixedArrayBufferWriter(int capacity) + { + _buffer = new T[capacity]; + _index = 0; + } + + public void Advance(int count) + { + _index += count; + } + + public Memory GetMemory(int sizeHint = 0) + { + Debug.Assert(_buffer.Length > _index); + return _buffer.AsMemory(_index); + } + + public Span GetSpan(int sizeHint = 0) + { + Debug.Assert(_buffer.Length > _index); + return _buffer.AsSpan(_index); + } +} diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp/RlpReader.cs b/src/Nethermind/Nethermind.Serialization.FluentRlp/RlpReader.cs new file mode 100644 index 00000000000..6d7048de3bd --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.FluentRlp/RlpReader.cs @@ -0,0 +1,154 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Numerics; + +namespace Nethermind.Serialization.FluentRlp; + +public delegate TResult RefRlpReaderFunc(scoped ref RlpReader arg, TContext value) + where TResult : allows ref struct; + +public delegate TResult RefRlpReaderFunc(scoped ref RlpReader arg) + where TResult : allows ref struct; + +public class RlpReaderException(string message) : Exception(message); + +public ref struct RlpReader +{ + private readonly ReadOnlySpan _buffer; + private int _position; + + public RlpReader(ReadOnlySpan buffer) + { + _buffer = buffer; + _position = 0; + } + + public bool HasNext => _position < _buffer.Length; + public int BytesRead => _position; + + public unsafe T ReadInteger() where T : unmanaged, IBinaryInteger + { + ReadOnlySpan bigEndian; + var header = _buffer[_position]; + if (header < 0x80) + { + bigEndian = _buffer.Slice(_position++, 1); + } + else + { + bigEndian = ReadBytes(); + } + + Span buffer = stackalloc byte[sizeof(T)]; + bigEndian.CopyTo(buffer[^bigEndian.Length..]); + return T.ReadBigEndian(buffer, false); + } + + public ReadOnlySpan ReadBytes() + { + ReadOnlySpan result; + var header = _buffer[_position]; + if (header < 0x80 || header >= 0xC0) + { + throw new RlpReaderException("RLP does not correspond to a byte string"); + } + + if (header < 0xB8) + { + header -= 0x80; + result = _buffer.Slice(++_position, header); + _position += header; + } + else + { + header -= 0xB7; + ReadOnlySpan binaryLength = _buffer.Slice(++_position, header); + _position += header; + var length = Int32Primitive.Read(binaryLength); + result = _buffer.Slice(_position, length); + _position += length; + } + + return result; + } + + public T ReadSequence(RefRlpReaderFunc func) + => ReadSequence(func, static (scoped ref RlpReader reader, RefRlpReaderFunc func) => func(ref reader)); + + public T ReadSequence(TContext ctx, RefRlpReaderFunc func) + { + T result; + var header = _buffer[_position++]; + if (header < 0xC0) + { + throw new RlpReaderException("RLP does not correspond to a sequence"); + } + + if (header < 0xF8) + { + var length = header - 0xC0; + var reader = new RlpReader(_buffer.Slice(_position, length)); + result = func(ref reader, ctx); + _position += length; + } + else + { + var lengthOfLength = header - 0xF7; + ReadOnlySpan binaryLength = _buffer.Slice(_position, lengthOfLength); + _position += lengthOfLength; + int length = Int32Primitive.Read(binaryLength); + var reader = new RlpReader(_buffer.Slice(_position, length)); + result = func(ref reader, ctx); + _position += length; + } + + return result; + } + + public T Choice(params ReadOnlySpan> alternatives) + { + int startingPosition = _position; + foreach (var f in alternatives) + { + try + { + return f(ref this); + } + catch (Exception) + { + _position = startingPosition; + } + } + throw new RlpReaderException("RLP does not correspond to any alternative"); + } + + public T? Optional(RefRlpReaderFunc f, T? _ = null) where T : class + { + int startingPosition = _position; + try + { + return f(ref this); + } + catch (Exception) + { + _position = startingPosition; + return null; + } + } + + public T? Optional(RefRlpReaderFunc f, T? _ = null) where T : struct + { + int startingPosition = _position; + try + { + return f(ref this); + } + catch (Exception) + { + _position = startingPosition; + return null; + } + } +} diff --git a/src/Nethermind/Nethermind.Serialization.FluentRlp/RlpWriter.cs b/src/Nethermind/Nethermind.Serialization.FluentRlp/RlpWriter.cs new file mode 100644 index 00000000000..bcc144b28b5 --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.FluentRlp/RlpWriter.cs @@ -0,0 +1,201 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Buffers; +using System.Buffers.Binary; +using System.Numerics; + +namespace Nethermind.Serialization.FluentRlp; + +public delegate void RefRlpWriterAction(ref RlpWriter arg, TContext value) + where TContext : allows ref struct; + +public delegate void RefRlpWriterAction(ref RlpWriter arg); + +public ref struct RlpWriter +{ + private const bool LengthMode = false; + private const bool ContentMode = true; + + private bool _mode; + + public int Length { get; private set; } + + private IBufferWriter _buffer; + + public static RlpWriter LengthWriter() + { + return new RlpWriter + { + _mode = LengthMode + }; + } + + public static RlpWriter ContentWriter(IBufferWriter buffer) + { + return new RlpWriter + { + _mode = ContentMode, + _buffer = buffer + }; + } + + public void Write(T value) where T : unmanaged, IBinaryInteger + { + switch (_mode) + { + case LengthMode: + LengthWrite(value); + break; + case ContentMode: + ContentWrite(value); + break; + } + } + + private unsafe void LengthWrite(T value) where T : unmanaged, IBinaryInteger + { + var size = sizeof(T); + Span bigEndian = stackalloc byte[size]; + value.WriteBigEndian(bigEndian); + bigEndian = bigEndian.TrimStart((byte)0); + + if (bigEndian.Length == 0) + { + Length++; + } + else if (bigEndian.Length == 1 && bigEndian[0] < 0x80) + { + Length++; + } + else + { + LengthWrite(bigEndian); + } + } + + private unsafe void ContentWrite(T value) where T : unmanaged, IBinaryInteger + { + var size = sizeof(T); + Span bigEndian = stackalloc byte[size]; + value.WriteBigEndian(bigEndian); + bigEndian = bigEndian.TrimStart((byte)0); + + if (bigEndian.Length == 0) + { + _buffer.Write([(byte)0x80]); + } + else if (bigEndian.Length == 1 && bigEndian[0] < 0x80) + { + _buffer.Write(bigEndian[..1]); + } + else + { + ContentWrite(bigEndian); + } + } + + public void Write(scoped ReadOnlySpan value) + { + switch (_mode) + { + case LengthMode: + LengthWrite(value); + break; + case ContentMode: + ContentWrite(value); + break; + } + } + + private void LengthWrite(scoped ReadOnlySpan value) + { + if (value.Length < 55) + { + Length++; + } + else + { + Span binaryLength = stackalloc byte[sizeof(int)]; + BinaryPrimitives.WriteInt32BigEndian(binaryLength, value.Length); + binaryLength = binaryLength.TrimStart((byte)0); + Length += 1 + binaryLength.Length; + } + + Length += value.Length; + } + + private void ContentWrite(scoped ReadOnlySpan value) + { + if (value.Length < 55) + { + _buffer.Write([(byte)(0x80 + value.Length)]); + } + else + { + Span binaryLength = stackalloc byte[sizeof(int)]; + BinaryPrimitives.WriteInt32BigEndian(binaryLength, value.Length); + binaryLength = binaryLength.TrimStart((byte)0); + + _buffer.Write([(byte)(0xB7 + binaryLength.Length)]); + _buffer.Write(binaryLength); + } + + _buffer.Write(value); + } + + public void WriteSequence(RefRlpWriterAction action) + => WriteSequence(action, static (ref RlpWriter w, RefRlpWriterAction action) => action(ref w)); + + public void WriteSequence(TContext ctx, RefRlpWriterAction action) + { + switch (_mode) + { + case LengthMode: + LengthWriteSequence(ctx, action); + break; + case ContentMode: + ContentWriteSequence(ctx, action); + break; + } + } + + private void LengthWriteSequence(TContext ctx, RefRlpWriterAction action) + { + var inner = LengthWriter(); + action(ref inner, ctx); + if (inner.Length < 55) + { + Length += 1 + inner.Length; + } + else + { + Span binaryLength = stackalloc byte[sizeof(Int32)]; + BinaryPrimitives.WriteInt32BigEndian(binaryLength, inner.Length); + binaryLength = binaryLength.TrimStart((byte)0); + Length += 1 + inner.Length + binaryLength.Length; + } + } + + private void ContentWriteSequence(TContext ctx, RefRlpWriterAction action) + { + var lengthWriter = LengthWriter(); + action(ref lengthWriter, ctx); + if (lengthWriter.Length < 55) + { + _buffer.Write([(byte)(0xC0 + lengthWriter.Length)]); + } + else + { + Span binaryLength = stackalloc byte[sizeof(Int32)]; + BinaryPrimitives.WriteInt32BigEndian(binaryLength, lengthWriter.Length); + binaryLength = binaryLength.TrimStart((byte)0); + + _buffer.Write([(byte)(0xF7 + binaryLength.Length)]); + _buffer.Write(binaryLength); + } + + action(ref this, ctx); + } +} diff --git a/src/Nethermind/Nethermind.Serialization.Rlp.Benchmark/Nethermind.Serialization.Rlp.Benchmark.csproj b/src/Nethermind/Nethermind.Serialization.Rlp.Benchmark/Nethermind.Serialization.Rlp.Benchmark.csproj new file mode 100644 index 00000000000..74025581ed1 --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.Rlp.Benchmark/Nethermind.Serialization.Rlp.Benchmark.csproj @@ -0,0 +1,20 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + + + + + diff --git a/src/Nethermind/Nethermind.Serialization.Rlp.Benchmark/Program.cs b/src/Nethermind/Nethermind.Serialization.Rlp.Benchmark/Program.cs new file mode 100644 index 00000000000..32c290c3480 --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.Rlp.Benchmark/Program.cs @@ -0,0 +1,120 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using Nethermind.Int256; +using Nethermind.Serialization.FluentRlp; +using Nethermind.Serialization.FluentRlp.Generator; + +namespace Nethermind.Serialization.Rlp.Benchmark; + +[RlpSerializable(RlpRepresentation.Newtype)] +public record Address(byte[] Bytes) +{ + public const int Size = 20; +} + +[RlpSerializable] +public record AccessList(List<(Address, List)> Addresses); + +[MemoryDiagnoser] +public class CurrentFluentBenchmark +{ + private readonly Nethermind.Core.Eip2930.AccessList _current; + private readonly AccessList _fluent; + + public CurrentFluentBenchmark() + { + _current = Benchmark.Current.BuildAccessList(new Random(42)); + _fluent = Benchmark.Fluent.BuildAccessList(new Random(42)); + } + + [Benchmark(Baseline = true)] + public Nethermind.Core.Eip2930.AccessList Current() + { + var decoder = Eip2930.AccessListDecoder.Instance; + + var length = decoder.GetLength(_current, RlpBehaviors.None); + var stream = new RlpStream(length); + decoder.Encode(stream, _current); + + stream.Reset(); + + var decoded = decoder.Decode(stream); + return decoded!; + } + + [Benchmark] + public AccessList Fluent() + { + var decoded = FluentRlp.Rlp.Write(_fluent, (ref RlpWriter writer, AccessList value) => writer.Write(value)); + var encoded = FluentRlp.Rlp.Read(decoded, (scoped ref RlpReader reader) => reader.ReadAccessList()); + + return encoded; + } +} + +public static class Current +{ + private static Nethermind.Core.Address BuildAddress(Random rnd) + { + var bytes = new byte[Core.Address.Size]; + rnd.NextBytes(bytes); + return new Nethermind.Core.Address(bytes); + } + + public static Nethermind.Core.Eip2930.AccessList BuildAccessList(Random rnd) + { + var builder = new Nethermind.Core.Eip2930.AccessList.Builder(); + for (int i = 0; i < 1_000; i++) + { + builder.AddAddress(BuildAddress(rnd)); + var keyCount = rnd.Next(10); + for (int j = 0; j < keyCount; j++) + { + var bytes = new byte[32]; + rnd.NextBytes(bytes); + builder.AddStorage(new UInt256(bytes)); + } + } + + return builder.Build(); + } +} + +public static class Fluent +{ + private static Address BuildAddress(Random rnd) + { + var bytes = new byte[Address.Size]; + rnd.NextBytes(bytes); + return new Address(bytes); + } + + public static AccessList BuildAccessList(Random rnd) + { + var result = new List<(Address, List)>(1_000); + for (int i = 0; i < 1_000; i++) + { + Address address = BuildAddress(rnd); + List keys = []; + var keyCount = rnd.Next(10); + for (int j = 0; j < keyCount; j++) + { + var bytes = new byte[32]; + rnd.NextBytes(bytes); + keys.Add(new UInt256(bytes)); + } + + result.Add((address, keys)); + } + + return new AccessList(result); + } +} + +public static class Program +{ + public static void Main() + { + BenchmarkRunner.Run(typeof(Program).Assembly); + } +} diff --git a/src/Nethermind/Nethermind.Serialization.Rlp.Benchmark/UInt256RlpConverter.cs b/src/Nethermind/Nethermind.Serialization.Rlp.Benchmark/UInt256RlpConverter.cs new file mode 100644 index 00000000000..95a421e0c59 --- /dev/null +++ b/src/Nethermind/Nethermind.Serialization.Rlp.Benchmark/UInt256RlpConverter.cs @@ -0,0 +1,156 @@ +// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Numerics; +using Nethermind.Int256; +using Nethermind.Serialization.FluentRlp; + +namespace Nethermind.Serialization.Rlp.Benchmark; + +// NOTE: This converter is required since `UInt256` does not implement `IBinaryInteger` (which it should) +public abstract class UInt256RlpConverter : IRlpConverter +{ + private readonly struct Wrap(UInt256 value) : IBinaryInteger + { + public UInt256 Unwrap => value; + + public static bool TryReadBigEndian(ReadOnlySpan source, bool isUnsigned, out Wrap value) + { + var uint256 = new UInt256(source, isBigEndian: true); + value = new Wrap(uint256); + return true; + } + + public bool TryWriteBigEndian(Span destination, out int bytesWritten) + { + var uint256 = Unwrap; + uint256.ToBigEndian(destination); + bytesWritten = 32; + return true; + } + + public int WriteBigEndian(Span destination) + { + var uint256 = Unwrap; + uint256.ToBigEndian(destination); + return 32; + } + + public int WriteBigEndian(byte[] destination, int startIndex) + { + var uint256 = Unwrap; + uint256.ToBigEndian(destination.AsSpan()[startIndex..]); + return 32; + } + + public int WriteBigEndian(byte[] destination) + { + var uint256 = Unwrap; + uint256.ToBigEndian(destination); + return 32; + } + + // NOTE: None of the following are required + public override bool Equals(object? obj) => obj is Wrap other && Equals(other); + public override int GetHashCode() => throw new NotImplementedException(); + public int CompareTo(object? obj) => throw new NotImplementedException(); + public int CompareTo(Wrap other) => throw new NotImplementedException(); + public bool Equals(Wrap other) => throw new NotImplementedException(); + public string ToString(string? format, IFormatProvider? formatProvider) => throw new NotImplementedException(); + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) => throw new NotImplementedException(); + public static Wrap Parse(string s, IFormatProvider? provider) => throw new NotImplementedException(); + public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out Wrap result) => throw new NotImplementedException(); + public static Wrap Parse(ReadOnlySpan s, IFormatProvider? provider) => throw new NotImplementedException(); + public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, out Wrap result) => throw new NotImplementedException(); + public static Wrap operator +(Wrap left, Wrap right) => throw new NotImplementedException(); + public static Wrap AdditiveIdentity { get; } + public static Wrap operator &(Wrap left, Wrap right) => throw new NotImplementedException(); + public static Wrap operator |(Wrap left, Wrap right) => throw new NotImplementedException(); + public static Wrap operator ^(Wrap left, Wrap right) => throw new NotImplementedException(); + public static Wrap operator ~(Wrap value) => throw new NotImplementedException(); + public static bool operator ==(Wrap left, Wrap right) => throw new NotImplementedException(); + public static bool operator !=(Wrap left, Wrap right) => throw new NotImplementedException(); + public static bool operator >(Wrap left, Wrap right) => throw new NotImplementedException(); + public static bool operator >=(Wrap left, Wrap right) => throw new NotImplementedException(); + public static bool operator <(Wrap left, Wrap right) => throw new NotImplementedException(); + public static bool operator <=(Wrap left, Wrap right) => throw new NotImplementedException(); + public static Wrap operator --(Wrap value) => throw new NotImplementedException(); + public static Wrap operator /(Wrap left, Wrap right) => throw new NotImplementedException(); + public static Wrap operator ++(Wrap value) => throw new NotImplementedException(); + public static Wrap operator %(Wrap left, Wrap right) => throw new NotImplementedException(); + public static Wrap MultiplicativeIdentity { get; } + public static Wrap operator *(Wrap left, Wrap right) => throw new NotImplementedException(); + public static Wrap operator -(Wrap left, Wrap right) => throw new NotImplementedException(); + public static Wrap operator -(Wrap value) => throw new NotImplementedException(); + public static Wrap operator +(Wrap value) => throw new NotImplementedException(); + public static Wrap Abs(Wrap value) => throw new NotImplementedException(); + public static bool IsCanonical(Wrap value) => throw new NotImplementedException(); + public static bool IsComplexNumber(Wrap value) => throw new NotImplementedException(); + public static bool IsEvenInteger(Wrap value) => throw new NotImplementedException(); + public static bool IsFinite(Wrap value) => throw new NotImplementedException(); + public static bool IsImaginaryNumber(Wrap value) => throw new NotImplementedException(); + public static bool IsInfinity(Wrap value) => throw new NotImplementedException(); + public static bool IsInteger(Wrap value) => throw new NotImplementedException(); + public static bool IsNaN(Wrap value) => throw new NotImplementedException(); + public static bool IsNegative(Wrap value) => throw new NotImplementedException(); + public static bool IsNegativeInfinity(Wrap value) => throw new NotImplementedException(); + public static bool IsNormal(Wrap value) => throw new NotImplementedException(); + public static bool IsOddInteger(Wrap value) => throw new NotImplementedException(); + public static bool IsPositive(Wrap value) => throw new NotImplementedException(); + public static bool IsPositiveInfinity(Wrap value) => throw new NotImplementedException(); + public static bool IsRealNumber(Wrap value) => throw new NotImplementedException(); + public static bool IsSubnormal(Wrap value) => throw new NotImplementedException(); + public static bool IsZero(Wrap value) => throw new NotImplementedException(); + public static Wrap MaxMagnitude(Wrap x, Wrap y) => throw new NotImplementedException(); + public static Wrap MaxMagnitudeNumber(Wrap x, Wrap y) => throw new NotImplementedException(); + public static Wrap MinMagnitude(Wrap x, Wrap y) => throw new NotImplementedException(); + public static Wrap MinMagnitudeNumber(Wrap x, Wrap y) => throw new NotImplementedException(); + public static Wrap Parse(ReadOnlySpan s, NumberStyles style, IFormatProvider? provider) => throw new NotImplementedException(); + public static Wrap Parse(string s, NumberStyles style, IFormatProvider? provider) => throw new NotImplementedException(); + public static bool TryConvertFromChecked(TOther value, out Wrap result) where TOther : INumberBase => throw new NotImplementedException(); + public static bool TryConvertFromSaturating(TOther value, out Wrap result) where TOther : INumberBase => throw new NotImplementedException(); + public static bool TryConvertFromTruncating(TOther value, out Wrap result) where TOther : INumberBase => throw new NotImplementedException(); + public static bool TryConvertToChecked(Wrap value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase => throw new NotImplementedException(); + public static bool TryConvertToSaturating(Wrap value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase => throw new NotImplementedException(); + public static bool TryConvertToTruncating(Wrap value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase => throw new NotImplementedException(); + public static bool TryParse(ReadOnlySpan s, NumberStyles style, IFormatProvider? provider, out Wrap result) => throw new NotImplementedException(); + public static bool TryParse([NotNullWhen(true)] string? s, NumberStyles style, IFormatProvider? provider, out Wrap result) => throw new NotImplementedException(); + public static Wrap One { get; } + public static int Radix { get; } + public static Wrap Zero { get; } + public static bool IsPow2(Wrap value) => throw new NotImplementedException(); + public static Wrap Log2(Wrap value) => throw new NotImplementedException(); + public static Wrap operator <<(Wrap value, int shiftAmount) => throw new NotImplementedException(); + public static Wrap operator >>(Wrap value, int shiftAmount) => throw new NotImplementedException(); + public static Wrap operator >>>(Wrap value, int shiftAmount) => throw new NotImplementedException(); + public int GetByteCount() => throw new NotImplementedException(); + public int GetShortestBitLength() => throw new NotImplementedException(); + public static Wrap PopCount(Wrap value) => throw new NotImplementedException(); + public static Wrap TrailingZeroCount(Wrap value) => throw new NotImplementedException(); + public static bool TryReadLittleEndian(ReadOnlySpan source, bool isUnsigned, out Wrap value) => throw new NotImplementedException(); + public bool TryWriteLittleEndian(Span destination, out int bytesWritten) => throw new NotImplementedException(); + public int WriteLittleEndian(Span destination) => throw new NotImplementedException(); + public int WriteLittleEndian(byte[] destination) => throw new NotImplementedException(); + public int WriteLittleEndian(byte[] destination, int startIndex) => throw new NotImplementedException(); + } + + public static UInt256 Read(ref RlpReader reader) + { + var wrap = reader.ReadInteger(); + return wrap.Unwrap; + } + + public static void Write(ref RlpWriter writer, UInt256 value) + { + var wrap = new Wrap(value); + writer.Write(wrap); + } +} + +public static class UInt256RlpConverterExt +{ + public static UInt256 ReadUInt256(this ref RlpReader reader) => UInt256RlpConverter.Read(ref reader); + public static void Write(this ref RlpWriter writer, UInt256 value) => UInt256RlpConverter.Write(ref writer, value); +} diff --git a/src/Nethermind/Nethermind.sln b/src/Nethermind/Nethermind.sln index 44cf9aa69bd..6c84eec649d 100644 --- a/src/Nethermind/Nethermind.sln +++ b/src/Nethermind/Nethermind.sln @@ -226,6 +226,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nethermind.Shutter", "Nethe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nethermind.Shutter.Test", "Nethermind.Shutter.Test\Nethermind.Shutter.Test.csproj", "{CEA1C413-A96C-4339-AC1C-839B603DECC8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nethermind.Serialization.FluentRlp.Test", "Nethermind.Serialization.FluentRlp.Test\Nethermind.Serialization.FluentRlp.Test.csproj", "{C8A91B54-F9CA-4211-BE16-F7A0B38223EB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nethermind.Serialization.FluentRlp", "Nethermind.Serialization.FluentRlp\Nethermind.Serialization.FluentRlp.csproj", "{3D358FD8-E047-4770-BCDF-8FBBBBDDE03A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nethermind.Serialization.FluentRlp.Generator", "Nethermind.Serialization.FluentRlp.Generator\Nethermind.Serialization.FluentRlp.Generator.csproj", "{838687B6-9915-4BD2-98CC-79A4130B89B0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Nethermind.Serialization.Rlp.Benchmark", "Nethermind.Serialization.Rlp.Benchmark\Nethermind.Serialization.Rlp.Benchmark.csproj", "{2D355B4C-AE53-4F93-9E3B-91C027CC5075}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -624,6 +632,22 @@ Global {CEA1C413-A96C-4339-AC1C-839B603DECC8}.Debug|Any CPU.Build.0 = Debug|Any CPU {CEA1C413-A96C-4339-AC1C-839B603DECC8}.Release|Any CPU.ActiveCfg = Release|Any CPU {CEA1C413-A96C-4339-AC1C-839B603DECC8}.Release|Any CPU.Build.0 = Release|Any CPU + {C8A91B54-F9CA-4211-BE16-F7A0B38223EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8A91B54-F9CA-4211-BE16-F7A0B38223EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8A91B54-F9CA-4211-BE16-F7A0B38223EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8A91B54-F9CA-4211-BE16-F7A0B38223EB}.Release|Any CPU.Build.0 = Release|Any CPU + {3D358FD8-E047-4770-BCDF-8FBBBBDDE03A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D358FD8-E047-4770-BCDF-8FBBBBDDE03A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D358FD8-E047-4770-BCDF-8FBBBBDDE03A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D358FD8-E047-4770-BCDF-8FBBBBDDE03A}.Release|Any CPU.Build.0 = Release|Any CPU + {838687B6-9915-4BD2-98CC-79A4130B89B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {838687B6-9915-4BD2-98CC-79A4130B89B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {838687B6-9915-4BD2-98CC-79A4130B89B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {838687B6-9915-4BD2-98CC-79A4130B89B0}.Release|Any CPU.Build.0 = Release|Any CPU + {2D355B4C-AE53-4F93-9E3B-91C027CC5075}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D355B4C-AE53-4F93-9E3B-91C027CC5075}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D355B4C-AE53-4F93-9E3B-91C027CC5075}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D355B4C-AE53-4F93-9E3B-91C027CC5075}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -684,6 +708,7 @@ Global {E1E7BEFC-52C0-49ED-B0A7-CB8C3250D120} = {4019B82F-1104-4D2C-9F96-05FD7D3575E8} {89311B58-AF36-4956-883D-54531BC1D5A3} = {78BED57D-720E-4E6C-ABA2-397B73B494F9} {6528010D-7DCE-4935-9785-5270FF515F3E} = {89311B58-AF36-4956-883D-54531BC1D5A3} + {C8A91B54-F9CA-4211-BE16-F7A0B38223EB} = {4019B82F-1104-4D2C-9F96-05FD7D3575E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {092CA5E3-6180-4ED7-A3CB-9B57FAC2AA85}