From ffd8c068438977dc4d64d0c0dee2eeb11b7dcca8 Mon Sep 17 00:00:00 2001 From: scooletz Date: Thu, 13 Jun 2024 11:38:07 +0200 Subject: [PATCH] Merkle nodes with even-length paths starting with 0b11 will be shorter now --- src/Paprika.Tests/Merkle/NodeTests.cs | 10 +++-- src/Paprika/Merkle/Node.cs | 58 +++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/Paprika.Tests/Merkle/NodeTests.cs b/src/Paprika.Tests/Merkle/NodeTests.cs index aadbc232..dd9fa95d 100644 --- a/src/Paprika.Tests/Merkle/NodeTests.cs +++ b/src/Paprika.Tests/Merkle/NodeTests.cs @@ -176,9 +176,13 @@ public void Leaf_read_write(byte[] pathBytes, Keccak keccak) decoded.Equals(leaf).Should().BeTrue($"Expected {leaf.ToString()}, got {decoded.ToString()}"); } - [TestCase(new byte[] { 253, 137 }, 0, 2, 2)] - [TestCase(new byte[] { 253, 137 }, 1, 1, 1)] - [TestCase(new byte[] { 253, 137 }, 1, 3, 2)] + [TestCase(new byte[] { 129, 137 }, 0, 2, 2)] + [TestCase(new byte[] { 129, 137 }, 1, 1, 1)] + [TestCase(new byte[] { 129, 137 }, 1, 3, 2)] + [TestCase(new byte[] { 0b1100_0000, 137 }, 0, 4, 2)] + [TestCase(new byte[] { 0b1100_0001, 137 }, 0, 4, 2, TestName = "Even path - special 1st nibble (1)")] + [TestCase(new byte[] { 0b1100_1001, 137 }, 0, 4, 2, TestName = "Even path - special 1st nibble (2)")] + [TestCase(new byte[] { 0b1101_0001, 137 }, 0, 4, 2, TestName = "Even path - special 1st nibble (3)")] public void Leaf_paths(byte[] raw, int odd, int length, int expectedLength) { var path = NibblePath.FromKey(raw).SliceFrom(odd).SliceTo(length); diff --git a/src/Paprika/Merkle/Node.cs b/src/Paprika/Merkle/Node.cs index 80ed3aa8..cdac045e 100644 --- a/src/Paprika/Merkle/Node.cs +++ b/src/Paprika/Merkle/Node.cs @@ -105,7 +105,7 @@ public readonly struct Header { public const int Size = sizeof(byte); - private const byte HighestBit = 0b1000_0000; + public const byte HighestBit = 0b1000_0000; private const byte NodeTypeMask = 0b1100_0000; private const int NodeTypeMaskShift = 6; @@ -130,7 +130,8 @@ public Type NodeType /// The part ((_header & HighestBit) >> 1)) allows for node types with the highest bit set /// to have 7 bits of metadata. /// - public byte Metadata => (byte)((((_header & MetadataMask) | ((_header & HighestBit) >> 1)) & _header) >> MetadataMaskShift); + public byte Metadata => + (byte)((((_header & MetadataMask) | ((_header & HighestBit) >> 1)) & _header) >> MetadataMaskShift); public Header(Type nodeType, byte metadata = 0b0000) { @@ -189,7 +190,19 @@ public readonly ref partial struct Leaf public int MaxByteLength => Header.Size + Path.RawSpan.Length - Path.Oddity; private const byte OddPathMetadata = 0b0001_0000; - public const int MinimalLeafPathLength = 1; + + /// + /// This is a special case where a is: + /// - even + /// - starts with 0b11XX nibble + /// It allows to write the path as is, and treat it as both, + /// the leaf and directly as the nibble path. + /// + private const byte EvenPathMetadata = 0b0100_0000; + + private const byte EvenPathFirstNibbleMask = EvenPathMetadata | Header.HighestBit; + + private const int MinimalLeafPathLength = 1; public readonly Header Header; public readonly NibblePath Path; @@ -208,12 +221,31 @@ public Leaf(NibblePath path) Assert((path.Oddity + path.Length) % 2 == 0, "If path is odd, length should be odd as well. If even, even"); - var metadata = path.IsOdd ? (byte)(OddPathMetadata | path.FirstNibble) : 0; - Header = new Header(Type.Leaf, (byte)metadata); + int metadata; + if (path.IsOdd) + { + metadata = (byte)(OddPathMetadata | path.FirstNibble); + } + // even length + else if (IsEvenPathCompressible(path)) + { + // special case where first nibble is in form of 0x11XX, which allows to encode 2 first nibbles a special way + metadata = path.FirstNibble << NibblePath.NibbleShift | path.GetAt(1); + } + else + { + // the path is even, but it does not start with + metadata = 0; + } + Header = new Header(Type.Leaf, (byte)metadata); Path = path; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsEvenPathCompressible(in NibblePath path) => + (path.RawSpan[0] & EvenPathFirstNibbleMask) == EvenPathFirstNibbleMask; + public Span WriteTo(Span output) { var leftover = WriteToWithLeftover(output); @@ -222,6 +254,13 @@ public Span WriteTo(Span output) public Span WriteToWithLeftover(Span output) { + // Special case of even, compressible part + if (Path.IsOdd == false && IsEvenPathCompressible(Path)) + { + Path.RawSpan.CopyTo(output); + return output[Path.RawSpan.Length..]; + } + var leftover = Header.WriteToWithLeftover(output); var span = Path.RawSpan; if (Path.IsOdd) @@ -243,7 +282,12 @@ public static ReadOnlySpan ReadFrom(ReadOnlySpan source, out Leaf le var leftover = Header.ReadFrom(source, out var header); NibblePath path; - if ((header.Metadata & OddPathMetadata) == OddPathMetadata) + if ((source[0] & EvenPathFirstNibbleMask) == EvenPathFirstNibbleMask) + { + // Even, special case + path = NibblePath.FromKey(source, 0); + } + else if ((header.Metadata & OddPathMetadata) == OddPathMetadata) { // Construct path by wrapping the source and slicing by one to move to first nibble. path = NibblePath.FromKey(source, 1); @@ -428,4 +472,4 @@ public override string ToString() => $"{nameof(Header)}: {Header.ToString()}, " + $"{nameof(Children)}: {Children} }}"; } -} +} \ No newline at end of file