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