Skip to content

Commit

Permalink
Merkle nodes with even-length paths starting with 0b11 will be shorte…
Browse files Browse the repository at this point in the history
…r now
  • Loading branch information
Scooletz committed Jun 13, 2024
1 parent 17e2704 commit ffd8c06
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 10 deletions.
10 changes: 7 additions & 3 deletions src/Paprika.Tests/Merkle/NodeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
58 changes: 51 additions & 7 deletions src/Paprika/Merkle/Node.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -130,7 +130,8 @@ public Type NodeType
/// The part ((_header & HighestBit) >> 1)) allows for node types with the highest bit set
/// <see cref="Type.Leaf"/> to have 7 bits of metadata.
/// </summary>
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)
{
Expand Down Expand Up @@ -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;

/// <summary>
/// This is a special case where a <see cref="NibblePath"/> 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.
/// </summary>
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;
Expand All @@ -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<byte> WriteTo(Span<byte> output)
{
var leftover = WriteToWithLeftover(output);
Expand All @@ -222,6 +254,13 @@ public Span<byte> WriteTo(Span<byte> output)

public Span<byte> WriteToWithLeftover(Span<byte> 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)
Expand All @@ -243,7 +282,12 @@ public static ReadOnlySpan<byte> ReadFrom(ReadOnlySpan<byte> 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);
Expand Down Expand Up @@ -428,4 +472,4 @@ public override string ToString() =>
$"{nameof(Header)}: {Header.ToString()}, " +
$"{nameof(Children)}: {Children} }}";
}
}
}

0 comments on commit ffd8c06

Please sign in to comment.