Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merkle nodes with even-length paths made shorter #354

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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} }}";
}
}
}
Loading