From 23e063144028ea3e711b32c02631bcaef100e346 Mon Sep 17 00:00:00 2001 From: Diptanshu Kakwani Date: Thu, 26 Dec 2024 11:55:34 +0530 Subject: [PATCH 01/15] initial commit --- src/Paprika.Tests/Merkle/RlpMemoTests.cs | 17 +- src/Paprika/Merkle/CommitExtensions.cs | 1 + src/Paprika/Merkle/ComputeMerkleBehavior.cs | 96 ++++++----- src/Paprika/Merkle/RlpMemo.cs | 168 ++++++++++++++++++-- 4 files changed, 210 insertions(+), 72 deletions(-) diff --git a/src/Paprika.Tests/Merkle/RlpMemoTests.cs b/src/Paprika.Tests/Merkle/RlpMemoTests.cs index 567f5985..64ff1d35 100644 --- a/src/Paprika.Tests/Merkle/RlpMemoTests.cs +++ b/src/Paprika.Tests/Merkle/RlpMemoTests.cs @@ -5,6 +5,7 @@ namespace Paprika.Tests.Merkle; +[Ignore("Needs refactoring as RLPMemo compression/decompression is removed")] public class RlpMemoTests { public const bool OddKey = true; @@ -64,8 +65,8 @@ public void All_children_set_with_two_zeros(int zero0, int zero1) // clear zeroes var memo = new RlpMemo(raw); - memo.Clear((byte)zero0); - memo.Clear((byte)zero1); + memo.Clear((byte)zero0, NibbleSet.Readonly.All); + memo.Clear((byte)zero1, NibbleSet.Readonly.All); Run(raw, RlpMemo.Size - 2 * Keccak.Size + NibbleSet.MaxByteSize, NibbleSet.Readonly.All, OddKey); } @@ -123,17 +124,5 @@ private static void Run(Span memoRaw, int compressedSize, NibbleSet.Readon Span writeTo = stackalloc byte[compressedSize]; var written = RlpMemo.Compress(key, memo.Raw, children, writeTo); written.Should().Be(compressedSize); - - if (!isOdd && children.SetCount == 2) - { - // cleanup - var decompressed = RlpMemo.Decompress(writeTo, children, stackalloc byte[RlpMemo.Size]); - decompressed.Raw.SequenceEqual(RlpMemo.Empty).Should().BeTrue(); - } - else - { - var decompressed = RlpMemo.Decompress(writeTo, children, stackalloc byte[RlpMemo.Size]); - decompressed.Raw.SequenceEqual(memoRaw).Should().BeTrue(); - } } } \ No newline at end of file diff --git a/src/Paprika/Merkle/CommitExtensions.cs b/src/Paprika/Merkle/CommitExtensions.cs index 61a5d3eb..9cee5e0a 100644 --- a/src/Paprika/Merkle/CommitExtensions.cs +++ b/src/Paprika/Merkle/CommitExtensions.cs @@ -42,6 +42,7 @@ public static void SetBranch(this ICommit commit, in Key key, NibbleSet.Readonly EntryType type = EntryType.Persistent) { var branch = new Branch(children); + Debug.Assert(rlp.Length == 0 || rlp.Length == (children.SetCount * Keccak.Size)); commit.Set(key, branch.WriteTo(stackalloc byte[Branch.MaxByteLength]), rlp, type); } diff --git a/src/Paprika/Merkle/ComputeMerkleBehavior.cs b/src/Paprika/Merkle/ComputeMerkleBehavior.cs index 82598a4e..53efa694 100644 --- a/src/Paprika/Merkle/ComputeMerkleBehavior.cs +++ b/src/Paprika/Merkle/ComputeMerkleBehavior.cs @@ -406,15 +406,12 @@ public ReadOnlySpan InspectBeforeApply(in Key key, ReadOnlySpan data return data; } - Debug.Assert(memoizedRlp.Length == RlpMemo.Size); - - // There are RLPs here, compress them - var dataLength = data.Length - RlpMemo.Size; + // Copy the memoized RLPs + var dataLength = data.Length - memoizedRlp.Length; data[..dataLength].CopyTo(workingSet); + memoizedRlp.CopyTo(workingSet[dataLength..]); - var compressedLength = RlpMemo.Compress(key, memoizedRlp, branch.Children, workingSet[dataLength..]); - - return workingSet[..(dataLength + compressedLength)]; + return workingSet[..data.Length]; } public Keccak RootHash { get; private set; } @@ -597,9 +594,9 @@ private void EncodeBranch(scoped in Key key, scoped in ComputeContext ctx, scope if (memoize) { - var childRlpRequiresUpdate = isOwnedByThisCommit == false || previousRlp.Length != RlpMemo.Size; + var childRlpRequiresUpdate = isOwnedByThisCommit == false; memo = childRlpRequiresUpdate - ? RlpMemo.Decompress(previousRlp, branch.Children, rlpMemoization) + ? RlpMemo.Copy(previousRlp, rlpMemoization) : new RlpMemo(MakeRlpWritable(previousRlp)); } @@ -619,7 +616,7 @@ private void EncodeBranch(scoped in Key key, scoped in ComputeContext ctx, scope { if (branch.Children[i]) { - if (memoize && memo.TryGetKeccak(i, out var keccak)) + if (memoize && memo.TryGetKeccak(i, out var keccak, branch.Children)) { // keccak from cache stream.Encode(keccak); @@ -651,16 +648,24 @@ private void EncodeBranch(scoped in Key key, scoped in ComputeContext ctx, scope if (memoize) { memoizedUpdated = true; - memo.Set(keccakOrRlp, i); + + if (memo.Exists(i, branch.Children)) + { + memo.Set(keccakOrRlp, i, branch.Children); + } + else if (keccakOrRlp.DataType == KeccakOrRlp.Type.Keccak) + { + memo = RlpMemo.Insert(memo, i, branch.Children, keccakOrRlp.Span, rlpMemoization); + } } } else { stream.EncodeEmptyArray(); - if (memoize) + if (memoize && memo.TryGetKeccak(i, out var keccak, branch.Children)) { - memo.Clear(i); + memo = RlpMemo.Delete(memo, i, branch.Children, rlpMemoization); memoizedUpdated = true; } } @@ -732,12 +737,12 @@ private void EncodeBranch(scoped in Key key, scoped in ComputeContext ctx, scope if (value.Length == Keccak.Size) { memoizedUpdated = true; - memo.SetRaw(value, i); + memo.SetRaw(value, i, branch.Children); } else { memoizedUpdated = true; - memo.Clear(i); + memo.Clear(i, branch.Children); } } } @@ -759,7 +764,7 @@ private void EncodeBranch(scoped in Key key, scoped in ComputeContext ctx, scope if (memoize && !isOwnedByThisCommit && memoizedUpdated) { // - ctx.Commit.SetBranch(key, branch.Children, rlpMemoization, EntryType.Persistent); + ctx.Commit.SetBranch(key, branch.Children, memo.Raw, EntryType.Persistent); } KeccakOrRlp.FromSpan(rlp.Slice(from, end - from), out keccakOrRlp); @@ -952,6 +957,7 @@ private static DeleteStatus Delete(in NibblePath path, int at, ICommit commit, C case Node.Type.Branch: { var nibble = path[at]; + Debug.Assert(leftover.Length == 0 || leftover.Length == (branch.Children.SetCount * Keccak.Size)); if (!branch.Children[nibble]) { // no such child @@ -1040,22 +1046,31 @@ or DeleteStatus.ExtensionToLeaf static void UpdateBranchOnDelete(ICommit commit, in Node.Branch branch, NibbleSet.Readonly children, ReadOnlySpan leftover, ReadOnlySpanOwnerWithMetadata owner, byte nibble, in Key key) { - var childRlpRequiresUpdate = owner.IsOwnedBy(commit) == false || leftover.Length != RlpMemo.Size; + var childRlpRequiresUpdate = owner.IsOwnedBy(commit) == false; RlpMemo memo; byte[]? rlpWorkingSet = null; + byte[]? deleteWorkingSet = null; if (childRlpRequiresUpdate) { // TODO: make it a context and pass through all the layers - rlpWorkingSet = ArrayPool.Shared.Rent(RlpMemo.Size); - memo = RlpMemo.Decompress(leftover, branch.Children, rlpWorkingSet.AsSpan()); + rlpWorkingSet = ArrayPool.Shared.Rent(leftover.Length); + memo = RlpMemo.Copy(leftover, rlpWorkingSet.AsSpan()); } else { memo = new RlpMemo(MakeRlpWritable(leftover)); } - memo.Clear(nibble); + if (memo.Exists(nibble, children)) + { + memo.Clear(nibble, children); + } + else if (memo.Length > 0) + { + deleteWorkingSet = ArrayPool.Shared.Rent(leftover.Length - Keccak.Size); + memo = RlpMemo.Delete(memo, nibble, children, deleteWorkingSet); + } var shouldUpdate = !branch.Children.Equals(children); @@ -1069,6 +1084,11 @@ static void UpdateBranchOnDelete(ICommit commit, in Node.Branch branch, NibbleSe { ArrayPool.Shared.Return(rlpWorkingSet); } + + if (deleteWorkingSet != null) + { + ArrayPool.Shared.Return(deleteWorkingSet); + } } static DeleteStatus ThrowUnknownType() @@ -1288,20 +1308,28 @@ private static void MarkPathDirty(in NibblePath path, in Span rlpMemoWorki { var nibble = path[i]; - var childRlpRequiresUpdate = owner.IsOwnedBy(commit) == false || leftover.Length != RlpMemo.Size; + var childRlpRequiresUpdate = owner.IsOwnedBy(commit) == false; var memo = childRlpRequiresUpdate - ? RlpMemo.Decompress(leftover, branch.Children, rlpMemoWorkingSet) + ? RlpMemo.Copy(leftover, rlpMemoWorkingSet) : new RlpMemo(MakeRlpWritable(leftover)); - memo.Clear(nibble); - createLeaf = !branch.Children[nibble]; var children = branch.Children.Set(nibble); var shouldUpdateBranch = createLeaf; + // If this path does not exist, insert an empty Keccak. Otherwise, clear it in the memo. + if (createLeaf) + { + memo = RlpMemo.Insert(memo, nibble, children, Keccak.Zero.Span, rlpMemoWorkingSet); + } + else + { + memo.Clear(nibble, children); + } + if (shouldUpdateBranch || childRlpRequiresUpdate) { - // Set the branch if either the children has hanged or the RLP requires the update + // Set the branch if either the children has changed or the RLP requires the update commit.SetBranch(key, children, memo.Raw); } @@ -1655,23 +1683,7 @@ private static ReadOnlySpan Transform(in ReadOnlySpan data, in Span< // Branch should be always persistent, already copied and ready to work with. type = EntryType.Persistent; - - var leftoverLength = Node.Branch.ReadFrom(data, out var branch).Length; - if (leftoverLength == RlpMemo.Size) - { - // Rlp memo is decompressed, good to be stored as is. - return data; - } - - // RlpMemo not decompressed. - - // Write branch first - var leftover = branch.WriteToWithLeftover(workspace); - - // Decompress to the leftover - RlpMemo.Decompress(leftover, branch.Children, leftover); - - return workspace[..(workspace.Length - leftover.Length + RlpMemo.Size)]; + return data; } [SkipLocalsInit] diff --git a/src/Paprika/Merkle/RlpMemo.cs b/src/Paprika/Merkle/RlpMemo.cs index 96a1dc3c..40cd069f 100644 --- a/src/Paprika/Merkle/RlpMemo.cs +++ b/src/Paprika/Merkle/RlpMemo.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Numerics; using System.Runtime.CompilerServices; using Paprika.Crypto; using Paprika.Data; @@ -9,7 +10,9 @@ namespace Paprika.Merkle; public readonly ref struct RlpMemo { - public static readonly byte[] Empty = new byte[Size]; + public static readonly byte[] FullyEmpty = new byte[Size]; + + public static readonly byte[] Empty = []; private readonly Span _buffer; @@ -17,22 +20,32 @@ public readonly ref struct RlpMemo public RlpMemo(Span buffer) { - Debug.Assert(buffer.Length == Size); - _buffer = buffer; } + public RlpMemo(int count) + { + _buffer = new byte[count * Keccak.Size]; + } + public ReadOnlySpan Raw => _buffer; - public void SetRaw(ReadOnlySpan keccak, byte nibble) + public int Length => _buffer.Length; + + public void SetRaw(ReadOnlySpan keccak, byte nibble, NibbleSet.Readonly children) { - Debug.Assert(keccak.Length == Keccak.Size); - keccak.CopyTo(GetAtNibble(nibble)); + var span = GetAtNibble(nibble, children); + Debug.Assert(span != null); + Debug.Assert(_buffer.Length == (children.SetCount * Keccak.Size)); + + keccak.CopyTo(span); } - public void Set(in KeccakOrRlp keccakOrRlp, byte nibble) + public void Set(in KeccakOrRlp keccakOrRlp, byte nibble, NibbleSet.Readonly children) { - var span = GetAtNibble(nibble); + var span = GetAtNibble(nibble, children); + Debug.Assert(span != null); + Debug.Assert(_buffer.Length == (children.SetCount * Keccak.Size)); if (keccakOrRlp.DataType == KeccakOrRlp.Type.Keccak) { @@ -45,16 +58,23 @@ public void Set(in KeccakOrRlp keccakOrRlp, byte nibble) } } - public void Clear(byte nibble) + public void Clear(byte nibble, NibbleSet.Readonly children) { - GetAtNibble(nibble).Clear(); + var span = GetAtNibble(nibble, children); + Debug.Assert(_buffer.Length == 0 || _buffer.Length == (children.SetCount * Keccak.Size)); + + if (span != null) + { + span.Clear(); + } } - public bool TryGetKeccak(byte nibble, out ReadOnlySpan keccak) + public bool TryGetKeccak(byte nibble, out ReadOnlySpan keccak, NibbleSet.Readonly children) { - var span = GetAtNibble(nibble); + var span = GetAtNibble(nibble, children); + Debug.Assert(_buffer.Length == 0 || _buffer.Length == (children.SetCount * Keccak.Size)); - if (span.IndexOfAnyExcept((byte)0) >= 0) + if (span != null && span.IndexOfAnyExcept((byte)0) >= 0) { keccak = span; return true; @@ -64,7 +84,123 @@ public bool TryGetKeccak(byte nibble, out ReadOnlySpan keccak) return false; } - private Span GetAtNibble(byte nibble) => _buffer.Slice(nibble * Keccak.Size, Keccak.Size); + public bool Exists(byte nibble, NibbleSet.Readonly children) + { + var leftChildren = (ushort)((ushort)children & ((1U << (nibble + 1)) - 1)); + + // Check if the element exists + if (_buffer.Length == 0 || (leftChildren & (1U << nibble)) == 0) + { + return false; + } + + return true; + } + + private Span GetAtNibble(byte nibble, NibbleSet.Readonly children) + { + var leftChildren = (ushort)((ushort)children & ((1U << (nibble + 1)) - 1)); + + // Check if the element exists + if (_buffer.Length == 0 || (leftChildren & (1U << nibble)) == 0) + { + return null; + } + + var index = BitOperations.PopCount(leftChildren) - 1; + return _buffer.Slice(index * Keccak.Size, Keccak.Size); + } + + public static RlpMemo Copy(ReadOnlySpan from, scoped in Span to) + { + var span = to[..from.Length]; + from.CopyTo(span); + return new RlpMemo(span); + } + + public static RlpMemo Insert(RlpMemo memo, byte nibble, NibbleSet.Readonly children, + ReadOnlySpan keccak, scoped in Span workingSet) + { + // Compute the destination size for copying. + var size = children.SetCount * Keccak.Size; + + Debug.Assert(workingSet.Length >= size); + var span = workingSet[..size]; + + // Ensure that this element already exists in the list of children + var leftChildren = (ushort)((ushort)children & ((1U << (nibble + 1)) - 1)); + Debug.Assert((leftChildren & (1U << nibble)) != 0); + + // Find the index of this nibble in the memo + var insertIndex = BitOperations.PopCount(leftChildren) - 1; + var insertOffset = insertIndex * Keccak.Size; + + if (memo.Length != 0) + { + var remainingBytes = (children.SetCount - insertIndex - 1) * Keccak.Size; + + // Copy all the elements after the new element + if (remainingBytes > 0) + { + memo._buffer.Slice(insertOffset, remainingBytes) + .CopyTo(span.Slice(insertOffset + Keccak.Size)); + } + + // Copy elements before the new element + if (insertOffset > 0) + { + memo._buffer.Slice(0, insertOffset).CopyTo(span); + } + } + else + { + // Insert empty keccak for all the existing children + span.Clear(); + } + + keccak.CopyTo(span.Slice(insertOffset)); + + return new RlpMemo(span); + } + + public static RlpMemo Delete(RlpMemo memo, byte nibble, NibbleSet.Readonly children, + scoped in Span workingSet) + { + // Compute the destination size for copying. + var size = memo.Length - Keccak.Size; + if (size < 0) + { + // Memo is already empty, nothing to delete. + size = 0; + return new RlpMemo(workingSet[..size]); + } + + var span = workingSet[..size]; + + // Ensure that this element does not exist in the list of children + var leftChildren = (ushort)((ushort)children & ((1U << (nibble + 1)) - 1)); + Debug.Assert((leftChildren & (1U << nibble)) == 0); + + // Find the index of this nibble in the memo + var deleteIndex = BitOperations.PopCount(leftChildren); + var deleteOffset = deleteIndex * Keccak.Size; + + // Copy elements after the deleted element + int remainingBytes = (children.SetCount - deleteIndex) * Keccak.Size; + if (remainingBytes > 0) + { + memo._buffer.Slice(deleteOffset + Keccak.Size, remainingBytes) + .CopyTo(span.Slice(deleteOffset)); + } + + // Copy elements before the deleted element + if (deleteOffset > 0) + { + memo._buffer.Slice(0, deleteOffset).CopyTo(span); + } + + return new RlpMemo(span); + } public static RlpMemo Decompress(scoped in ReadOnlySpan leftover, NibbleSet.Readonly children, scoped in Span workingSet) @@ -116,7 +252,7 @@ public static RlpMemo Decompress(scoped in ReadOnlySpan leftover, NibbleSe var keccak = leftover.Slice(at * Keccak.Size, Keccak.Size); at++; - memo.SetRaw(keccak, i); + memo.SetRaw(keccak, i, children); } } @@ -147,7 +283,7 @@ public static int Compress(in Key key, scoped in ReadOnlySpan memoizedRlp, { if (children[i]) { - if (memo.TryGetKeccak(i, out var keccak)) + if (memo.TryGetKeccak(i, out var keccak, children)) { var dest = writeTo.Slice(at * Keccak.Size, Keccak.Size); at++; From 5d67a57fdfe4adc5116d9d7afcb3438ce84f735a Mon Sep 17 00:00:00 2001 From: Diptanshu Kakwani Date: Fri, 27 Dec 2024 17:27:45 +0530 Subject: [PATCH 02/15] modify/add tests --- src/Paprika.Tests/Chain/PrefetchingTests.cs | 61 ----- src/Paprika.Tests/Merkle/RlpMemoTests.cs | 272 ++++++++++++++------ src/Paprika/Merkle/ComputeMerkleBehavior.cs | 5 +- src/Paprika/Merkle/RlpMemo.cs | 157 +---------- 4 files changed, 209 insertions(+), 286 deletions(-) diff --git a/src/Paprika.Tests/Chain/PrefetchingTests.cs b/src/Paprika.Tests/Chain/PrefetchingTests.cs index 6a24aa33..f5f9983e 100644 --- a/src/Paprika.Tests/Chain/PrefetchingTests.cs +++ b/src/Paprika.Tests/Chain/PrefetchingTests.cs @@ -71,67 +71,6 @@ public async Task Prefetches_properly_on_not_changed_structure() } } - [Test] - public async Task Makes_all_decompression_on_prefetch() - { - using var db = PagedDb.NativeMemoryDb(8 * 1024 * 1024, 2); - var merkle = new ComputeMerkleBehavior(ComputeMerkleBehavior.ParallelismNone); - await using var blockchain = new Blockchain(db, merkle); - - // Create one block with some values, commit it and finalize - var hash = Keccak.EmptyTreeHash; - - hash = BuildBlock(blockchain, hash, 1); - blockchain.Finalize(hash); - await blockchain.WaitTillFlush(hash); - - hash = BuildBlock(blockchain, hash, 2); - - return; - - static Keccak BuildBlock(Blockchain blockchain, Keccak parent, uint number) - { - var isFirst = number == 1; - - byte[] value = isFirst ? [17] : [23]; - - const int seed = 13; - const int contracts = 10; - const int slots = 10; - - using var block = blockchain.StartNew(parent); - var random = new Random(seed); - - // Open prefetcher on blocks beyond first - var prefetcher = isFirst == false ? block.OpenPrefetcher() : null; - - for (var i = 0; i < contracts; i++) - { - var contract = random.NextKeccak(); - prefetcher?.PrefetchAccount(contract); - - if (isFirst) - { - block.SetAccount(contract, new Account(1, 1, Keccak.Zero, Keccak.Zero)); - } - - for (var j = 0; j < slots; j++) - { - var storage = random.NextKeccak(); - prefetcher?.PrefetchStorage(contract, storage); - block.SetStorage(contract, storage, value); - } - } - - prefetcher?.SpinTillPrefetchDone(); - - using (RlpMemo.NoDecompression()) - { - return block.Commit(number); - } - } - } - private static void Set(Keccak[] accounts, uint account, IWorldState start, UInt256 bigNonce) { ref var k = ref accounts[account]; diff --git a/src/Paprika.Tests/Merkle/RlpMemoTests.cs b/src/Paprika.Tests/Merkle/RlpMemoTests.cs index 64ff1d35..31bd5426 100644 --- a/src/Paprika.Tests/Merkle/RlpMemoTests.cs +++ b/src/Paprika.Tests/Merkle/RlpMemoTests.cs @@ -1,128 +1,254 @@ -using FluentAssertions; +using FluentAssertions; using Paprika.Crypto; using Paprika.Data; using Paprika.Merkle; +using Paprika.RLP; namespace Paprika.Tests.Merkle; -[Ignore("Needs refactoring as RLPMemo compression/decompression is removed")] public class RlpMemoTests { - public const bool OddKey = true; - public const bool EvenKey = false; - [Test] - public void All_children_set_with_all_Keccaks_empty() + // All the write operations on RlpMemo + private enum RlpMemoOperation { - Span raw = stackalloc byte[RlpMemo.Size]; - - Run(raw, 0, NibbleSet.Readonly.All, OddKey); + Set, + SetRaw, + Clear, + Delete, + Insert } [Test] - public void All_children_set_with_all_Keccaks_set() + public void Random_delete() { - Span raw = stackalloc byte[RlpMemo.Size]; + Span raw = stackalloc byte[RlpMemo.MaxSize]; + var children = new NibbleSet(); - for (var i = 0; i < RlpMemo.Size; i++) + for (var i = 0; i < RlpMemo.MaxSize; i++) { raw[i] = (byte)(i & 0xFF); } - Run(raw, RlpMemo.Size, NibbleSet.Readonly.All, OddKey); + for (var i = 0; i < NibbleSet.NibbleCount; i++) + { + children[(byte)i] = true; + } + + var memo = new RlpMemo(raw); + var rand = new Random(13); + + for (var i = 0; i < NibbleSet.NibbleCount; i++) + { + var child = (byte)rand.Next(NibbleSet.NibbleCount); + + while (children[child] == false) + { + child = (byte)rand.Next(NibbleSet.NibbleCount); + } + + children[child] = false; + memo = RlpMemo.Delete(memo, child, children, raw); + + memo.Length.Should().Be(RlpMemo.MaxSize - (i + 1) * Keccak.Size); + memo.Exists(child, children).Should().BeFalse(); + memo.TryGetKeccak(child, out var keccak, children).Should().BeFalse(); + keccak.IsEmpty.Should().BeTrue(); + } + + memo.Length.Should().Be(0); } - [TestCase(0)] - [TestCase(1)] - [TestCase(3)] - [TestCase(11)] - [TestCase(NibbleSet.NibbleCount - 1)] - public void All_children_set_with_one_zero(int zero) + [Test] + public void Random_insert() { - Span raw = stackalloc byte[RlpMemo.Size]; + Span raw = []; + Span workingMemory = new byte[RlpMemo.MaxSize]; + var children = new NibbleSet(); + + Span keccak = new byte[Keccak.Size]; + keccak.Fill(0xFF); - for (var i = 0; i < RlpMemo.Size; i++) + for (var i = 0; i < NibbleSet.NibbleCount; i++) { - raw[i] = (byte)(i & 0xFF); + children[(byte)i] = false; } - // zero one - raw.Slice(zero * Keccak.Size, Keccak.Size).Clear(); + var memo = new RlpMemo(raw); + var rand = new Random(13); + + for (var i = 0; i < NibbleSet.NibbleCount; i++) + { + var child = (byte)rand.Next(NibbleSet.NibbleCount); + + while (children[child]) + { + child = (byte)rand.Next(NibbleSet.NibbleCount); + } - Run(raw, RlpMemo.Size - Keccak.Size + NibbleSet.MaxByteSize, NibbleSet.Readonly.All, OddKey); + children[child] = true; + memo = RlpMemo.Insert(memo, child, children, keccak, workingMemory); + + memo.Length.Should().Be((i + 1) * Keccak.Size); + memo.Exists(child, children).Should().BeTrue(); + memo.TryGetKeccak(child, out var k, children).Should().BeTrue(); + k.SequenceEqual(keccak).Should().BeTrue(); + } + + memo.Length.Should().Be(RlpMemo.MaxSize); } - [TestCase(0, NibbleSet.NibbleCount - 1)] - [TestCase(1, 11)] - public void All_children_set_with_two_zeros(int zero0, int zero1) + [Test] + public void In_place_update() { - Span raw = stackalloc byte[RlpMemo.Size]; + Span raw = stackalloc byte[RlpMemo.MaxSize]; - for (var i = 0; i < RlpMemo.Size; i++) + for (var i = 0; i < RlpMemo.MaxSize; i++) { raw[i] = (byte)(i & 0xFF); } - // clear zeroes var memo = new RlpMemo(raw); - memo.Clear((byte)zero0, NibbleSet.Readonly.All); - memo.Clear((byte)zero1, NibbleSet.Readonly.All); - - Run(raw, RlpMemo.Size - 2 * Keccak.Size + NibbleSet.MaxByteSize, NibbleSet.Readonly.All, OddKey); - } - [TestCase(0, NibbleSet.NibbleCount - 1)] - [TestCase(1, 11)] - public void Two_children_set_with_Keccak(int child0, int child1) - { - Span raw = stackalloc byte[RlpMemo.Size]; + var children = new NibbleSet(); + for (var i = 0; i < NibbleSet.NibbleCount; i++) + { + children[(byte)i] = true; + } - // fill set - raw.Slice(child0 * Keccak.Size, Keccak.Size).Fill(1); - raw.Slice(child1 * Keccak.Size, Keccak.Size).Fill(17); + // Delete each child and the corresponding keccak + for (byte i = 0; i < NibbleSet.NibbleCount; i++) + { + children[i] = false; + memo = RlpMemo.Delete(memo, i, children, raw); - var children = new NibbleSet((byte)child0, (byte)child1); + memo.Length.Should().Be(RlpMemo.MaxSize - (i + 1) * Keccak.Size); + memo.Exists(i, children).Should().BeFalse(); + memo.TryGetKeccak(i, out var k, children).Should().BeFalse(); + k.IsEmpty.Should().BeTrue(); + } - // none of the children set is empty, keccaks encoded without the empty map - const int size = 2 * Keccak.Size; - Run(raw, size, children, OddKey); - } + memo.Length.Should().Be(0); - [TestCase(1, 11)] - public void Two_children_on_even_level_have_no_memo(int child0, int child1) - { - Span raw = stackalloc byte[RlpMemo.Size]; + // Try adding back the children and the corresponding keccak + Span keccak = stackalloc byte[Keccak.Size]; + keccak.Fill(0xFF); - // fill set - raw.Slice(child0 * Keccak.Size, Keccak.Size).Fill(1); - raw.Slice(child1 * Keccak.Size, Keccak.Size).Fill(17); + for (byte i = 0; i < NibbleSet.NibbleCount; i++) + { + children[i] = true; + memo = RlpMemo.Insert(memo, i, children, keccak, raw); - var children = new NibbleSet((byte)child0, (byte)child1); + memo.Length.Should().Be((i + 1) * Keccak.Size); + memo.Exists(i, children).Should().BeTrue(); + memo.TryGetKeccak(i, out var k, children).Should().BeTrue(); + k.SequenceEqual(keccak).Should().BeTrue(); + } - Run(raw, 0, children, EvenKey); + memo.Length.Should().Be(RlpMemo.MaxSize); } - [TestCase(0, NibbleSet.NibbleCount - 1)] - [TestCase(1, 11)] - public void Two_children_set_with_no_Keccak(int child0, int child1) + [Test] + public void Copy_data() { - Span raw = stackalloc byte[RlpMemo.Size]; + Span raw = stackalloc byte[RlpMemo.MaxSize]; + Span rawCopy = stackalloc byte[RlpMemo.MaxSize]; - var children = new NibbleSet((byte)child0, (byte)child1); + for (var i = 0; i < RlpMemo.MaxSize; i++) + { + raw[i] = (byte)(i & 0xFF); + } - // just empty map encoded - Run(raw, 0, children, OddKey); + var memo = new RlpMemo(raw); + memo = RlpMemo.Copy(memo.Raw, rawCopy); + memo.Raw.SequenceEqual(raw).Should().BeTrue(); } - private static void Run(Span memoRaw, int compressedSize, NibbleSet.Readonly children, bool oddKey) + [TestCase(1000)] + [TestCase(10_000)] + [TestCase(100_000)] + public void Large_random_operations(int numOperations) { - var isOdd = oddKey == OddKey; + Span raw = stackalloc byte[RlpMemo.MaxSize]; + var children = new NibbleSet(); + + Span keccak = new byte[Keccak.Size]; + keccak.Fill(0xFF); + KeccakOrRlp.FromSpan(keccak, out var keccakOrRlp); + + for (var i = 0; i < RlpMemo.MaxSize; i++) + { + raw[i] = (byte)(i & 0xFF); + } - var key = Key.Merkle(isOdd ? NibblePath.Single(1, 0) : NibblePath.Empty); - var memo = new RlpMemo(memoRaw); + for (var i = 0; i < NibbleSet.NibbleCount; i++) + { + children[(byte)i] = false; + } + + var memo = new RlpMemo(raw); + var rand = new Random(13); - Span writeTo = stackalloc byte[compressedSize]; - var written = RlpMemo.Compress(key, memo.Raw, children, writeTo); - written.Should().Be(compressedSize); + for (var i = 0; i < numOperations; i++) + { + var child = (byte)rand.Next(NibbleSet.NibbleCount); + var op = (RlpMemoOperation)rand.Next(Enum.GetValues().Length); + + switch (op) + { + case RlpMemoOperation.Set: + if (memo.Exists(child, children)) + { + memo.Set(keccakOrRlp, child, children); + + memo.TryGetKeccak(child, out var k, children).Should().BeTrue(); + k.SequenceEqual(keccakOrRlp.Span).Should().BeTrue(); + } + + break; + case RlpMemoOperation.SetRaw: + if (memo.Exists(child, children)) + { + memo.SetRaw(keccak, child, children); + + memo.TryGetKeccak(child, out var k, children).Should().BeTrue(); + k.SequenceEqual(keccak).Should().BeTrue(); + } + + break; + case RlpMemoOperation.Clear: + if (memo.Exists(child, children)) + { + memo.Clear(child, children); + + memo.TryGetKeccak(child, out var k, children).Should().BeFalse(); + k.IsEmpty.Should().BeTrue(); + } + + break; + case RlpMemoOperation.Delete: + if (memo.Exists(child, children)) + { + children[child] = false; + memo = RlpMemo.Delete(memo, child, children, raw); + + memo.TryGetKeccak(child, out var k, children).Should().BeFalse(); + k.IsEmpty.Should().BeTrue(); + } + + break; + case RlpMemoOperation.Insert: + if (!memo.Exists(child, children)) + { + children[child] = true; + memo = RlpMemo.Insert(memo, child, children, keccak, raw); + + memo.TryGetKeccak(child, out var k, children).Should().BeTrue(); + k.SequenceEqual(keccak).Should().BeTrue(); + } + + break; + } + } } -} \ No newline at end of file +} diff --git a/src/Paprika/Merkle/ComputeMerkleBehavior.cs b/src/Paprika/Merkle/ComputeMerkleBehavior.cs index 53efa694..b38c7683 100644 --- a/src/Paprika/Merkle/ComputeMerkleBehavior.cs +++ b/src/Paprika/Merkle/ComputeMerkleBehavior.cs @@ -587,7 +587,7 @@ private void EncodeBranch(scoped in Key key, scoped in ComputeContext ctx, scope const int rlpSlice = 1024; var rlp = buffer.Span[..rlpSlice]; - var rlpMemoization = buffer.Span.Slice(rlpSlice, RlpMemo.Size); + var rlpMemoization = buffer.Span.Slice(rlpSlice, RlpMemo.MaxSize); var memoizedUpdated = false; RlpMemo memo = default; @@ -610,7 +610,7 @@ private void EncodeBranch(scoped in Key key, scoped in ComputeContext ctx, scope if (!runInParallel) { - var childSpan = buffer.Span[(RlpMemo.Size + rlpSlice)..]; + var childSpan = buffer.Span[(RlpMemo.MaxSize + rlpSlice)..]; for (byte i = 0; i < NibbleSet.NibbleCount; i++) { @@ -1062,6 +1062,7 @@ static void UpdateBranchOnDelete(ICommit commit, in Node.Branch branch, NibbleSe memo = new RlpMemo(MakeRlpWritable(leftover)); } + // If this child still exists, only clear the memo. Otherwise, delete it from the memo. if (memo.Exists(nibble, children)) { memo.Clear(nibble, children); diff --git a/src/Paprika/Merkle/RlpMemo.cs b/src/Paprika/Merkle/RlpMemo.cs index 40cd069f..3bcd998f 100644 --- a/src/Paprika/Merkle/RlpMemo.cs +++ b/src/Paprika/Merkle/RlpMemo.cs @@ -10,24 +10,17 @@ namespace Paprika.Merkle; public readonly ref struct RlpMemo { - public static readonly byte[] FullyEmpty = new byte[Size]; - public static readonly byte[] Empty = []; private readonly Span _buffer; - public const int Size = NibbleSet.NibbleCount * Keccak.Size; + public const int MaxSize = NibbleSet.NibbleCount * Keccak.Size; public RlpMemo(Span buffer) { _buffer = buffer; } - public RlpMemo(int count) - { - _buffer = new byte[count * Keccak.Size]; - } - public ReadOnlySpan Raw => _buffer; public int Length => _buffer.Length; @@ -35,7 +28,7 @@ public RlpMemo(int count) public void SetRaw(ReadOnlySpan keccak, byte nibble, NibbleSet.Readonly children) { var span = GetAtNibble(nibble, children); - Debug.Assert(span != null); + Debug.Assert(!span.IsEmpty); Debug.Assert(_buffer.Length == (children.SetCount * Keccak.Size)); keccak.CopyTo(span); @@ -44,7 +37,7 @@ public void SetRaw(ReadOnlySpan keccak, byte nibble, NibbleSet.Readonly ch public void Set(in KeccakOrRlp keccakOrRlp, byte nibble, NibbleSet.Readonly children) { var span = GetAtNibble(nibble, children); - Debug.Assert(span != null); + Debug.Assert(!span.IsEmpty); Debug.Assert(_buffer.Length == (children.SetCount * Keccak.Size)); if (keccakOrRlp.DataType == KeccakOrRlp.Type.Keccak) @@ -63,7 +56,7 @@ public void Clear(byte nibble, NibbleSet.Readonly children) var span = GetAtNibble(nibble, children); Debug.Assert(_buffer.Length == 0 || _buffer.Length == (children.SetCount * Keccak.Size)); - if (span != null) + if (!span.IsEmpty) { span.Clear(); } @@ -74,7 +67,7 @@ public bool TryGetKeccak(byte nibble, out ReadOnlySpan keccak, NibbleSet.R var span = GetAtNibble(nibble, children); Debug.Assert(_buffer.Length == 0 || _buffer.Length == (children.SetCount * Keccak.Size)); - if (span != null && span.IndexOfAnyExcept((byte)0) >= 0) + if (span.IndexOfAnyExcept((byte)0) >= 0) { keccak = span; return true; @@ -86,10 +79,8 @@ public bool TryGetKeccak(byte nibble, out ReadOnlySpan keccak, NibbleSet.R public bool Exists(byte nibble, NibbleSet.Readonly children) { - var leftChildren = (ushort)((ushort)children & ((1U << (nibble + 1)) - 1)); - // Check if the element exists - if (_buffer.Length == 0 || (leftChildren & (1U << nibble)) == 0) + if (_buffer.Length == 0 || ((ushort)children & (1U << nibble)) == 0) { return false; } @@ -104,7 +95,7 @@ private Span GetAtNibble(byte nibble, NibbleSet.Readonly children) // Check if the element exists if (_buffer.Length == 0 || (leftChildren & (1U << nibble)) == 0) { - return null; + return []; } var index = BitOperations.PopCount(leftChildren) - 1; @@ -201,138 +192,4 @@ public static RlpMemo Delete(RlpMemo memo, byte nibble, NibbleSet.Readonly child return new RlpMemo(span); } - - public static RlpMemo Decompress(scoped in ReadOnlySpan leftover, NibbleSet.Readonly children, - scoped in Span workingSet) - { - if (_decompressionForbidden) - { - ThrowDecompressionForbidden(); - } - - var span = workingSet[..Size]; - - if (leftover.IsEmpty) - { - // no RLP cached yet - span.Clear(); - return new RlpMemo(span); - } - - if (leftover.Length == Size) - { - leftover.CopyTo(span); - return new RlpMemo(span); - } - - // It's neither empty nor full. It must be the compressed form, prepare setup first - span.Clear(); - var memo = new RlpMemo(span); - - // Extract empty bits if any - NibbleSet.Readonly empty; - - // The empty bytes length is anything that is not aligned to the Keccak size - var emptyBytesLength = leftover.Length % Keccak.Size; - if (emptyBytesLength > 0) - { - var bits = leftover[^emptyBytesLength..]; - NibbleSet.Readonly.ReadFrom(bits, out empty); - } - else - { - empty = default; - } - - var at = 0; - for (byte i = 0; i < NibbleSet.NibbleCount; i++) - { - if (children[i] && empty[i] == false) - { - var keccak = leftover.Slice(at * Keccak.Size, Keccak.Size); - at++; - - memo.SetRaw(keccak, i, children); - } - } - - return memo; - - [DoesNotReturn] - [MethodImpl(MethodImplOptions.NoInlining)] - static void ThrowDecompressionForbidden() => throw new InvalidOperationException("Decompression is forbidden."); - } - - [SkipLocalsInit] - public static int Compress(in Key key, scoped in ReadOnlySpan memoizedRlp, NibbleSet.Readonly children, scoped in Span writeTo) - { - // Optimization, omitting some of the branches to memoize. - // It omits only these with two children where the cost of the recompute is not big. - // To prevent an attack of spawning multiple levels of such branches, only even are skipped - if (children.SetCount == 2 && (key.Path.Length + key.StoragePath.Length) % 2 == 0) - { - return 0; - } - - var memo = new RlpMemo(ComputeMerkleBehavior.MakeRlpWritable(memoizedRlp)); - var at = 0; - - var empty = new NibbleSet(); - - for (byte i = 0; i < NibbleSet.NibbleCount; i++) - { - if (children[i]) - { - if (memo.TryGetKeccak(i, out var keccak, children)) - { - var dest = writeTo.Slice(at * Keccak.Size, Keccak.Size); - at++; - keccak.CopyTo(dest); - } - else - { - empty[i] = true; - } - } - } - - Debug.Assert(at != 16 || empty.SetCount == 0, "If at = 16, empty should be empty"); - - if (empty.SetCount == children.SetCount) - { - // None of children has their Keccak memoized. Instead of reporting it, return nothing written. - return 0; - } - - if (empty.SetCount > 0) - { - var dest = writeTo.Slice(at * Keccak.Size, NibbleSet.MaxByteSize); - new NibbleSet.Readonly(empty).WriteToWithLeftover(dest); - return at * Keccak.Size + NibbleSet.MaxByteSize; - } - - // Return only children that were written - return at * Keccak.Size; - } - - /// - /// Test only method to forbid decompression. - /// - /// - public static NoDecompressionScope NoDecompression() => new(); - - private static volatile bool _decompressionForbidden; - - public readonly struct NoDecompressionScope : IDisposable - { - public NoDecompressionScope() - { - _decompressionForbidden = true; - } - - public void Dispose() - { - _decompressionForbidden = false; - } - } } From 67797b23f5252cb8f65b65b9094867d2e96eedbd Mon Sep 17 00:00:00 2001 From: Diptanshu Kakwani Date: Mon, 30 Dec 2024 17:34:01 +0530 Subject: [PATCH 03/15] minor changes --- src/Paprika/Merkle/ComputeMerkleBehavior.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Paprika/Merkle/ComputeMerkleBehavior.cs b/src/Paprika/Merkle/ComputeMerkleBehavior.cs index b38c7683..d4badfea 100644 --- a/src/Paprika/Merkle/ComputeMerkleBehavior.cs +++ b/src/Paprika/Merkle/ComputeMerkleBehavior.cs @@ -663,7 +663,7 @@ private void EncodeBranch(scoped in Key key, scoped in ComputeContext ctx, scope { stream.EncodeEmptyArray(); - if (memoize && memo.TryGetKeccak(i, out var keccak, branch.Children)) + if (memoize && memo.Exists(i, branch.Children)) { memo = RlpMemo.Delete(memo, i, branch.Children, rlpMemoization); memoizedUpdated = true; @@ -1049,7 +1049,6 @@ static void UpdateBranchOnDelete(ICommit commit, in Node.Branch branch, NibbleSe var childRlpRequiresUpdate = owner.IsOwnedBy(commit) == false; RlpMemo memo; byte[]? rlpWorkingSet = null; - byte[]? deleteWorkingSet = null; if (childRlpRequiresUpdate) { @@ -1067,10 +1066,14 @@ static void UpdateBranchOnDelete(ICommit commit, in Node.Branch branch, NibbleSe { memo.Clear(nibble, children); } - else if (memo.Length > 0) + else if (leftover.Length > 0) { - deleteWorkingSet = ArrayPool.Shared.Rent(leftover.Length - Keccak.Size); - memo = RlpMemo.Delete(memo, nibble, children, deleteWorkingSet); + if (rlpWorkingSet == null) + { + rlpWorkingSet = ArrayPool.Shared.Rent(leftover.Length - Keccak.Size); + } + + memo = RlpMemo.Delete(memo, nibble, children, rlpWorkingSet); } var shouldUpdate = !branch.Children.Equals(children); @@ -1085,11 +1088,6 @@ static void UpdateBranchOnDelete(ICommit commit, in Node.Branch branch, NibbleSe { ArrayPool.Shared.Return(rlpWorkingSet); } - - if (deleteWorkingSet != null) - { - ArrayPool.Shared.Return(deleteWorkingSet); - } } static DeleteStatus ThrowUnknownType() From 14c0e65b0f0dd41d3441cce64dd9c1979521959d Mon Sep 17 00:00:00 2001 From: Diptanshu Kakwani Date: Tue, 31 Dec 2024 15:49:39 +0530 Subject: [PATCH 04/15] minor fixes --- src/Paprika.Tests/Merkle/RlpMemoTests.cs | 2 +- src/Paprika/Merkle/ComputeMerkleBehavior.cs | 15 ++++++++--- src/Paprika/Merkle/RlpMemo.cs | 29 ++++++++++----------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/Paprika.Tests/Merkle/RlpMemoTests.cs b/src/Paprika.Tests/Merkle/RlpMemoTests.cs index 31bd5426..04bdad7d 100644 --- a/src/Paprika.Tests/Merkle/RlpMemoTests.cs +++ b/src/Paprika.Tests/Merkle/RlpMemoTests.cs @@ -183,7 +183,7 @@ public void Large_random_operations(int numOperations) for (var i = 0; i < NibbleSet.NibbleCount; i++) { - children[(byte)i] = false; + children[(byte)i] = true; } var memo = new RlpMemo(raw); diff --git a/src/Paprika/Merkle/ComputeMerkleBehavior.cs b/src/Paprika/Merkle/ComputeMerkleBehavior.cs index d4badfea..7e4acb39 100644 --- a/src/Paprika/Merkle/ComputeMerkleBehavior.cs +++ b/src/Paprika/Merkle/ComputeMerkleBehavior.cs @@ -734,14 +734,21 @@ private void EncodeBranch(scoped in Key key, scoped in ComputeContext ctx, scope stream.Encode(value); if (memoize) { + memoizedUpdated = true; + if (value.Length == Keccak.Size) { - memoizedUpdated = true; - memo.SetRaw(value, i, branch.Children); + if (memo.Exists(i, branch.Children)) + { + memo.SetRaw(value, i, branch.Children); + } + else + { + memo = RlpMemo.Insert(memo, i, branch.Children, value, rlpMemoization); + } } - else + else if (memo.Exists(i, branch.Children)) { - memoizedUpdated = true; memo.Clear(i, branch.Children); } } diff --git a/src/Paprika/Merkle/RlpMemo.cs b/src/Paprika/Merkle/RlpMemo.cs index 3bcd998f..0a26ea3e 100644 --- a/src/Paprika/Merkle/RlpMemo.cs +++ b/src/Paprika/Merkle/RlpMemo.cs @@ -128,20 +128,19 @@ public static RlpMemo Insert(RlpMemo memo, byte nibble, NibbleSet.Readonly child if (memo.Length != 0) { - var remainingBytes = (children.SetCount - insertIndex - 1) * Keccak.Size; + // Copy all the elements before the new element + if (insertOffset > 0) + { + memo._buffer.Slice(0, insertOffset).CopyTo(span); + } // Copy all the elements after the new element + var remainingBytes = (children.SetCount - insertIndex - 1) * Keccak.Size; if (remainingBytes > 0) { memo._buffer.Slice(insertOffset, remainingBytes) .CopyTo(span.Slice(insertOffset + Keccak.Size)); } - - // Copy elements before the new element - if (insertOffset > 0) - { - memo._buffer.Slice(0, insertOffset).CopyTo(span); - } } else { @@ -176,18 +175,18 @@ public static RlpMemo Delete(RlpMemo memo, byte nibble, NibbleSet.Readonly child var deleteIndex = BitOperations.PopCount(leftChildren); var deleteOffset = deleteIndex * Keccak.Size; - // Copy elements after the deleted element - int remainingBytes = (children.SetCount - deleteIndex) * Keccak.Size; - if (remainingBytes > 0) + // Copy all the elements before the deleted element + if (deleteOffset > 0) { - memo._buffer.Slice(deleteOffset + Keccak.Size, remainingBytes) - .CopyTo(span.Slice(deleteOffset)); + memo._buffer.Slice(0, deleteOffset).CopyTo(span); } - // Copy elements before the deleted element - if (deleteOffset > 0) + // Copy all the elements after the deleted element + var remainingBytes = (children.SetCount - deleteIndex) * Keccak.Size; + if (remainingBytes > 0) { - memo._buffer.Slice(0, deleteOffset).CopyTo(span); + memo._buffer.Slice(deleteOffset + Keccak.Size, remainingBytes) + .CopyTo(span.Slice(deleteOffset)); } return new RlpMemo(span); From b4f20932cbd70ba3bcc104ac8c0b76dacd33928d Mon Sep 17 00:00:00 2001 From: Diptanshu Kakwani Date: Mon, 6 Jan 2025 11:45:20 +0530 Subject: [PATCH 05/15] add nibbleset in RlpMemo to remember the order --- src/Paprika.Tests/Merkle/RlpMemoTests.cs | 114 +++++++------ src/Paprika/Merkle/CommitExtensions.cs | 1 - src/Paprika/Merkle/ComputeMerkleBehavior.cs | 63 +++----- src/Paprika/Merkle/NibbleSet.cs | 11 ++ src/Paprika/Merkle/RlpMemo.cs | 169 +++++++++++--------- 5 files changed, 189 insertions(+), 169 deletions(-) diff --git a/src/Paprika.Tests/Merkle/RlpMemoTests.cs b/src/Paprika.Tests/Merkle/RlpMemoTests.cs index 04bdad7d..75ae90e1 100644 --- a/src/Paprika.Tests/Merkle/RlpMemoTests.cs +++ b/src/Paprika.Tests/Merkle/RlpMemoTests.cs @@ -13,7 +13,6 @@ public class RlpMemoTests private enum RlpMemoOperation { Set, - SetRaw, Clear, Delete, Insert @@ -25,11 +24,17 @@ public void Random_delete() Span raw = stackalloc byte[RlpMemo.MaxSize]; var children = new NibbleSet(); - for (var i = 0; i < RlpMemo.MaxSize; i++) + for (var i = 0; i < RlpMemo.MaxSize - NibbleSet.MaxByteSize; i++) { raw[i] = (byte)(i & 0xFF); } + // Set all the index bits at the end. + for (var i = RlpMemo.MaxSize - 1; i >= RlpMemo.MaxSize - NibbleSet.MaxByteSize; i--) + { + raw[i] = 0xFF; + } + for (var i = 0; i < NibbleSet.NibbleCount; i++) { children[(byte)i] = true; @@ -48,11 +53,15 @@ public void Random_delete() } children[child] = false; - memo = RlpMemo.Delete(memo, child, children, raw); + memo = RlpMemo.Delete(memo, child, raw); - memo.Length.Should().Be(RlpMemo.MaxSize - (i + 1) * Keccak.Size); - memo.Exists(child, children).Should().BeFalse(); - memo.TryGetKeccak(child, out var keccak, children).Should().BeFalse(); + var expectedLength = (i != NibbleSet.NibbleCount - 1) + ? RlpMemo.MaxSize - (i + 1) * Keccak.Size + NibbleSet.MaxByteSize + : 0; + + memo.Length.Should().Be(expectedLength); + memo.Exists(child).Should().BeFalse(); + memo.TryGetKeccak(child, out var keccak).Should().BeFalse(); keccak.IsEmpty.Should().BeTrue(); } @@ -87,11 +96,15 @@ public void Random_insert() } children[child] = true; - memo = RlpMemo.Insert(memo, child, children, keccak, workingMemory); + memo = RlpMemo.Insert(memo, child, keccak, workingMemory); + + var expectedLength = (i != NibbleSet.NibbleCount - 1) + ? (i + 1) * Keccak.Size + NibbleSet.MaxByteSize + : RlpMemo.MaxSize; - memo.Length.Should().Be((i + 1) * Keccak.Size); - memo.Exists(child, children).Should().BeTrue(); - memo.TryGetKeccak(child, out var k, children).Should().BeTrue(); + memo.Length.Should().Be(expectedLength); + memo.Exists(child).Should().BeTrue(); + memo.TryGetKeccak(child, out var k).Should().BeTrue(); k.SequenceEqual(keccak).Should().BeTrue(); } @@ -102,29 +115,39 @@ public void Random_insert() public void In_place_update() { Span raw = stackalloc byte[RlpMemo.MaxSize]; + var children = new NibbleSet(); - for (var i = 0; i < RlpMemo.MaxSize; i++) + for (var i = 0; i < RlpMemo.MaxSize - NibbleSet.MaxByteSize; i++) { raw[i] = (byte)(i & 0xFF); } - var memo = new RlpMemo(raw); + // Set all the index bits at the end. + for (var i = RlpMemo.MaxSize - 1; i >= RlpMemo.MaxSize - NibbleSet.MaxByteSize; i--) + { + raw[i] = 0xFF; + } - var children = new NibbleSet(); for (var i = 0; i < NibbleSet.NibbleCount; i++) { children[(byte)i] = true; } + var memo = new RlpMemo(raw); + // Delete each child and the corresponding keccak for (byte i = 0; i < NibbleSet.NibbleCount; i++) { children[i] = false; - memo = RlpMemo.Delete(memo, i, children, raw); + memo = RlpMemo.Delete(memo, i, raw); + + var expectedLength = (i != NibbleSet.NibbleCount - 1) + ? RlpMemo.MaxSize - (i + 1) * Keccak.Size + NibbleSet.MaxByteSize + : 0; - memo.Length.Should().Be(RlpMemo.MaxSize - (i + 1) * Keccak.Size); - memo.Exists(i, children).Should().BeFalse(); - memo.TryGetKeccak(i, out var k, children).Should().BeFalse(); + memo.Length.Should().Be(expectedLength); + memo.Exists(i).Should().BeFalse(); + memo.TryGetKeccak(i, out var k).Should().BeFalse(); k.IsEmpty.Should().BeTrue(); } @@ -137,11 +160,15 @@ public void In_place_update() for (byte i = 0; i < NibbleSet.NibbleCount; i++) { children[i] = true; - memo = RlpMemo.Insert(memo, i, children, keccak, raw); + memo = RlpMemo.Insert(memo, i, keccak, raw); - memo.Length.Should().Be((i + 1) * Keccak.Size); - memo.Exists(i, children).Should().BeTrue(); - memo.TryGetKeccak(i, out var k, children).Should().BeTrue(); + var expectedLength = (i != NibbleSet.NibbleCount - 1) + ? (i + 1) * Keccak.Size + NibbleSet.MaxByteSize + : RlpMemo.MaxSize; + + memo.Length.Should().Be(expectedLength); + memo.Exists(i).Should().BeTrue(); + memo.TryGetKeccak(i, out var k).Should().BeTrue(); k.SequenceEqual(keccak).Should().BeTrue(); } @@ -173,14 +200,19 @@ public void Large_random_operations(int numOperations) var children = new NibbleSet(); Span keccak = new byte[Keccak.Size]; - keccak.Fill(0xFF); - KeccakOrRlp.FromSpan(keccak, out var keccakOrRlp); + keccak.Fill(0xFF); - for (var i = 0; i < RlpMemo.MaxSize; i++) + for (var i = 0; i < RlpMemo.MaxSize - NibbleSet.MaxByteSize; i++) { raw[i] = (byte)(i & 0xFF); } + // Set all the index bits at the end. + for (var i = RlpMemo.MaxSize - 1; i >= RlpMemo.MaxSize - NibbleSet.MaxByteSize; i--) + { + raw[i] = 0xFF; + } + for (var i = 0; i < NibbleSet.NibbleCount; i++) { children[(byte)i] = true; @@ -197,53 +229,43 @@ public void Large_random_operations(int numOperations) switch (op) { case RlpMemoOperation.Set: - if (memo.Exists(child, children)) - { - memo.Set(keccakOrRlp, child, children); - - memo.TryGetKeccak(child, out var k, children).Should().BeTrue(); - k.SequenceEqual(keccakOrRlp.Span).Should().BeTrue(); - } - - break; - case RlpMemoOperation.SetRaw: - if (memo.Exists(child, children)) + if (memo.Exists(child)) { - memo.SetRaw(keccak, child, children); + memo.Set(keccak, child); - memo.TryGetKeccak(child, out var k, children).Should().BeTrue(); + memo.TryGetKeccak(child, out var k).Should().BeTrue(); k.SequenceEqual(keccak).Should().BeTrue(); } break; case RlpMemoOperation.Clear: - if (memo.Exists(child, children)) + if (memo.Exists(child)) { - memo.Clear(child, children); + memo.Clear(child); - memo.TryGetKeccak(child, out var k, children).Should().BeFalse(); + memo.TryGetKeccak(child, out var k).Should().BeFalse(); k.IsEmpty.Should().BeTrue(); } break; case RlpMemoOperation.Delete: - if (memo.Exists(child, children)) + if (memo.Exists(child)) { children[child] = false; - memo = RlpMemo.Delete(memo, child, children, raw); + memo = RlpMemo.Delete(memo, child, raw); - memo.TryGetKeccak(child, out var k, children).Should().BeFalse(); + memo.TryGetKeccak(child, out var k).Should().BeFalse(); k.IsEmpty.Should().BeTrue(); } break; case RlpMemoOperation.Insert: - if (!memo.Exists(child, children)) + if (!memo.Exists(child)) { children[child] = true; - memo = RlpMemo.Insert(memo, child, children, keccak, raw); + memo = RlpMemo.Insert(memo, child, keccak, raw); - memo.TryGetKeccak(child, out var k, children).Should().BeTrue(); + memo.TryGetKeccak(child, out var k).Should().BeTrue(); k.SequenceEqual(keccak).Should().BeTrue(); } diff --git a/src/Paprika/Merkle/CommitExtensions.cs b/src/Paprika/Merkle/CommitExtensions.cs index 9cee5e0a..61a5d3eb 100644 --- a/src/Paprika/Merkle/CommitExtensions.cs +++ b/src/Paprika/Merkle/CommitExtensions.cs @@ -42,7 +42,6 @@ public static void SetBranch(this ICommit commit, in Key key, NibbleSet.Readonly EntryType type = EntryType.Persistent) { var branch = new Branch(children); - Debug.Assert(rlp.Length == 0 || rlp.Length == (children.SetCount * Keccak.Size)); commit.Set(key, branch.WriteTo(stackalloc byte[Branch.MaxByteLength]), rlp, type); } diff --git a/src/Paprika/Merkle/ComputeMerkleBehavior.cs b/src/Paprika/Merkle/ComputeMerkleBehavior.cs index 7e4acb39..c2da3d40 100644 --- a/src/Paprika/Merkle/ComputeMerkleBehavior.cs +++ b/src/Paprika/Merkle/ComputeMerkleBehavior.cs @@ -379,39 +379,19 @@ private BuildStorageTriesItem[] GetStorageWorkItems(ICommitWithStats commit, Cac public ReadOnlySpan InspectBeforeApply(in Key key, ReadOnlySpan data, Span workingSet) { - if (data.IsEmpty) - return data; - - if (key.Type != DataType.Merkle) - return data; - - var node = Node.Header.Peek(data).NodeType; - - if (node != Node.Type.Branch) + if (data.IsEmpty || key.Type != DataType.Merkle) { - // Return data as is, either the node is not a branch or the memoization is not set for branches. return data; } - if (key.Path.Length < SkipRlpMemoizationForTopLevelsCount) + if (key.Path.Length < SkipRlpMemoizationForTopLevelsCount && + Node.Header.Peek(data).NodeType == Node.Type.Branch) { // For State branches, omit top levels of RLP memoization return Node.Branch.GetOnlyBranchData(data); } - var memoizedRlp = Node.Branch.ReadFrom(data, out var branch); - if (memoizedRlp.Length == 0) - { - // no RLP of children memoized, return - return data; - } - - // Copy the memoized RLPs - var dataLength = data.Length - memoizedRlp.Length; - data[..dataLength].CopyTo(workingSet); - memoizedRlp.CopyTo(workingSet[dataLength..]); - - return workingSet[..data.Length]; + return data; } public Keccak RootHash { get; private set; } @@ -616,7 +596,7 @@ private void EncodeBranch(scoped in Key key, scoped in ComputeContext ctx, scope { if (branch.Children[i]) { - if (memoize && memo.TryGetKeccak(i, out var keccak, branch.Children)) + if (memoize && memo.TryGetKeccak(i, out var keccak)) { // keccak from cache stream.Encode(keccak); @@ -649,13 +629,13 @@ private void EncodeBranch(scoped in Key key, scoped in ComputeContext ctx, scope { memoizedUpdated = true; - if (memo.Exists(i, branch.Children)) + if (memo.Exists(i)) { - memo.Set(keccakOrRlp, i, branch.Children); + memo.Set(keccakOrRlp.Span, i); } else if (keccakOrRlp.DataType == KeccakOrRlp.Type.Keccak) { - memo = RlpMemo.Insert(memo, i, branch.Children, keccakOrRlp.Span, rlpMemoization); + memo = RlpMemo.Insert(memo, i, keccakOrRlp.Span, rlpMemoization); } } } @@ -663,9 +643,9 @@ private void EncodeBranch(scoped in Key key, scoped in ComputeContext ctx, scope { stream.EncodeEmptyArray(); - if (memoize && memo.Exists(i, branch.Children)) + if (memoize && memo.Exists(i)) { - memo = RlpMemo.Delete(memo, i, branch.Children, rlpMemoization); + memo = RlpMemo.Delete(memo, i, rlpMemoization); memoizedUpdated = true; } } @@ -738,18 +718,18 @@ private void EncodeBranch(scoped in Key key, scoped in ComputeContext ctx, scope if (value.Length == Keccak.Size) { - if (memo.Exists(i, branch.Children)) + if (memo.Exists(i)) { - memo.SetRaw(value, i, branch.Children); + memo.Set(value, i); } else { - memo = RlpMemo.Insert(memo, i, branch.Children, value, rlpMemoization); + memo = RlpMemo.Insert(memo, i, value, rlpMemoization); } } - else if (memo.Exists(i, branch.Children)) + else if (memo.Exists(i)) { - memo.Clear(i, branch.Children); + memo = RlpMemo.Delete(memo, i, rlpMemoization); } } } @@ -964,7 +944,6 @@ private static DeleteStatus Delete(in NibblePath path, int at, ICommit commit, C case Node.Type.Branch: { var nibble = path[at]; - Debug.Assert(leftover.Length == 0 || leftover.Length == (branch.Children.SetCount * Keccak.Size)); if (!branch.Children[nibble]) { // no such child @@ -1069,18 +1048,18 @@ static void UpdateBranchOnDelete(ICommit commit, in Node.Branch branch, NibbleSe } // If this child still exists, only clear the memo. Otherwise, delete it from the memo. - if (memo.Exists(nibble, children)) + if (children[nibble] && memo.Exists(nibble)) { - memo.Clear(nibble, children); + memo.Clear(nibble); } - else if (leftover.Length > 0) + else if (memo.Exists(nibble)) { if (rlpWorkingSet == null) { rlpWorkingSet = ArrayPool.Shared.Rent(leftover.Length - Keccak.Size); } - memo = RlpMemo.Delete(memo, nibble, children, rlpWorkingSet); + memo = RlpMemo.Delete(memo, nibble, rlpWorkingSet); } var shouldUpdate = !branch.Children.Equals(children); @@ -1326,11 +1305,11 @@ private static void MarkPathDirty(in NibblePath path, in Span rlpMemoWorki // If this path does not exist, insert an empty Keccak. Otherwise, clear it in the memo. if (createLeaf) { - memo = RlpMemo.Insert(memo, nibble, children, Keccak.Zero.Span, rlpMemoWorkingSet); + memo = RlpMemo.Insert(memo, nibble, Keccak.Zero.Span, rlpMemoWorkingSet); } else { - memo.Clear(nibble, children); + memo.Clear(nibble); } if (shouldUpdateBranch || childRlpRequiresUpdate) diff --git a/src/Paprika/Merkle/NibbleSet.cs b/src/Paprika/Merkle/NibbleSet.cs index 86019147..4c0952a1 100644 --- a/src/Paprika/Merkle/NibbleSet.cs +++ b/src/Paprika/Merkle/NibbleSet.cs @@ -55,6 +55,13 @@ public bool this[byte nibble] public int SetCount => BitOperations.PopCount(_value); + public int SetCountBefore(byte nibble) + { + // Compute the number of set bits before `nibble` (including itself). + var leftChildren = (ushort)(_value & ((1U << (nibble + 1)) - 1)); + return BitOperations.PopCount(leftChildren); + } + public byte SmallestNibbleSet => (byte)BitOperations.TrailingZeroCount(_value); public byte BiggestNibbleSet => (byte)(31 - BitOperations.LeadingZeroCount((uint)_value)); @@ -102,10 +109,14 @@ public Readonly(ushort value) public static Readonly AllWithout(byte nibble) => new((ushort)(AllSetValue & ~(1 << nibble))); + public static Readonly None => new(0); + public bool AllSet => _value == AllSetValue; public int SetCount => new NibbleSet(_value).SetCount; + public int SetCountBefore(byte nibble) => new NibbleSet(_value).SetCountBefore(nibble); + public byte SmallestNibbleSet => new NibbleSet(_value).SmallestNibbleSet; public byte SmallestNibbleNotSet => new NibbleSet(_value).SmallestNibbleNotSet; diff --git a/src/Paprika/Merkle/RlpMemo.cs b/src/Paprika/Merkle/RlpMemo.cs index 0a26ea3e..d45e36e9 100644 --- a/src/Paprika/Merkle/RlpMemo.cs +++ b/src/Paprika/Merkle/RlpMemo.cs @@ -25,36 +25,17 @@ public RlpMemo(Span buffer) public int Length => _buffer.Length; - public void SetRaw(ReadOnlySpan keccak, byte nibble, NibbleSet.Readonly children) + public void Set(ReadOnlySpan keccak, byte nibble) { - var span = GetAtNibble(nibble, children); + var span = GetAtNibble(nibble); Debug.Assert(!span.IsEmpty); - Debug.Assert(_buffer.Length == (children.SetCount * Keccak.Size)); keccak.CopyTo(span); } - public void Set(in KeccakOrRlp keccakOrRlp, byte nibble, NibbleSet.Readonly children) + public void Clear(byte nibble) { - var span = GetAtNibble(nibble, children); - Debug.Assert(!span.IsEmpty); - Debug.Assert(_buffer.Length == (children.SetCount * Keccak.Size)); - - if (keccakOrRlp.DataType == KeccakOrRlp.Type.Keccak) - { - keccakOrRlp.Span.CopyTo(span); - } - else - { - // on rlp, memoize none - span.Clear(); - } - } - - public void Clear(byte nibble, NibbleSet.Readonly children) - { - var span = GetAtNibble(nibble, children); - Debug.Assert(_buffer.Length == 0 || _buffer.Length == (children.SetCount * Keccak.Size)); + var span = GetAtNibble(nibble); if (!span.IsEmpty) { @@ -62,10 +43,9 @@ public void Clear(byte nibble, NibbleSet.Readonly children) } } - public bool TryGetKeccak(byte nibble, out ReadOnlySpan keccak, NibbleSet.Readonly children) + public bool TryGetKeccak(byte nibble, out ReadOnlySpan keccak) { - var span = GetAtNibble(nibble, children); - Debug.Assert(_buffer.Length == 0 || _buffer.Length == (children.SetCount * Keccak.Size)); + var span = GetAtNibble(nibble); if (span.IndexOfAnyExcept((byte)0) >= 0) { @@ -77,29 +57,53 @@ public bool TryGetKeccak(byte nibble, out ReadOnlySpan keccak, NibbleSet.R return false; } - public bool Exists(byte nibble, NibbleSet.Readonly children) + public bool Exists(byte nibble) { - // Check if the element exists - if (_buffer.Length == 0 || ((ushort)children & (1U << nibble)) == 0) + if (_buffer.Length == 0) { return false; } - return true; + GetIndex(out var index); + + return index[nibble]; } - private Span GetAtNibble(byte nibble, NibbleSet.Readonly children) + private Span GetAtNibble(byte nibble) { - var leftChildren = (ushort)((ushort)children & ((1U << (nibble + 1)) - 1)); + if (_buffer.Length == 0) + { + return []; + } + + GetIndex(out var index); // Check if the element exists - if (_buffer.Length == 0 || (leftChildren & (1U << nibble)) == 0) + if (!index[nibble]) { return []; } - var index = BitOperations.PopCount(leftChildren) - 1; - return _buffer.Slice(index * Keccak.Size, Keccak.Size); + var nibbleIndex = index.SetCountBefore(nibble) - 1; + return _buffer.Slice(nibbleIndex * Keccak.Size, Keccak.Size); + } + + private void GetIndex(out NibbleSet.Readonly index) + { + // Extract the index bits. + var indexLength = _buffer.Length % Keccak.Size; + + if (indexLength != 0) + { + var bits = _buffer[^indexLength..]; + NibbleSet.Readonly.ReadFrom(bits, out index); + } + else + { + Debug.Assert(_buffer.Length is 0 or MaxSize); + + index = _buffer.IsEmpty ? NibbleSet.Readonly.None : NibbleSet.Readonly.All; + } } public static RlpMemo Copy(ReadOnlySpan from, scoped in Span to) @@ -109,86 +113,91 @@ public static RlpMemo Copy(ReadOnlySpan from, scoped in Span to) return new RlpMemo(span); } - public static RlpMemo Insert(RlpMemo memo, byte nibble, NibbleSet.Readonly children, - ReadOnlySpan keccak, scoped in Span workingSet) + public static RlpMemo Insert(RlpMemo memo, byte nibble, ReadOnlySpan keccak, scoped in Span workingSet) { - // Compute the destination size for copying. - var size = children.SetCount * Keccak.Size; + memo.GetIndex(out var index); + // Ensure that this element doesn't already exist. + Debug.Assert(!index[nibble]); + + // Compute the destination size for copying. + var size = (index.SetCount < NibbleSet.NibbleCount - 1) + ? (index.SetCount + 1) * Keccak.Size + NibbleSet.MaxByteSize + : MaxSize; Debug.Assert(workingSet.Length >= size); - var span = workingSet[..size]; - // Ensure that this element already exists in the list of children - var leftChildren = (ushort)((ushort)children & ((1U << (nibble + 1)) - 1)); - Debug.Assert((leftChildren & (1U << nibble)) != 0); + var span = workingSet[..size]; - // Find the index of this nibble in the memo - var insertIndex = BitOperations.PopCount(leftChildren) - 1; + // Compute the index of this nibble in the memo + var insertIndex = index.SetCountBefore(nibble); var insertOffset = insertIndex * Keccak.Size; - if (memo.Length != 0) + // Copy all the elements before the new element + if (insertOffset > 0) { - // Copy all the elements before the new element - if (insertOffset > 0) - { - memo._buffer.Slice(0, insertOffset).CopyTo(span); - } - - // Copy all the elements after the new element - var remainingBytes = (children.SetCount - insertIndex - 1) * Keccak.Size; - if (remainingBytes > 0) - { - memo._buffer.Slice(insertOffset, remainingBytes) - .CopyTo(span.Slice(insertOffset + Keccak.Size)); - } + memo._buffer[..insertOffset].CopyTo(span); } - else + + // Copy all the elements after the new element (except the index) + if (memo.Length > insertOffset) { - // Insert empty keccak for all the existing children - span.Clear(); + memo._buffer[insertOffset..^NibbleSet.MaxByteSize].CopyTo(span[(insertOffset + Keccak.Size)..]); } - keccak.CopyTo(span.Slice(insertOffset)); + keccak.CopyTo(span[insertOffset..]); + + // Update the index. + index = index.Set(nibble); + + if (size != MaxSize) + { + index.WriteToWithLeftover(span[^NibbleSet.MaxByteSize..]); + } return new RlpMemo(span); } - public static RlpMemo Delete(RlpMemo memo, byte nibble, NibbleSet.Readonly children, - scoped in Span workingSet) + public static RlpMemo Delete(RlpMemo memo, byte nibble, scoped in Span workingSet) { + memo.GetIndex(out var index); + + // Ensure that this element isn't already deleted. + Debug.Assert(index[nibble]); + // Compute the destination size for copying. - var size = memo.Length - Keccak.Size; - if (size < 0) + var size = (index.SetCount < NibbleSet.NibbleCount) + ? memo.Length - Keccak.Size + : memo.Length - Keccak.Size + NibbleSet.MaxByteSize; + + if (size <= NibbleSet.MaxByteSize) { - // Memo is already empty, nothing to delete. + // Empty RlpMemo after this delete operation. size = 0; return new RlpMemo(workingSet[..size]); } var span = workingSet[..size]; - // Ensure that this element does not exist in the list of children - var leftChildren = (ushort)((ushort)children & ((1U << (nibble + 1)) - 1)); - Debug.Assert((leftChildren & (1U << nibble)) == 0); - - // Find the index of this nibble in the memo - var deleteIndex = BitOperations.PopCount(leftChildren); + // Compute the index of this nibble in the memo + var deleteIndex = index.SetCountBefore(nibble) - 1; var deleteOffset = deleteIndex * Keccak.Size; // Copy all the elements before the deleted element if (deleteOffset > 0) { - memo._buffer.Slice(0, deleteOffset).CopyTo(span); + memo._buffer[..deleteOffset].CopyTo(span); } - // Copy all the elements after the deleted element - var remainingBytes = (children.SetCount - deleteIndex) * Keccak.Size; - if (remainingBytes > 0) + // Copy all the elements after the deleted element (except the index) + if (memo.Length > (deleteOffset + Keccak.Size)) { - memo._buffer.Slice(deleteOffset + Keccak.Size, remainingBytes) - .CopyTo(span.Slice(deleteOffset)); + memo._buffer[(deleteOffset + Keccak.Size)..^NibbleSet.MaxByteSize].CopyTo(span[deleteOffset..]); } + // Update the index. + index = index.Remove(nibble); + index.WriteToWithLeftover(span[^NibbleSet.MaxByteSize..]); + return new RlpMemo(span); } } From bcec7e1df15a2ae7fe581865cc5a947204a72391 Mon Sep 17 00:00:00 2001 From: Diptanshu Kakwani Date: Mon, 6 Jan 2025 11:46:57 +0530 Subject: [PATCH 06/15] nit spacing fix --- src/Paprika.Tests/Merkle/RlpMemoTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Paprika.Tests/Merkle/RlpMemoTests.cs b/src/Paprika.Tests/Merkle/RlpMemoTests.cs index 75ae90e1..3a3bac97 100644 --- a/src/Paprika.Tests/Merkle/RlpMemoTests.cs +++ b/src/Paprika.Tests/Merkle/RlpMemoTests.cs @@ -200,7 +200,7 @@ public void Large_random_operations(int numOperations) var children = new NibbleSet(); Span keccak = new byte[Keccak.Size]; - keccak.Fill(0xFF); + keccak.Fill(0xFF); for (var i = 0; i < RlpMemo.MaxSize - NibbleSet.MaxByteSize; i++) { From eeec2e7fb2812ab6fc69cbb9fdd6cb10def0b1c6 Mon Sep 17 00:00:00 2001 From: Diptanshu Kakwani Date: Mon, 6 Jan 2025 12:15:56 +0530 Subject: [PATCH 07/15] merge with latest main --- src/Paprika/Chain/Blockchain.cs | 8 ++++---- src/Paprika/Merkle/ComputeMerkleBehavior.cs | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 0e9788a2..122f390b 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -1499,7 +1499,7 @@ protected override void CleanUp() public void ProcessProofNodes(Keccak accountKeccak, Span packedProofPaths, int proofCount) { Span workingSpan = stackalloc byte[NibblePath.MaxLengthValue * 2 + 1]; - Span rlpMemoization = stackalloc byte[RlpMemo.Size]; + Span rlpMemoization = stackalloc byte[RlpMemo.MaxSize]; Span targetKeySpan = stackalloc byte[NibblePath.FullKeccakByteLength * 2 + 1]; Span nodeWorkingSpan = stackalloc byte[Node.Extension.MaxByteLength]; @@ -1536,7 +1536,7 @@ public void ProcessProofNodes(Keccak accountKeccak, Span packedProofPaths, bool allChildrenPersisted = true; - RlpMemo memo = RlpMemo.Decompress(leftover, branch.Children, rlpMemoization); + var memo = RlpMemo.Copy(leftover, rlpMemoization); for (byte i = 0; i < NibbleSet.NibbleCount; i++) { @@ -2007,14 +2007,14 @@ public void CreateMerkleBranch(in Keccak account, in NibblePath storagePath, byt Key key = account == Keccak.Zero ? Key.Merkle(storagePath) : Key.Raw(NibblePath.FromKey(account), DataType.Merkle, storagePath); NibbleSet set = new NibbleSet(); - Span rlpMemoization = stackalloc byte[RlpMemo.Size]; + Span rlpMemoization = stackalloc byte[RlpMemo.MaxSize]; RlpMemo memo = new RlpMemo(rlpMemoization); for (int i = 0; i < childNibbles.Length; i++) { set[childNibbles[i]] = true; if (childHashes[i] != Keccak.Zero) - memo.Set(childHashes[i], childNibbles[i]); + memo.Set(childHashes[i].Span, childNibbles[i]); } _current.SetBranch(key, set, memo.Raw, persist ? EntryType.Persistent : EntryType.Proof); } diff --git a/src/Paprika/Merkle/ComputeMerkleBehavior.cs b/src/Paprika/Merkle/ComputeMerkleBehavior.cs index c2da3d40..3947bd6f 100644 --- a/src/Paprika/Merkle/ComputeMerkleBehavior.cs +++ b/src/Paprika/Merkle/ComputeMerkleBehavior.cs @@ -120,11 +120,11 @@ public Keccak GetHash(scoped in NibblePath path, IReadOnlyWorldState commit, boo using var merkleData = commit.Get(parentKey); if (!merkleData.Span.IsEmpty) { - var leftover = Node.ReadFrom(out var parenType, out var leaf, out _, out var branch, merkleData.Span); - if (parenType == Node.Type.Branch && !ignoreCache) + var leftover = Node.ReadFrom(out var parentType, out var leaf, out _, out var branch, merkleData.Span); + if (parentType == Node.Type.Branch && !ignoreCache) { - Span rlpMemoization = stackalloc byte[RlpMemo.Size]; - RlpMemo memo = RlpMemo.Decompress(leftover, branch.Children, rlpMemoization); + Span rlpMemoization = stackalloc byte[leftover.Length]; + var memo = RlpMemo.Copy(leftover, rlpMemoization); if (memo.TryGetKeccak(path[NibblePath.KeccakNibbleCount - 1], out var keccakSpan)) return new Keccak(keccakSpan); } @@ -161,8 +161,8 @@ public Keccak GetStorageHash(IReadOnlyWorldState commit, in Keccak account, Nibb var leftover = Node.ReadFrom(out var parenType, out var leaf, out _, out var branch, merkleData.Span); if (parenType == Node.Type.Branch && !ignoreCache) { - Span rlpMemoization = stackalloc byte[RlpMemo.Size]; - RlpMemo memo = RlpMemo.Decompress(leftover, branch.Children, rlpMemoization); + Span rlpMemoization = stackalloc byte[leftover.Length]; + var memo = RlpMemo.Copy(leftover, rlpMemoization); if (memo.TryGetKeccak(storagePath[NibblePath.KeccakNibbleCount - 1], out var keccakSpan)) return new Keccak(keccakSpan); } @@ -1406,7 +1406,7 @@ public void Accept(IMerkleTrieVisitor visitor, NibblePath path, MerkleVisitorCon context.Level++; Span workingSpan = stackalloc byte[NibblePath.MaxLengthValue * 2 + 1]; - Span rlpMemoization = stackalloc byte[RlpMemo.Size]; + Span rlpMemoization = stackalloc byte[leftover.Length]; for (byte i = 0; i < NibbleSet.NibbleCount; i++) { if (branch.Children[i]) @@ -1416,7 +1416,7 @@ public void Accept(IMerkleTrieVisitor visitor, NibblePath path, MerkleVisitorCon if (childPath.Length == NibblePath.KeccakNibbleCount) { - RlpMemo memo = RlpMemo.Decompress(leftover, branch.Children, rlpMemoization); + var memo = RlpMemo.Copy(leftover, rlpMemoization); KeccakOrRlp childKeccakOrRlp = Keccak.Zero; if (memo.TryGetKeccak(i, out var keccakSpan)) { From 8587bf1e21c997a96be012c182b94877cceb4daa Mon Sep 17 00:00:00 2001 From: Diptanshu Kakwani Date: Mon, 13 Jan 2025 17:26:49 +0530 Subject: [PATCH 08/15] address comments, re-order header - breaking merkle computation tests --- src/Paprika.Tests/Merkle/RlpMemoTests.cs | 174 ++++++++++++++++++-- src/Paprika/Merkle/ComputeMerkleBehavior.cs | 25 ++- src/Paprika/Merkle/RlpMemo.cs | 127 ++++++++------ 3 files changed, 260 insertions(+), 66 deletions(-) diff --git a/src/Paprika.Tests/Merkle/RlpMemoTests.cs b/src/Paprika.Tests/Merkle/RlpMemoTests.cs index 3a3bac97..c0d1213e 100644 --- a/src/Paprika.Tests/Merkle/RlpMemoTests.cs +++ b/src/Paprika.Tests/Merkle/RlpMemoTests.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using Paprika.Chain; using Paprika.Crypto; using Paprika.Data; using Paprika.Merkle; @@ -18,6 +19,141 @@ private enum RlpMemoOperation Insert } + [Test] + public void Insert_get_operation() + { + Span raw = []; + Span workingMemory = new byte[RlpMemo.MaxSize]; + + Span keccak = new byte[Keccak.Size]; + keccak.Fill(0xFF); + + NibbleSet.Readonly children = new NibbleSet(0xA, 0xB, 0xC); + + var memo = new RlpMemo(raw); + memo = InsertKeccak(memo, children, keccak, workingMemory); + + memo.Length.Should().Be(GetExpectedSize(children.SetCount)); + + for (byte i = 0; i < NibbleSet.NibbleCount; i++) + { + if (children[i]) + { + memo.Exists(i).Should().BeTrue(); + memo.TryGetKeccak(i, out var k).Should().BeTrue(); + k.SequenceEqual(keccak).Should().BeTrue(); + } + } + } + + [Test] + public void Set_get_operation() + { + Span raw = []; + Span workingMemory = new byte[RlpMemo.MaxSize]; + + Span keccak = new byte[Keccak.Size]; + keccak.Fill(0xFF); + + Span keccakNew = new byte[Keccak.Size]; + keccakNew.Fill(0xAA); + + NibbleSet.Readonly children = new NibbleSet(0xA, 0xB, 0xC); + + var memo = new RlpMemo(raw); + memo = InsertKeccak(memo, children, keccak, workingMemory); + + for (byte i = 0; i < NibbleSet.NibbleCount; i++) + { + if (children[i]) + { + memo.Set(keccakNew, i); + } + } + + memo.Length.Should().Be(GetExpectedSize(children.SetCount)); + + for (byte i = 0; i < NibbleSet.NibbleCount; i++) + { + if (children[i]) + { + memo.Exists(i).Should().BeTrue(); + memo.TryGetKeccak(i, out var k).Should().BeTrue(); + k.SequenceEqual(keccakNew).Should().BeTrue(); + } + } + } + + [Test] + public void Clear_get_operation() + { + Span raw = []; + Span workingMemory = new byte[RlpMemo.MaxSize]; + + Span keccak = new byte[Keccak.Size]; + keccak.Fill(0xFF); + + NibbleSet.Readonly children = new NibbleSet(0xA, 0xB, 0xC); + + var memo = new RlpMemo(raw); + memo = InsertKeccak(memo, children, keccak, workingMemory); + + for (byte i = 0; i < NibbleSet.NibbleCount; i++) + { + if (children[i]) + { + memo.Clear(i); + } + } + + memo.Length.Should().Be(GetExpectedSize(children.SetCount)); + + for (byte i = 0; i < NibbleSet.NibbleCount; i++) + { + if (children[i]) + { + memo.Exists(i).Should().BeTrue(); + memo.TryGetKeccak(i, out var k).Should().BeFalse(); + k.IsEmpty.Should().BeTrue(); + } + } + } + + [Test] + public void Delete_get_operation() + { + Span raw = []; + Span workingMemory = new byte[RlpMemo.MaxSize]; + + Span keccak = new byte[Keccak.Size]; + keccak.Fill(0xFF); + + NibbleSet.Readonly children = new NibbleSet(0xA, 0xB, 0xC); + + var memo = new RlpMemo(raw); + memo = InsertKeccak(memo, children, keccak, workingMemory); + + for (byte i = 0; i < NibbleSet.NibbleCount; i++) + { + if (children[i]) + { + memo = RlpMemo.Delete(memo, i, workingMemory); + } + } + + memo.Length.Should().Be(0); + + for (byte i = 0; i < NibbleSet.NibbleCount; i++) + { + if (children[i]) + { + memo.Exists(i).Should().BeFalse(); + memo.TryGetKeccak(i, out var k).Should().BeFalse(); + k.IsEmpty.Should().BeTrue(); + } + } + } + [Test] public void Random_delete() { @@ -55,11 +191,7 @@ public void Random_delete() children[child] = false; memo = RlpMemo.Delete(memo, child, raw); - var expectedLength = (i != NibbleSet.NibbleCount - 1) - ? RlpMemo.MaxSize - (i + 1) * Keccak.Size + NibbleSet.MaxByteSize - : 0; - - memo.Length.Should().Be(expectedLength); + memo.Length.Should().Be(GetExpectedSize(NibbleSet.NibbleCount - i - 1)); memo.Exists(child).Should().BeFalse(); memo.TryGetKeccak(child, out var keccak).Should().BeFalse(); keccak.IsEmpty.Should().BeTrue(); @@ -98,11 +230,7 @@ public void Random_insert() children[child] = true; memo = RlpMemo.Insert(memo, child, keccak, workingMemory); - var expectedLength = (i != NibbleSet.NibbleCount - 1) - ? (i + 1) * Keccak.Size + NibbleSet.MaxByteSize - : RlpMemo.MaxSize; - - memo.Length.Should().Be(expectedLength); + memo.Length.Should().Be(GetExpectedSize(i + 1)); memo.Exists(child).Should().BeTrue(); memo.TryGetKeccak(child, out var k).Should().BeTrue(); k.SequenceEqual(keccak).Should().BeTrue(); @@ -273,4 +401,30 @@ public void Large_random_operations(int numOperations) } } } + + private RlpMemo InsertKeccak(RlpMemo memo, NibbleSet.Readonly children, ReadOnlySpan keccak, Span workingMemory) + { + for (byte i = 0; i < NibbleSet.NibbleCount; i++) + { + if (children[i]) + { + memo = RlpMemo.Insert(memo, i, keccak, workingMemory); + } + } + + return memo; + } + + private static int GetExpectedSize(int numElements) + { + var size = numElements * Keccak.Size; + + // Empty and full memo doesn't contain the index. + if (size != 0 && size != RlpMemo.MaxSize) + { + size += NibbleSet.MaxByteSize; + } + + return size; + } } diff --git a/src/Paprika/Merkle/ComputeMerkleBehavior.cs b/src/Paprika/Merkle/ComputeMerkleBehavior.cs index 3947bd6f..13e68441 100644 --- a/src/Paprika/Merkle/ComputeMerkleBehavior.cs +++ b/src/Paprika/Merkle/ComputeMerkleBehavior.cs @@ -158,8 +158,8 @@ public Keccak GetStorageHash(IReadOnlyWorldState commit, in Keccak account, Nibb using var merkleData = prefixed.Get(parentKey); if (!merkleData.Span.IsEmpty) { - var leftover = Node.ReadFrom(out var parenType, out var leaf, out _, out var branch, merkleData.Span); - if (parenType == Node.Type.Branch && !ignoreCache) + var leftover = Node.ReadFrom(out var parentType, out var leaf, out _, out var branch, merkleData.Span); + if (parentType == Node.Type.Branch && !ignoreCache) { Span rlpMemoization = stackalloc byte[leftover.Length]; var memo = RlpMemo.Copy(leftover, rlpMemoization); @@ -384,13 +384,30 @@ public ReadOnlySpan InspectBeforeApply(in Key key, ReadOnlySpan data return data; } - if (key.Path.Length < SkipRlpMemoizationForTopLevelsCount && - Node.Header.Peek(data).NodeType == Node.Type.Branch) + var node = Node.Header.Peek(data).NodeType; + + if (node != Node.Type.Branch) + { + // Return data as is, either the node is not a branch or the memoization is not set for branches. + return data; + } + + if (key.Path.Length < SkipRlpMemoizationForTopLevelsCount) { // For State branches, omit top levels of RLP memoization return Node.Branch.GetOnlyBranchData(data); } + Node.Branch.ReadFrom(data, out var branch); + + // Optimization, omitting some of the branches to memoize. + // It omits only these with two children where the cost of the recompute is not big. + // To prevent an attack of spawning multiple levels of such branches, only even are skipped + if (branch.Children.SetCount == 2 && (key.Path.Length + key.StoragePath.Length) % 2 == 0) + { + return Node.Branch.GetOnlyBranchData(data); + } + return data; } diff --git a/src/Paprika/Merkle/RlpMemo.cs b/src/Paprika/Merkle/RlpMemo.cs index d45e36e9..1946ef39 100644 --- a/src/Paprika/Merkle/RlpMemo.cs +++ b/src/Paprika/Merkle/RlpMemo.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Numerics; using System.Runtime.CompilerServices; using Paprika.Crypto; using Paprika.Data; @@ -28,7 +27,8 @@ public RlpMemo(Span buffer) public void Set(ReadOnlySpan keccak, byte nibble) { var span = GetAtNibble(nibble); - Debug.Assert(!span.IsEmpty); + Debug.Assert(!span.IsEmpty, "Attempted to set a value on a non-existent index"); + Debug.Assert(keccak.Length == Keccak.Size && span.Length == Keccak.Size, "Attempted to set incorrect length"); keccak.CopyTo(span); } @@ -64,7 +64,7 @@ public bool Exists(byte nibble) return false; } - GetIndex(out var index); + var index = GetIndex(); return index[nibble]; } @@ -76,7 +76,7 @@ private Span GetAtNibble(byte nibble) return []; } - GetIndex(out var index); + var index = GetIndex(); // Check if the element exists if (!index[nibble]) @@ -85,25 +85,29 @@ private Span GetAtNibble(byte nibble) } var nibbleIndex = index.SetCountBefore(nibble) - 1; - return _buffer.Slice(nibbleIndex * Keccak.Size, Keccak.Size); + var dataStartOffset = (_buffer.Length != MaxSize) ? NibbleSet.MaxByteSize : 0; + return _buffer.Slice(dataStartOffset + nibbleIndex * Keccak.Size, Keccak.Size); } - private void GetIndex(out NibbleSet.Readonly index) + private NibbleSet.Readonly GetIndex() { // Extract the index bits. var indexLength = _buffer.Length % Keccak.Size; + NibbleSet.Readonly index; if (indexLength != 0) { - var bits = _buffer[^indexLength..]; - NibbleSet.Readonly.ReadFrom(bits, out index); + Debug.Assert(indexLength == NibbleSet.MaxByteSize, "Unexpected index length"); + NibbleSet.Readonly.ReadFrom(_buffer, out index); } else { - Debug.Assert(_buffer.Length is 0 or MaxSize); + Debug.Assert(_buffer.Length is 0 or MaxSize, "Only empty or full RlpMemo can have no index"); index = _buffer.IsEmpty ? NibbleSet.Readonly.None : NibbleSet.Readonly.All; } + + return index; } public static RlpMemo Copy(ReadOnlySpan from, scoped in Span to) @@ -115,89 +119,108 @@ public static RlpMemo Copy(ReadOnlySpan from, scoped in Span to) public static RlpMemo Insert(RlpMemo memo, byte nibble, ReadOnlySpan keccak, scoped in Span workingSet) { - memo.GetIndex(out var index); + var index = memo.GetIndex(); + + Debug.Assert(!index[nibble], "Attempted to insert a value into an already existing index"); - // Ensure that this element doesn't already exist. - Debug.Assert(!index[nibble]); + // Update the index and then compute the destination size for copying. + index = index.Set(nibble); + var size = ComputeRlpMemoSize(index); - // Compute the destination size for copying. - var size = (index.SetCount < NibbleSet.NibbleCount - 1) - ? (index.SetCount + 1) * Keccak.Size + NibbleSet.MaxByteSize - : MaxSize; - Debug.Assert(workingSet.Length >= size); + Debug.Assert(size >= (Keccak.Size + NibbleSet.MaxByteSize), "Unexpected size during insert"); + Debug.Assert(workingSet.Length >= size, "Insufficient destination length for insertion"); var span = workingSet[..size]; - // Compute the index of this nibble in the memo - var insertIndex = index.SetCountBefore(nibble); - var insertOffset = insertIndex * Keccak.Size; + // Start offsets for the data in the source (if any) and destination memo. + const int sourceStartOffset = NibbleSet.MaxByteSize; + var destStartOffset = (size != MaxSize) ? NibbleSet.MaxByteSize : 0; + + // Insert the new index header only if the destination memo is not full. + if (size != MaxSize) + { + index.WriteToWithLeftover(span); + } + + // Compute the index of this nibble in the destination memo + var insertIndex = index.SetCountBefore(nibble) - 1; + var insertOffset = destStartOffset + insertIndex * Keccak.Size; // Copy all the elements before the new element - if (insertOffset > 0) + if (insertOffset > destStartOffset) { - memo._buffer[..insertOffset].CopyTo(span); + memo._buffer[sourceStartOffset..insertOffset].CopyTo(span[destStartOffset..]); } - // Copy all the elements after the new element (except the index) + // Copy all the elements after the new element if (memo.Length > insertOffset) { - memo._buffer[insertOffset..^NibbleSet.MaxByteSize].CopyTo(span[(insertOffset + Keccak.Size)..]); + var sourceRemaining = sourceStartOffset + insertIndex * Keccak.Size; + memo._buffer[sourceRemaining..].CopyTo(span[(insertOffset + Keccak.Size)..]); } keccak.CopyTo(span[insertOffset..]); - // Update the index. - index = index.Set(nibble); - - if (size != MaxSize) - { - index.WriteToWithLeftover(span[^NibbleSet.MaxByteSize..]); - } - return new RlpMemo(span); } public static RlpMemo Delete(RlpMemo memo, byte nibble, scoped in Span workingSet) { - memo.GetIndex(out var index); + var index = memo.GetIndex(); + + Debug.Assert(index[nibble], "Attempted to delete a non-existing index"); - // Ensure that this element isn't already deleted. - Debug.Assert(index[nibble]); + // Update the index and then compute the destination size for copying. + index = index.Remove(nibble); + var size = ComputeRlpMemoSize(index); - // Compute the destination size for copying. - var size = (index.SetCount < NibbleSet.NibbleCount) - ? memo.Length - Keccak.Size - : memo.Length - Keccak.Size + NibbleSet.MaxByteSize; + Debug.Assert(size is < MaxSize and >= 0, "Unexpected size during deletion"); + Debug.Assert(workingSet.Length >= size, "Insufficient destination length for deletion"); - if (size <= NibbleSet.MaxByteSize) + if (size == 0) { - // Empty RlpMemo after this delete operation. - size = 0; + // Return empty RlpMemo return new RlpMemo(workingSet[..size]); } var span = workingSet[..size]; + // Start offsets for the data in the source and destination memo. + var sourceStartOffset = (memo.Length != MaxSize) ? NibbleSet.MaxByteSize : 0; + const int destStartOffset = NibbleSet.MaxByteSize; + + // Since the destination memo is neither empty nor full here, it must always contain the index header. + index.WriteToWithLeftover(span); + // Compute the index of this nibble in the memo - var deleteIndex = index.SetCountBefore(nibble) - 1; - var deleteOffset = deleteIndex * Keccak.Size; + var deleteIndex = index.SetCountBefore(nibble); + var deleteOffset = sourceStartOffset + deleteIndex * Keccak.Size; // Copy all the elements before the deleted element - if (deleteOffset > 0) + if (deleteOffset > sourceStartOffset) { - memo._buffer[..deleteOffset].CopyTo(span); + memo._buffer[sourceStartOffset..deleteOffset].CopyTo(span[destStartOffset..]); } - // Copy all the elements after the deleted element (except the index) + // Copy all the elements after the deleted element if (memo.Length > (deleteOffset + Keccak.Size)) { - memo._buffer[(deleteOffset + Keccak.Size)..^NibbleSet.MaxByteSize].CopyTo(span[deleteOffset..]); + memo._buffer[(deleteOffset + Keccak.Size)..].CopyTo(span[(deleteOffset + (destStartOffset - sourceStartOffset))..]); } - // Update the index. - index = index.Remove(nibble); - index.WriteToWithLeftover(span[^NibbleSet.MaxByteSize..]); - return new RlpMemo(span); } + + private static int ComputeRlpMemoSize(NibbleSet.Readonly index) + { + var size = index.SetCount * Keccak.Size; + + // Add extra space for the index. Empty and full memo doesn't contain the index. + if (size != 0 && size != MaxSize) + { + size += NibbleSet.MaxByteSize; + } + + return size; + } } From 53dca766499fc57c54179574f1144a057b13d94f Mon Sep 17 00:00:00 2001 From: Diptanshu Kakwani Date: Tue, 14 Jan 2025 19:48:28 +0530 Subject: [PATCH 09/15] improve test to catch failures --- src/Paprika.Tests/Merkle/RlpMemoTests.cs | 222 ++++++++++++----------- src/Paprika/Merkle/RlpMemo.cs | 22 +-- 2 files changed, 129 insertions(+), 115 deletions(-) diff --git a/src/Paprika.Tests/Merkle/RlpMemoTests.cs b/src/Paprika.Tests/Merkle/RlpMemoTests.cs index c0d1213e..8fdbbaed 100644 --- a/src/Paprika.Tests/Merkle/RlpMemoTests.cs +++ b/src/Paprika.Tests/Merkle/RlpMemoTests.cs @@ -1,15 +1,11 @@ using FluentAssertions; -using Paprika.Chain; using Paprika.Crypto; -using Paprika.Data; using Paprika.Merkle; -using Paprika.RLP; namespace Paprika.Tests.Merkle; public class RlpMemoTests { - // All the write operations on RlpMemo private enum RlpMemoOperation { @@ -22,16 +18,11 @@ private enum RlpMemoOperation [Test] public void Insert_get_operation() { - Span raw = []; Span workingMemory = new byte[RlpMemo.MaxSize]; - - Span keccak = new byte[Keccak.Size]; - keccak.Fill(0xFF); - NibbleSet.Readonly children = new NibbleSet(0xA, 0xB, 0xC); + var memo = new RlpMemo([]); - var memo = new RlpMemo(raw); - memo = InsertKeccak(memo, children, keccak, workingMemory); + InsertRandomKeccak(ref memo, children, out var data, workingMemory); memo.Length.Should().Be(GetExpectedSize(children.SetCount)); @@ -41,33 +32,33 @@ public void Insert_get_operation() { memo.Exists(i).Should().BeTrue(); memo.TryGetKeccak(i, out var k).Should().BeTrue(); - k.SequenceEqual(keccak).Should().BeTrue(); + k.SequenceEqual(data[i].Span).Should().BeTrue(); } } + + CompareMemoAndDict(memo, data); } [Test] public void Set_get_operation() { - Span raw = []; Span workingMemory = new byte[RlpMemo.MaxSize]; - - Span keccak = new byte[Keccak.Size]; - keccak.Fill(0xFF); - - Span keccakNew = new byte[Keccak.Size]; - keccakNew.Fill(0xAA); - NibbleSet.Readonly children = new NibbleSet(0xA, 0xB, 0xC); + var memo = new RlpMemo([]); - var memo = new RlpMemo(raw); - memo = InsertKeccak(memo, children, keccak, workingMemory); + InsertRandomKeccak(ref memo, children, out var data, workingMemory); + + memo.Length.Should().Be(GetExpectedSize(children.SetCount)); + + var random = new Random(13); for (byte i = 0; i < NibbleSet.NibbleCount; i++) { if (children[i]) { - memo.Set(keccakNew, i); + data[i] = random.NextKeccak(); + memo.Set(data[i].Span, i); + CompareMemoAndDict(memo, data); } } @@ -79,30 +70,31 @@ public void Set_get_operation() { memo.Exists(i).Should().BeTrue(); memo.TryGetKeccak(i, out var k).Should().BeTrue(); - k.SequenceEqual(keccakNew).Should().BeTrue(); + k.SequenceEqual(data[i].Span).Should().BeTrue(); } } + + CompareMemoAndDict(memo, data); } [Test] public void Clear_get_operation() { - Span raw = []; Span workingMemory = new byte[RlpMemo.MaxSize]; - - Span keccak = new byte[Keccak.Size]; - keccak.Fill(0xFF); - NibbleSet.Readonly children = new NibbleSet(0xA, 0xB, 0xC); + var memo = new RlpMemo([]); - var memo = new RlpMemo(raw); - memo = InsertKeccak(memo, children, keccak, workingMemory); + InsertRandomKeccak(ref memo, children, out var data, workingMemory); + + memo.Length.Should().Be(GetExpectedSize(children.SetCount)); for (byte i = 0; i < NibbleSet.NibbleCount; i++) { if (children[i]) { + data[i] = Keccak.Zero; memo.Clear(i); + CompareMemoAndDict(memo, data); } } @@ -117,27 +109,28 @@ public void Clear_get_operation() k.IsEmpty.Should().BeTrue(); } } + + CompareMemoAndDict(memo, data); } [Test] public void Delete_get_operation() { - Span raw = []; Span workingMemory = new byte[RlpMemo.MaxSize]; - - Span keccak = new byte[Keccak.Size]; - keccak.Fill(0xFF); - NibbleSet.Readonly children = new NibbleSet(0xA, 0xB, 0xC); + var memo = new RlpMemo([]); - var memo = new RlpMemo(raw); - memo = InsertKeccak(memo, children, keccak, workingMemory); + InsertRandomKeccak(ref memo, children, out var data, workingMemory); + + memo.Length.Should().Be(GetExpectedSize(children.SetCount)); for (byte i = 0; i < NibbleSet.NibbleCount; i++) { if (children[i]) { + data.Remove(i); memo = RlpMemo.Delete(memo, i, workingMemory); + CompareMemoAndDict(memo, data); } } @@ -152,6 +145,8 @@ public void Delete_get_operation() k.IsEmpty.Should().BeTrue(); } } + + CompareMemoAndDict(memo, data); } [Test] @@ -240,64 +235,65 @@ public void Random_insert() } [Test] - public void In_place_update() + public void Grow_shrink() { - Span raw = stackalloc byte[RlpMemo.MaxSize]; - var children = new NibbleSet(); + Span raw = new byte[RlpMemo.MaxSize]; + var memo = new RlpMemo([]); + var random = new Random(13); + var data = new Dictionary(); - for (var i = 0; i < RlpMemo.MaxSize - NibbleSet.MaxByteSize; i++) + // Grow the RLPMemo + for (byte i = 0; i < NibbleSet.NibbleCount; i++) { - raw[i] = (byte)(i & 0xFF); + data[i] = random.NextKeccak(); + memo = RlpMemo.Insert(memo, i, data[i].Span, raw[..GetExpectedSize(i + 1)]); + CompareMemoAndDict(memo, data); } - // Set all the index bits at the end. - for (var i = RlpMemo.MaxSize - 1; i >= RlpMemo.MaxSize - NibbleSet.MaxByteSize; i--) + memo.Length.Should().Be(RlpMemo.MaxSize); + + // Shrink the RLPMemo + for (byte i = 0; i < NibbleSet.NibbleCount; i++) { - raw[i] = 0xFF; + memo = RlpMemo.Delete(memo, i, raw[..GetExpectedSize(NibbleSet.NibbleCount - i - 1)]); + data.Remove(i); + CompareMemoAndDict(memo, data); } - for (var i = 0; i < NibbleSet.NibbleCount; i++) + memo.Length.Should().Be(0); + } + + [Test] + public void Shrink_grow() + { + Span raw = stackalloc byte[RlpMemo.MaxSize]; + var random = new Random(13); + var data = new Dictionary(); + + for (byte i = 0; i < NibbleSet.NibbleCount; i++) { - children[(byte)i] = true; + data[i] = random.NextKeccak(); + data[i].Span.CopyTo(raw[(i * Keccak.Size)..]); } var memo = new RlpMemo(raw); - // Delete each child and the corresponding keccak + // Shrink the RLPMemo for (byte i = 0; i < NibbleSet.NibbleCount; i++) { - children[i] = false; - memo = RlpMemo.Delete(memo, i, raw); - - var expectedLength = (i != NibbleSet.NibbleCount - 1) - ? RlpMemo.MaxSize - (i + 1) * Keccak.Size + NibbleSet.MaxByteSize - : 0; - - memo.Length.Should().Be(expectedLength); - memo.Exists(i).Should().BeFalse(); - memo.TryGetKeccak(i, out var k).Should().BeFalse(); - k.IsEmpty.Should().BeTrue(); + memo = RlpMemo.Delete(memo, i, raw[..GetExpectedSize(NibbleSet.NibbleCount - i - 1)]); + data.Remove(i); + CompareMemoAndDict(memo, data); } memo.Length.Should().Be(0); - // Try adding back the children and the corresponding keccak - Span keccak = stackalloc byte[Keccak.Size]; - keccak.Fill(0xFF); - + // Grow the RLPMemo for (byte i = 0; i < NibbleSet.NibbleCount; i++) { - children[i] = true; - memo = RlpMemo.Insert(memo, i, keccak, raw); - - var expectedLength = (i != NibbleSet.NibbleCount - 1) - ? (i + 1) * Keccak.Size + NibbleSet.MaxByteSize - : RlpMemo.MaxSize; - - memo.Length.Should().Be(expectedLength); - memo.Exists(i).Should().BeTrue(); - memo.TryGetKeccak(i, out var k).Should().BeTrue(); - k.SequenceEqual(keccak).Should().BeTrue(); + data[i] = random.NextKeccak(); + memo = RlpMemo.Insert(memo, i, data[i].Span, raw[..GetExpectedSize(i + 1)]); + CompareMemoAndDict(memo, data); } memo.Length.Should().Be(RlpMemo.MaxSize); @@ -324,30 +320,18 @@ public void Copy_data() [TestCase(100_000)] public void Large_random_operations(int numOperations) { - Span raw = stackalloc byte[RlpMemo.MaxSize]; - var children = new NibbleSet(); - - Span keccak = new byte[Keccak.Size]; - keccak.Fill(0xFF); - - for (var i = 0; i < RlpMemo.MaxSize - NibbleSet.MaxByteSize; i++) - { - raw[i] = (byte)(i & 0xFF); - } - - // Set all the index bits at the end. - for (var i = RlpMemo.MaxSize - 1; i >= RlpMemo.MaxSize - NibbleSet.MaxByteSize; i--) - { - raw[i] = 0xFF; - } + Span workingSet = new byte[RlpMemo.MaxSize]; + var rand = new Random(13); + var memo = new RlpMemo([]); + NibbleSet children = new NibbleSet(); - for (var i = 0; i < NibbleSet.NibbleCount; i++) + for (byte i = 0; i < NibbleSet.NibbleCount; i++) { - children[(byte)i] = true; + children[i] = true; } - var memo = new RlpMemo(raw); - var rand = new Random(13); + // Start with full RLPMemo. + InsertRandomKeccak(ref memo, children, out var data, workingSet); for (var i = 0; i < numOperations; i++) { @@ -359,16 +343,18 @@ public void Large_random_operations(int numOperations) case RlpMemoOperation.Set: if (memo.Exists(child)) { - memo.Set(keccak, child); + data[child] = rand.NextKeccak(); + memo.Set(data[child].Span, child); memo.TryGetKeccak(child, out var k).Should().BeTrue(); - k.SequenceEqual(keccak).Should().BeTrue(); + k.SequenceEqual(data[child].Span).Should().BeTrue(); } break; case RlpMemoOperation.Clear: if (memo.Exists(child)) { + data[child] = Keccak.Zero; memo.Clear(child); memo.TryGetKeccak(child, out var k).Should().BeFalse(); @@ -380,7 +366,8 @@ public void Large_random_operations(int numOperations) if (memo.Exists(child)) { children[child] = false; - memo = RlpMemo.Delete(memo, child, raw); + data.Remove(child); + memo = RlpMemo.Delete(memo, child, workingSet); memo.TryGetKeccak(child, out var k).Should().BeFalse(); k.IsEmpty.Should().BeTrue(); @@ -391,28 +378,55 @@ public void Large_random_operations(int numOperations) if (!memo.Exists(child)) { children[child] = true; - memo = RlpMemo.Insert(memo, child, keccak, raw); + data[child] = rand.NextKeccak(); + memo = RlpMemo.Insert(memo, child, data[child].Span, workingSet); memo.TryGetKeccak(child, out var k).Should().BeTrue(); - k.SequenceEqual(keccak).Should().BeTrue(); + k.SequenceEqual(data[child].Span).Should().BeTrue(); } - break; } + + CompareMemoAndDict(memo, data); } } - private RlpMemo InsertKeccak(RlpMemo memo, NibbleSet.Readonly children, ReadOnlySpan keccak, Span workingMemory) + private static void InsertRandomKeccak(ref RlpMemo memo, NibbleSet.Readonly children, out Dictionary data + , Span workingMemory) { + data = new Dictionary(); + var random = new Random(13); + for (byte i = 0; i < NibbleSet.NibbleCount; i++) { if (children[i]) { - memo = RlpMemo.Insert(memo, i, keccak, workingMemory); + data[i] = random.NextKeccak(); + memo = RlpMemo.Insert(memo, i, data[i].Span, workingMemory); + } + } + } + + private static void CompareMemoAndDict(RlpMemo memo, Dictionary data) + { + foreach (var child in data) + { + memo.Exists(child.Key).Should().BeTrue(); + + if (child.Value == Keccak.Zero) + { + memo.TryGetKeccak(child.Key, out var k).Should().BeFalse(); + k.IsEmpty.Should().BeTrue(); + } + else + { + memo.TryGetKeccak(child.Key, out var k).Should().BeTrue(); + k.SequenceEqual(child.Value.Span).Should().BeTrue(); } + } - return memo; + memo.Length.Should().Be(GetExpectedSize(data.Count)); } private static int GetExpectedSize(int numElements) diff --git a/src/Paprika/Merkle/RlpMemo.cs b/src/Paprika/Merkle/RlpMemo.cs index 1946ef39..b8ff461f 100644 --- a/src/Paprika/Merkle/RlpMemo.cs +++ b/src/Paprika/Merkle/RlpMemo.cs @@ -47,7 +47,7 @@ public bool TryGetKeccak(byte nibble, out ReadOnlySpan keccak) { var span = GetAtNibble(nibble); - if (span.IndexOfAnyExcept((byte)0) >= 0) + if (!span.IsEmpty && span.IndexOfAnyExcept((byte)0) >= 0) { keccak = span; return true; @@ -127,7 +127,7 @@ public static RlpMemo Insert(RlpMemo memo, byte nibble, ReadOnlySpan kecca index = index.Set(nibble); var size = ComputeRlpMemoSize(index); - Debug.Assert(size >= (Keccak.Size + NibbleSet.MaxByteSize), "Unexpected size during insert"); + Debug.Assert(size is >= Keccak.Size + NibbleSet.MaxByteSize and <= MaxSize, "Unexpected size during insert"); Debug.Assert(workingSet.Length >= size, "Insufficient destination length for insertion"); var span = workingSet[..size]; @@ -136,12 +136,6 @@ public static RlpMemo Insert(RlpMemo memo, byte nibble, ReadOnlySpan kecca const int sourceStartOffset = NibbleSet.MaxByteSize; var destStartOffset = (size != MaxSize) ? NibbleSet.MaxByteSize : 0; - // Insert the new index header only if the destination memo is not full. - if (size != MaxSize) - { - index.WriteToWithLeftover(span); - } - // Compute the index of this nibble in the destination memo var insertIndex = index.SetCountBefore(nibble) - 1; var insertOffset = destStartOffset + insertIndex * Keccak.Size; @@ -161,6 +155,12 @@ public static RlpMemo Insert(RlpMemo memo, byte nibble, ReadOnlySpan kecca keccak.CopyTo(span[insertOffset..]); + // Insert the new index header only if the destination memo is not full. + if (size != MaxSize) + { + index.WriteToWithLeftover(span); + } + return new RlpMemo(span); } @@ -189,9 +189,6 @@ public static RlpMemo Delete(RlpMemo memo, byte nibble, scoped in Span wor var sourceStartOffset = (memo.Length != MaxSize) ? NibbleSet.MaxByteSize : 0; const int destStartOffset = NibbleSet.MaxByteSize; - // Since the destination memo is neither empty nor full here, it must always contain the index header. - index.WriteToWithLeftover(span); - // Compute the index of this nibble in the memo var deleteIndex = index.SetCountBefore(nibble); var deleteOffset = sourceStartOffset + deleteIndex * Keccak.Size; @@ -208,6 +205,9 @@ public static RlpMemo Delete(RlpMemo memo, byte nibble, scoped in Span wor memo._buffer[(deleteOffset + Keccak.Size)..].CopyTo(span[(deleteOffset + (destStartOffset - sourceStartOffset))..]); } + // Since the destination memo is neither empty nor full here, it must always contain the index header. + index.WriteToWithLeftover(span); + return new RlpMemo(span); } From 241be556cd8844bbb1a143d373a4620e28d25a2d Mon Sep 17 00:00:00 2001 From: Diptanshu Kakwani Date: Tue, 14 Jan 2025 19:52:26 +0530 Subject: [PATCH 10/15] minor whitespace fixes in test --- src/Paprika.Tests/Merkle/RlpMemoTests.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Paprika.Tests/Merkle/RlpMemoTests.cs b/src/Paprika.Tests/Merkle/RlpMemoTests.cs index 8fdbbaed..069409f3 100644 --- a/src/Paprika.Tests/Merkle/RlpMemoTests.cs +++ b/src/Paprika.Tests/Merkle/RlpMemoTests.cs @@ -49,7 +49,7 @@ public void Set_get_operation() InsertRandomKeccak(ref memo, children, out var data, workingMemory); memo.Length.Should().Be(GetExpectedSize(children.SetCount)); - + var random = new Random(13); for (byte i = 0; i < NibbleSet.NibbleCount; i++) @@ -421,9 +421,8 @@ private static void CompareMemoAndDict(RlpMemo memo, Dictionary da else { memo.TryGetKeccak(child.Key, out var k).Should().BeTrue(); - k.SequenceEqual(child.Value.Span).Should().BeTrue(); + k.SequenceEqual(child.Value.Span).Should().BeTrue(); } - } memo.Length.Should().Be(GetExpectedSize(data.Count)); From abafbc9a982ed2b582087a690fff8ca24adbbfd0 Mon Sep 17 00:00:00 2001 From: Diptanshu Kakwani Date: Tue, 14 Jan 2025 21:24:53 +0530 Subject: [PATCH 11/15] passing test + index at end --- src/Paprika/Merkle/RlpMemo.cs | 47 ++++++++++++++--------------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/src/Paprika/Merkle/RlpMemo.cs b/src/Paprika/Merkle/RlpMemo.cs index b8ff461f..190673d8 100644 --- a/src/Paprika/Merkle/RlpMemo.cs +++ b/src/Paprika/Merkle/RlpMemo.cs @@ -85,8 +85,7 @@ private Span GetAtNibble(byte nibble) } var nibbleIndex = index.SetCountBefore(nibble) - 1; - var dataStartOffset = (_buffer.Length != MaxSize) ? NibbleSet.MaxByteSize : 0; - return _buffer.Slice(dataStartOffset + nibbleIndex * Keccak.Size, Keccak.Size); + return _buffer.Slice(nibbleIndex * Keccak.Size, Keccak.Size); } private NibbleSet.Readonly GetIndex() @@ -98,7 +97,8 @@ private NibbleSet.Readonly GetIndex() if (indexLength != 0) { Debug.Assert(indexLength == NibbleSet.MaxByteSize, "Unexpected index length"); - NibbleSet.Readonly.ReadFrom(_buffer, out index); + var bits = _buffer[^indexLength..]; + NibbleSet.Readonly.ReadFrom(bits, out index); } else { @@ -132,33 +132,28 @@ public static RlpMemo Insert(RlpMemo memo, byte nibble, ReadOnlySpan kecca var span = workingSet[..size]; - // Start offsets for the data in the source (if any) and destination memo. - const int sourceStartOffset = NibbleSet.MaxByteSize; - var destStartOffset = (size != MaxSize) ? NibbleSet.MaxByteSize : 0; - // Compute the index of this nibble in the destination memo var insertIndex = index.SetCountBefore(nibble) - 1; - var insertOffset = destStartOffset + insertIndex * Keccak.Size; + var insertOffset = insertIndex * Keccak.Size; // Copy all the elements before the new element - if (insertOffset > destStartOffset) + if (insertOffset > 0) { - memo._buffer[sourceStartOffset..insertOffset].CopyTo(span[destStartOffset..]); + memo._buffer[..insertOffset].CopyTo(span); } - // Copy all the elements after the new element - if (memo.Length > insertOffset) + // Copy all the elements after the new element (except the index) + if (memo.Length > insertOffset + NibbleSet.MaxByteSize) { - var sourceRemaining = sourceStartOffset + insertIndex * Keccak.Size; - memo._buffer[sourceRemaining..].CopyTo(span[(insertOffset + Keccak.Size)..]); + memo._buffer[insertOffset..^NibbleSet.MaxByteSize].CopyTo(span[(insertOffset + Keccak.Size)..]); } keccak.CopyTo(span[insertOffset..]); - // Insert the new index header only if the destination memo is not full. + // Insert the new index only if the destination memo is not full. if (size != MaxSize) { - index.WriteToWithLeftover(span); + index.WriteToWithLeftover(span[^NibbleSet.MaxByteSize..]); } return new RlpMemo(span); @@ -174,7 +169,7 @@ public static RlpMemo Delete(RlpMemo memo, byte nibble, scoped in Span wor index = index.Remove(nibble); var size = ComputeRlpMemoSize(index); - Debug.Assert(size is < MaxSize and >= 0, "Unexpected size during deletion"); + Debug.Assert(size is >= 0 and < MaxSize, "Unexpected size during deletion"); Debug.Assert(workingSet.Length >= size, "Insufficient destination length for deletion"); if (size == 0) @@ -185,28 +180,24 @@ public static RlpMemo Delete(RlpMemo memo, byte nibble, scoped in Span wor var span = workingSet[..size]; - // Start offsets for the data in the source and destination memo. - var sourceStartOffset = (memo.Length != MaxSize) ? NibbleSet.MaxByteSize : 0; - const int destStartOffset = NibbleSet.MaxByteSize; - // Compute the index of this nibble in the memo var deleteIndex = index.SetCountBefore(nibble); - var deleteOffset = sourceStartOffset + deleteIndex * Keccak.Size; + var deleteOffset = deleteIndex * Keccak.Size; // Copy all the elements before the deleted element - if (deleteOffset > sourceStartOffset) + if (deleteOffset > 0) { - memo._buffer[sourceStartOffset..deleteOffset].CopyTo(span[destStartOffset..]); + memo._buffer[..deleteOffset].CopyTo(span); } // Copy all the elements after the deleted element - if (memo.Length > (deleteOffset + Keccak.Size)) + if (memo.Length > deleteOffset + Keccak.Size + NibbleSet.MaxByteSize) { - memo._buffer[(deleteOffset + Keccak.Size)..].CopyTo(span[(deleteOffset + (destStartOffset - sourceStartOffset))..]); + memo._buffer[(deleteOffset + Keccak.Size)..].CopyTo(span[deleteOffset..]); } - // Since the destination memo is neither empty nor full here, it must always contain the index header. - index.WriteToWithLeftover(span); + // Since the destination memo is neither empty nor full here, it must always contain the index. + index.WriteToWithLeftover(span[^NibbleSet.MaxByteSize..]); return new RlpMemo(span); } From d435533e3ea0854ba841f601680cea7791d35ee2 Mon Sep 17 00:00:00 2001 From: Diptanshu Kakwani Date: Wed, 15 Jan 2025 13:07:44 +0530 Subject: [PATCH 12/15] improve dictionary/memo comparison --- src/Paprika.Tests/Merkle/RlpMemoTests.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Paprika.Tests/Merkle/RlpMemoTests.cs b/src/Paprika.Tests/Merkle/RlpMemoTests.cs index 069409f3..14d0556b 100644 --- a/src/Paprika.Tests/Merkle/RlpMemoTests.cs +++ b/src/Paprika.Tests/Merkle/RlpMemoTests.cs @@ -409,6 +409,7 @@ private static void InsertRandomKeccak(ref RlpMemo memo, NibbleSet.Readonly chil private static void CompareMemoAndDict(RlpMemo memo, Dictionary data) { + // All the elements in dictionary should be in the memo. foreach (var child in data) { memo.Exists(child.Key).Should().BeTrue(); @@ -425,6 +426,25 @@ private static void CompareMemoAndDict(RlpMemo memo, Dictionary da } } + // All the elements in the memo should be in the dictionary. + for (byte i = 0; i < NibbleSet.NibbleCount; i++) + { + if (memo.Exists(i)) + { + data.ContainsKey(i).Should().BeTrue(); + + if (memo.TryGetKeccak(i, out var k)) + { + k.SequenceEqual(data[i].Span).Should().BeTrue(); + } + else + { + k.IsEmpty.Should().BeTrue(); + Keccak.Zero.Span.SequenceEqual(data[i].Span).Should().BeTrue(); + } + } + } + memo.Length.Should().Be(GetExpectedSize(data.Count)); } From 58b1af3ba0a48443153415378ea393e09cb7c64a Mon Sep 17 00:00:00 2001 From: Diptanshu Kakwani Date: Mon, 20 Jan 2025 14:10:50 +0530 Subject: [PATCH 13/15] add test case for keccak->rlp modification --- src/Paprika.Tests/Merkle/RlpMemoTests.cs | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/Paprika.Tests/Merkle/RlpMemoTests.cs b/src/Paprika.Tests/Merkle/RlpMemoTests.cs index 14d0556b..285131be 100644 --- a/src/Paprika.Tests/Merkle/RlpMemoTests.cs +++ b/src/Paprika.Tests/Merkle/RlpMemoTests.cs @@ -1,6 +1,8 @@ using FluentAssertions; using Paprika.Crypto; using Paprika.Merkle; +using Paprika.Data; +using Paprika.Chain; namespace Paprika.Tests.Merkle; @@ -315,6 +317,44 @@ public void Copy_data() memo.Raw.SequenceEqual(raw).Should().BeTrue(); } + [Test] + public void Keccak_to_rlp_children() + { + NibbleSet.Readonly children = new NibbleSet(1, 2); + Span workingMemory = new byte[RlpMemo.MaxSize]; + + // Create memo with random keccak for the corresponding children + var memo = new RlpMemo([]); + InsertRandomKeccak(ref memo, children, out _, workingMemory); + + // create E->B->L + // ->L + // leaves without any key and very small value cause to be inlined in branch + // encoded branch rlp is also < 32 bytes which causes it to be encoded as RLP in extension node + Keccak storageKey1 = + new Keccak(Convert.FromHexString("ccccccccccccccccccccddddddddddddddddeeeeeeeeeeeeeeeeeeeeeeeeeeb1")); + Keccak storageKey2 = + new Keccak(Convert.FromHexString("ccccccccccccccccccccddddddddddddddddeeeeeeeeeeeeeeeeeeeeeeeeeeb2")); + + var commit = new Commit(); + commit.Set(Key.Account(Values.Key0), + new Account(0, 1).WriteTo(stackalloc byte[Paprika.Account.MaxByteCount])); + commit.Set(Key.StorageCell(NibblePath.FromKey(Values.Key0), storageKey1), new byte[] { 1, 2, 3 }); + commit.Set(Key.StorageCell(NibblePath.FromKey(Values.Key0), storageKey2), new byte[] { 10, 20, 30 }); + + using var merkle = new ComputeMerkleBehavior(); + + merkle.BeforeCommit(commit, CacheBudget.Options.None.Build()); + + // Update the branch with memo + commit.SetBranch( + Key.Raw(NibblePath.FromKey(Values.Key0), DataType.Merkle, + NibblePath.Parse("CCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEEEEEEEEEEEB")), children, + memo.Raw); + + merkle.RecalculateStorageTrie(commit, Values.Key0, CacheBudget.Options.None.Build()); + } + [TestCase(1000)] [TestCase(10_000)] [TestCase(100_000)] From ee811e1040fb27460542feba39b8b6643a0b9b19 Mon Sep 17 00:00:00 2001 From: Diptanshu Kakwani Date: Mon, 20 Jan 2025 14:13:29 +0530 Subject: [PATCH 14/15] minor format changes in the new test --- src/Paprika.Tests/Merkle/RlpMemoTests.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Paprika.Tests/Merkle/RlpMemoTests.cs b/src/Paprika.Tests/Merkle/RlpMemoTests.cs index 285131be..44740165 100644 --- a/src/Paprika.Tests/Merkle/RlpMemoTests.cs +++ b/src/Paprika.Tests/Merkle/RlpMemoTests.cs @@ -320,6 +320,7 @@ public void Copy_data() [Test] public void Keccak_to_rlp_children() { + const string prefix = "ccccccccccccccccccccddddddddddddddddeeeeeeeeeeeeeeeeeeeeeeeeeeb"; NibbleSet.Readonly children = new NibbleSet(1, 2); Span workingMemory = new byte[RlpMemo.MaxSize]; @@ -332,9 +333,9 @@ public void Keccak_to_rlp_children() // leaves without any key and very small value cause to be inlined in branch // encoded branch rlp is also < 32 bytes which causes it to be encoded as RLP in extension node Keccak storageKey1 = - new Keccak(Convert.FromHexString("ccccccccccccccccccccddddddddddddddddeeeeeeeeeeeeeeeeeeeeeeeeeeb1")); + new Keccak(Convert.FromHexString(prefix + "1")); Keccak storageKey2 = - new Keccak(Convert.FromHexString("ccccccccccccccccccccddddddddddddddddeeeeeeeeeeeeeeeeeeeeeeeeeeb2")); + new Keccak(Convert.FromHexString(prefix + "2")); var commit = new Commit(); commit.Set(Key.Account(Values.Key0), @@ -347,9 +348,7 @@ public void Keccak_to_rlp_children() merkle.BeforeCommit(commit, CacheBudget.Options.None.Build()); // Update the branch with memo - commit.SetBranch( - Key.Raw(NibblePath.FromKey(Values.Key0), DataType.Merkle, - NibblePath.Parse("CCCCCCCCCCCCCCCCCCCCDDDDDDDDDDDDDDDDEEEEEEEEEEEEEEEEEEEEEEEEEEB")), children, + commit.SetBranch(Key.Raw(NibblePath.FromKey(Values.Key0), DataType.Merkle, NibblePath.Parse(prefix)), children, memo.Raw); merkle.RecalculateStorageTrie(commit, Values.Key0, CacheBudget.Options.None.Build()); From 3a9660551d5e01160c51bb80a9cd08e0dfa3e360 Mon Sep 17 00:00:00 2001 From: Diptanshu Kakwani Date: Tue, 21 Jan 2025 14:33:54 +0530 Subject: [PATCH 15/15] fix issues --- src/Paprika.Tests/Merkle/RlpMemoTests.cs | 2 +- src/Paprika/Chain/Blockchain.cs | 12 ++++- src/Paprika/Merkle/ComputeMerkleBehavior.cs | 60 ++++++++++++--------- 3 files changed, 47 insertions(+), 27 deletions(-) diff --git a/src/Paprika.Tests/Merkle/RlpMemoTests.cs b/src/Paprika.Tests/Merkle/RlpMemoTests.cs index 44740165..857b8f7c 100644 --- a/src/Paprika.Tests/Merkle/RlpMemoTests.cs +++ b/src/Paprika.Tests/Merkle/RlpMemoTests.cs @@ -320,7 +320,6 @@ public void Copy_data() [Test] public void Keccak_to_rlp_children() { - const string prefix = "ccccccccccccccccccccddddddddddddddddeeeeeeeeeeeeeeeeeeeeeeeeeeb"; NibbleSet.Readonly children = new NibbleSet(1, 2); Span workingMemory = new byte[RlpMemo.MaxSize]; @@ -332,6 +331,7 @@ public void Keccak_to_rlp_children() // ->L // leaves without any key and very small value cause to be inlined in branch // encoded branch rlp is also < 32 bytes which causes it to be encoded as RLP in extension node + const string prefix = "ccccccccccccccccccccddddddddddddddddeeeeeeeeeeeeeeeeeeeeeeeeeeb"; Keccak storageKey1 = new Keccak(Convert.FromHexString(prefix + "1")); Keccak storageKey2 = diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index 122f390b..87a7c858 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -2014,8 +2014,18 @@ public void CreateMerkleBranch(in Keccak account, in NibblePath storagePath, byt { set[childNibbles[i]] = true; if (childHashes[i] != Keccak.Zero) - memo.Set(childHashes[i].Span, childNibbles[i]); + { + if (memo.Exists(childNibbles[i])) + { + memo.Set(childHashes[i].Span, childNibbles[i]); + } + else + { + memo = RlpMemo.Insert(memo, childNibbles[i], childHashes[i].Span, rlpMemoization); + } + } } + _current.SetBranch(key, set, memo.Raw, persist ? EntryType.Persistent : EntryType.Proof); } diff --git a/src/Paprika/Merkle/ComputeMerkleBehavior.cs b/src/Paprika/Merkle/ComputeMerkleBehavior.cs index 13e68441..b4f6fc7d 100644 --- a/src/Paprika/Merkle/ComputeMerkleBehavior.cs +++ b/src/Paprika/Merkle/ComputeMerkleBehavior.cs @@ -586,6 +586,7 @@ private void EncodeBranch(scoped in Key key, scoped in ComputeContext ctx, scope var rlp = buffer.Span[..rlpSlice]; var rlpMemoization = buffer.Span.Slice(rlpSlice, RlpMemo.MaxSize); var memoizedUpdated = false; + var memoizedUpdatedUsingBuffer = false; RlpMemo memo = default; @@ -642,18 +643,20 @@ private void EncodeBranch(scoped in Key key, scoped in ComputeContext ctx, scope stream.Write(keccakOrRlp.Span); } - if (memoize) + // Memoize all the keccak values. + if (memoize && keccakOrRlp.DataType == KeccakOrRlp.Type.Keccak) { - memoizedUpdated = true; - if (memo.Exists(i)) { memo.Set(keccakOrRlp.Span, i); } - else if (keccakOrRlp.DataType == KeccakOrRlp.Type.Keccak) + else { memo = RlpMemo.Insert(memo, i, keccakOrRlp.Span, rlpMemoization); + memoizedUpdatedUsingBuffer = true; } + + memoizedUpdated = true; } } else @@ -664,6 +667,7 @@ private void EncodeBranch(scoped in Key key, scoped in ComputeContext ctx, scope { memo = RlpMemo.Delete(memo, i, rlpMemoization); memoizedUpdated = true; + memoizedUpdatedUsingBuffer = true; } } } @@ -742,11 +746,13 @@ private void EncodeBranch(scoped in Key key, scoped in ComputeContext ctx, scope else { memo = RlpMemo.Insert(memo, i, value, rlpMemoization); + memoizedUpdatedUsingBuffer = true; } } else if (memo.Exists(i)) { memo = RlpMemo.Delete(memo, i, rlpMemoization); + memoizedUpdatedUsingBuffer = true; } } } @@ -765,9 +771,9 @@ private void EncodeBranch(scoped in Key key, scoped in ComputeContext ctx, scope stream.Position = from; stream.StartSequence(actualLength); - if (memoize && !isOwnedByThisCommit && memoizedUpdated) + if (memoize && ((!isOwnedByThisCommit && memoizedUpdated) || memoizedUpdatedUsingBuffer)) { - // + // Set the branch if the memo has been updated using the new buffer memory. ctx.Commit.SetBranch(key, branch.Children, memo.Raw, EntryType.Persistent); } @@ -1052,6 +1058,7 @@ static void UpdateBranchOnDelete(ICommit commit, in Node.Branch branch, NibbleSe var childRlpRequiresUpdate = owner.IsOwnedBy(commit) == false; RlpMemo memo; byte[]? rlpWorkingSet = null; + var memoizedUpdatedUsingBuffer = false; if (childRlpRequiresUpdate) { @@ -1064,25 +1071,25 @@ static void UpdateBranchOnDelete(ICommit commit, in Node.Branch branch, NibbleSe memo = new RlpMemo(MakeRlpWritable(leftover)); } - // If this child still exists, only clear the memo. Otherwise, delete it from the memo. - if (children[nibble] && memo.Exists(nibble)) + if (memo.Exists(nibble)) { - memo.Clear(nibble); - } - else if (memo.Exists(nibble)) - { - if (rlpWorkingSet == null) + // If this child still exists, only clear the memo. Otherwise, delete it from the memo. + if (children[nibble]) { - rlpWorkingSet = ArrayPool.Shared.Rent(leftover.Length - Keccak.Size); + memo.Clear(nibble); + } + else + { + rlpWorkingSet ??= ArrayPool.Shared.Rent(leftover.Length - Keccak.Size); + memo = RlpMemo.Delete(memo, nibble, rlpWorkingSet); + memoizedUpdatedUsingBuffer = true; } - - memo = RlpMemo.Delete(memo, nibble, rlpWorkingSet); } var shouldUpdate = !branch.Children.Equals(children); // There's the cached RLP - if (shouldUpdate || childRlpRequiresUpdate) + if (shouldUpdate || childRlpRequiresUpdate || memoizedUpdatedUsingBuffer) { commit.SetBranch(key, children, memo.Raw); } @@ -1315,21 +1322,24 @@ private static void MarkPathDirty(in NibblePath path, in Span rlpMemoWorki ? RlpMemo.Copy(leftover, rlpMemoWorkingSet) : new RlpMemo(MakeRlpWritable(leftover)); - createLeaf = !branch.Children[nibble]; - var children = branch.Children.Set(nibble); - var shouldUpdateBranch = createLeaf; + var memoizedUpdatedUsingBuffer = false; - // If this path does not exist, insert an empty Keccak. Otherwise, clear it in the memo. - if (createLeaf) + // If this nibble exists in the memo, clear it from the memo. Otherwise, insert an empty Keccak. + if (memo.Exists(nibble)) { - memo = RlpMemo.Insert(memo, nibble, Keccak.Zero.Span, rlpMemoWorkingSet); + memo.Clear(nibble); } else { - memo.Clear(nibble); + memo = RlpMemo.Insert(memo, nibble, Keccak.Zero.Span, rlpMemoWorkingSet); + memoizedUpdatedUsingBuffer = true; } - if (shouldUpdateBranch || childRlpRequiresUpdate) + createLeaf = !branch.Children[nibble]; + var children = branch.Children.Set(nibble); + var shouldUpdateBranch = createLeaf; + + if (shouldUpdateBranch || childRlpRequiresUpdate || memoizedUpdatedUsingBuffer) { // Set the branch if either the children has changed or the RLP requires the update commit.SetBranch(key, children, memo.Raw);