From 6b1703cfc484d147cb015736544de09b284e6021 Mon Sep 17 00:00:00 2001 From: scooletz Date: Thu, 22 Aug 2024 17:17:06 +0200 Subject: [PATCH 1/4] by prefix delete --- src/Paprika/Data/NibblePath.cs | 9 +++++ src/Paprika/Data/SlottedArray.cs | 29 +++++++++++++- src/Paprika/IBatch.cs | 7 +++- src/Paprika/Store/DataPage.cs | 41 ++++++++++++++++---- src/Paprika/Store/FanOutListOf256.cs | 18 +++++++++ src/Paprika/Store/FanOutPage.cs | 30 +++++++++++++++ src/Paprika/Store/LeafPage.cs | 26 +++++++++++++ src/Paprika/Store/Merkle.cs | 29 ++++++++++++++ src/Paprika/Store/Page.cs | 5 +++ src/Paprika/Store/PagedDb.cs | 5 +++ src/Paprika/Store/RootPage.cs | 52 +++++++++++++++++++++++--- src/Paprika/Store/StorageFanOutPage.cs | 31 +++++++++++++++ 12 files changed, 267 insertions(+), 15 deletions(-) diff --git a/src/Paprika/Data/NibblePath.cs b/src/Paprika/Data/NibblePath.cs index 03f2bbed..52e1b932 100644 --- a/src/Paprika/Data/NibblePath.cs +++ b/src/Paprika/Data/NibblePath.cs @@ -644,6 +644,15 @@ public override string ToString() private static readonly char[] Hex = "0123456789ABCDEF".ToArray(); + // TODO: optimize + public bool StartsWith(in NibblePath prefix) + { + if (prefix.Length > Length) + return false; + + return SliceTo(prefix.Length).Equals(prefix); + } + public bool Equals(in NibblePath other) { if (((other.Length ^ Length) | (other._odd ^ _odd)) > 0) diff --git a/src/Paprika/Data/SlottedArray.cs b/src/Paprika/Data/SlottedArray.cs index b6381efc..e2d76970 100644 --- a/src/Paprika/Data/SlottedArray.cs +++ b/src/Paprika/Data/SlottedArray.cs @@ -83,6 +83,34 @@ public bool TrySet(in NibblePath key, ReadOnlySpan data) return TrySetImpl(hash, preamble, trimmed, data); } + public void DeleteByPrefix(in NibblePath prefix) + { + if (prefix.Length == 0) + { + Delete(prefix); + } + else if (prefix.Length == 1) + { + // TODO: optimize by filtering by hash. The key is at least 2 nibbles long so can be easily filtered with a bitwise mask over the hash. + // Don't materialize data! + foreach (var item in EnumerateNibble(prefix.FirstNibble)) + { + Delete(item); + } + } + else + { + // TODO: optimize by filtering by hash. The key is at least 2 nibbles long so can be easily filtered with a bitwise mask over the hash. + foreach (var item in EnumerateAll()) + { + if (item.Key.StartsWith(prefix)) + { + Delete(item); + } + } + } + } + private bool TrySetImpl(ushort hash, byte preamble, in NibblePath trimmed, ReadOnlySpan data) { var index = TryGetImpl(trimmed, hash, preamble, out var existingData); @@ -955,7 +983,6 @@ public static NibblePath UnPrepareKey(ushort hash, byte preamble, ReadOnlySpan void Destroy(in NibblePath account); + /// + /// Deletes all the keys that share the given prefix. + /// + void DeleteByPrefix(in Key prefix); + /// /// Commits the block returning its root hash. /// @@ -33,7 +38,7 @@ public interface IBatch : IReadOnlyBatch IBatchStats? Stats { get; } /// - /// Performs a time consuming verification when is called that all the pages are reachable. + /// Performs a time-consuming verification when is called that all the pages are reachable. /// void VerifyDbPagesOnCommit(); } diff --git a/src/Paprika/Store/DataPage.cs b/src/Paprika/Store/DataPage.cs index 98d7c886..03e06c44 100644 --- a/src/Paprika/Store/DataPage.cs +++ b/src/Paprika/Store/DataPage.cs @@ -3,7 +3,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Paprika.Data; - using static Paprika.Merkle.Node; namespace Paprika.Store; @@ -31,13 +30,39 @@ public readonly unsafe struct DataPage(Page page) : IPageWithData public ref Payload Data => ref Unsafe.AsRef(page.Payload); - public Page Set(in NibblePath key, in ReadOnlySpan data, IBatchContext batch) + public Page DeleteByPrefix(in NibblePath prefix, IBatchContext batch) { - if (page.Header.Level > 10) + if (Header.BatchId != batch.BatchId) + { + // the page is from another batch, meaning, it's readonly. Copy + var writable = batch.GetWritableCopy(page); + return new DataPage(writable).DeleteByPrefix(prefix, batch); + } + + Map.DeleteByPrefix(prefix); + + if (prefix.IsEmpty == false) { - Debugger.Break(); + var childAddr = Data.Buckets[prefix.FirstNibble]; + + if (childAddr.IsNull == false) + { + var sliced = prefix.SliceFrom(ConsumedNibbles); + var child = batch.GetAt(childAddr); + + child = child.Header.PageType == PageType.Leaf + ? new LeafPage(child).DeleteByPrefix(sliced, batch) + : new DataPage(child).DeleteByPrefix(sliced, batch); + + Data.Buckets[prefix.FirstNibble] = batch.GetAddress(child); + } } + return page; + } + + public Page Set(in NibblePath key, in ReadOnlySpan data, IBatchContext batch) + { if (Header.BatchId != batch.BatchId) { // the page is from another batch, meaning, it's readonly. Copy @@ -227,8 +252,7 @@ public struct Payload private const int DataOffset = Size - DataSize; - [FieldOffset(0)] - public DbAddressList.Of16 Buckets; + [FieldOffset(0)] public DbAddressList.Of16 Buckets; /// /// The first item of map of frames to allow ref to it. @@ -244,7 +268,8 @@ public struct Payload public bool TryGet(IReadOnlyBatchContext batch, scoped in NibblePath key, out ReadOnlySpan result) => TryGet(batch, key, out result, this); - private static bool TryGet(IReadOnlyBatchContext batch, scoped in NibblePath key, out ReadOnlySpan result, DataPage page) + private static bool TryGet(IReadOnlyBatchContext batch, scoped in NibblePath key, out ReadOnlySpan result, + DataPage page) { var returnValue = false; var sliced = key; @@ -334,4 +359,4 @@ public void Accept(IPageVisitor visitor, IPageResolver resolver, DbAddress addr) } } } -} +} \ No newline at end of file diff --git a/src/Paprika/Store/FanOutListOf256.cs b/src/Paprika/Store/FanOutListOf256.cs index 5e45ed03..dacf1121 100644 --- a/src/Paprika/Store/FanOutListOf256.cs +++ b/src/Paprika/Store/FanOutListOf256.cs @@ -54,6 +54,24 @@ public void Set(in NibblePath key, in ReadOnlySpan data, IBatchContext bat _addresses[index] = batch.GetAddress(updated); } + public void DeleteByPrefix(in NibblePath path, IBatchContext batch) + { + var index = GetIndex(path); + var sliced = path.SliceFrom(ConsumedNibbles); + + var addr = _addresses[index]; + + if (addr.IsNull) + { + // There's nothing to delete here + return; + } + + // The page exists, update + var updated = TPage.Wrap(batch.GetAt(addr)).DeleteByPrefix(sliced, batch); + _addresses[index] = batch.GetAddress(updated); + } + public void Report(IReporter reporter, IPageResolver resolver, int level, int trimmedNibbles) { var consumedNibbles = trimmedNibbles + ConsumedNibbles; diff --git a/src/Paprika/Store/FanOutPage.cs b/src/Paprika/Store/FanOutPage.cs index a216451e..80e0772c 100644 --- a/src/Paprika/Store/FanOutPage.cs +++ b/src/Paprika/Store/FanOutPage.cs @@ -112,6 +112,36 @@ public Page Set(in NibblePath key, in ReadOnlySpan data, IBatchContext bat return page; } + public Page DeleteByPrefix(in NibblePath prefix, IBatchContext batch) + { + if (Header.BatchId != batch.BatchId) + { + // the page is from another batch, meaning, it's readonly. Copy + var writable = batch.GetWritableCopy(page); + return new FanOutPage(writable).DeleteByPrefix(prefix, batch); + } + + if (IsKeyLocal(prefix)) + { + new SlottedArray(Data.Data).DeleteByPrefix(prefix); + return page; + } + + var index = GetIndex(prefix); + var sliced = prefix.SliceFrom(ConsumedNibbles); + + ref var addr = ref Data.Addresses[index]; + + if (addr.IsNull) + { + return page; + } + + // update after set + addr = batch.GetAddress(new DataPage(batch.GetAt(addr)).DeleteByPrefix(sliced, batch)); + return page; + } + private static bool IsKeyLocal(in NibblePath key) => key.Length < ConsumedNibbles; [DoesNotReturn] diff --git a/src/Paprika/Store/LeafPage.cs b/src/Paprika/Store/LeafPage.cs index b0d7395d..47ecd0ee 100644 --- a/src/Paprika/Store/LeafPage.cs +++ b/src/Paprika/Store/LeafPage.cs @@ -17,6 +17,32 @@ public readonly unsafe struct LeafPage(Page page) : IPageWithData private ref Payload Data => ref Unsafe.AsRef(page.Payload); + public Page DeleteByPrefix(in NibblePath prefix, IBatchContext batch) + { + if (Header.BatchId != batch.BatchId) + { + // the page is from another batch, meaning, it's readonly. Copy + var writable = batch.GetWritableCopy(page); + return new LeafPage(writable).DeleteByPrefix(prefix, batch); + } + + Map.DeleteByPrefix(prefix); + + for (var i = 0; i < BucketCount; i++) + { + var childAddr = Data.Buckets[i]; + if (childAddr.IsNull == false) + { + var child = batch.EnsureWritableCopy(ref childAddr); + var overflow = new LeafOverflowPage(child); + overflow.Map.DeleteByPrefix(prefix); + Data.Buckets[i] = childAddr; + } + } + + return page; + } + public Page Set(in NibblePath key, in ReadOnlySpan data, IBatchContext batch) { if (Header.BatchId != batch.BatchId) diff --git a/src/Paprika/Store/Merkle.cs b/src/Paprika/Store/Merkle.cs index 9f2bd5c8..2d0e43bc 100644 --- a/src/Paprika/Store/Merkle.cs +++ b/src/Paprika/Store/Merkle.cs @@ -70,6 +70,35 @@ public Page Set(in NibblePath key, in ReadOnlySpan data, IBatchContext bat return page; } + public Page DeleteByPrefix(in NibblePath prefix, IBatchContext batch) + { + if (Header.BatchId != batch.BatchId) + { + // the page is from another batch, meaning, it's readonly. Copy + var writable = batch.GetWritableCopy(page); + return new StateRootPage(writable).DeleteByPrefix(prefix, batch); + } + + if (prefix.Length < ConsumedNibbles) + { + var map = new SlottedArray(Data.DataSpan); + map.DeleteByPrefix(prefix); + return page; + } + + var index = GetIndex(prefix); + var sliced = prefix.SliceFrom(ConsumedNibbles); + ref var addr = ref Data.Buckets[index]; + + if (addr.IsNull) + { + return page; + } + + addr = batch.GetAddress(new DataPage(batch.GetAt(addr)).DeleteByPrefix(sliced, batch)); + return page; + } + /// /// Represents the data of this data page. This type of payload stores data in 16 nibble-addressable buckets. /// These buckets is used to store up to entries before flushing them down as other pages diff --git a/src/Paprika/Store/Page.cs b/src/Paprika/Store/Page.cs index 7ff28662..d981d0cd 100644 --- a/src/Paprika/Store/Page.cs +++ b/src/Paprika/Store/Page.cs @@ -26,6 +26,11 @@ public interface IPageWithData : IPage bool TryGet(IReadOnlyBatchContext batch, scoped in NibblePath key, out ReadOnlySpan result); + /// + /// Delete all the values by the given prefix in the page and below. + /// + Page DeleteByPrefix(in NibblePath prefix, IBatchContext batch); + Page Set(in NibblePath key, in ReadOnlySpan data, IBatchContext batch); void Report(IReporter reporter, IPageResolver resolver, int pageLevel, int trimmedNibbles); diff --git a/src/Paprika/Store/PagedDb.cs b/src/Paprika/Store/PagedDb.cs index 2bad3b08..57d4f00d 100644 --- a/src/Paprika/Store/PagedDb.cs +++ b/src/Paprika/Store/PagedDb.cs @@ -594,6 +594,11 @@ public void Destroy(in NibblePath account) _root.Destroy(this, account); } + public void DeleteByPrefix(in Key prefix) + { + _root.DeleteByPrefix(in prefix, this); + } + private void CheckDisposed() { if (_disposed) diff --git a/src/Paprika/Store/RootPage.cs b/src/Paprika/Store/RootPage.cs index b3bfecff..23a0a693 100644 --- a/src/Paprika/Store/RootPage.cs +++ b/src/Paprika/Store/RootPage.cs @@ -165,7 +165,9 @@ public bool TryGet(scoped in Key key, IReadOnlyBatchContext batch, out ReadOnlyS } private static uint ReadId(ReadOnlySpan id) => BinaryPrimitives.ReadUInt32LittleEndian(id); - private static void WriteId(Span idSpan, uint cachedId) => BinaryPrimitives.WriteUInt32LittleEndian(idSpan, cachedId); + + private static void WriteId(Span idSpan, uint cachedId) => + BinaryPrimitives.WriteUInt32LittleEndian(idSpan, cachedId); public void SetRaw(in Key key, IBatchContext batch, ReadOnlySpan rawData) @@ -217,6 +219,9 @@ public void SetRaw(in Key key, IBatchContext batch, ReadOnlySpan rawData) public void Destroy(IBatchContext batch, in NibblePath account) { + // GC for storage + DeleteByPrefix(Key.Merkle(account), batch); + // Destroy the Id entry about it Data.Ids.Set(account, ReadOnlySpan.Empty, batch); @@ -225,12 +230,49 @@ public void Destroy(IBatchContext batch, in NibblePath account) // Remove the cached batch.IdCache.Remove(account.UnsafeAsKeccak); + } - // TODO: there' no garbage collection for storage - // It should not be hard. Walk down by the mapped path, then remove all the pages underneath. + public void DeleteByPrefix(in Key prefix, IBatchContext batch) + { + if (prefix.IsState) + { + var data = batch.TryGetPageAlloc(ref Data.StateRoot, PageType.Standard); + var updated = new Merkle.StateRootPage(data).DeleteByPrefix(prefix.Path, batch); + Data.StateRoot = batch.GetAddress(updated); + } + else + { + scoped NibblePath id; + Span idSpan = stackalloc byte[sizeof(uint)]; + + var keccak = prefix.Path.UnsafeAsKeccak; + + if (batch.IdCache.TryGetValue(keccak, out var cachedId)) + { + WriteId(idSpan, cachedId); + id = NibblePath.FromKey(idSpan); + } + else + { + // Not in cache, try fetch from db + if (Data.Ids.TryGet(batch, prefix.Path, out var existingId) != false) + { + id = NibblePath.FromKey(existingId); + } + else + { + // Has never been mapped, return. + return; + } + } + + var path = id.Append(prefix.StoragePath, stackalloc byte[StorageKeySize]); + Data.Storage.DeleteByPrefix(path, batch); + } } - private static void SetAtRoot(IBatchContext batch, in NibblePath path, in ReadOnlySpan rawData, ref DbAddress root) + private static void SetAtRoot(IBatchContext batch, in NibblePath path, in ReadOnlySpan rawData, + ref DbAddress root) { var data = batch.TryGetPageAlloc(ref root, PageType.Standard); var updated = new Merkle.StateRootPage(data).Set(path, rawData, batch); @@ -252,4 +294,4 @@ public Metadata(uint blockNumber, Keccak stateHash) BlockNumber = blockNumber; StateHash = stateHash; } -} +} \ No newline at end of file diff --git a/src/Paprika/Store/StorageFanOutPage.cs b/src/Paprika/Store/StorageFanOutPage.cs index 8e1ccb7e..f972e7a7 100644 --- a/src/Paprika/Store/StorageFanOutPage.cs +++ b/src/Paprika/Store/StorageFanOutPage.cs @@ -92,6 +92,37 @@ public Page Set(in NibblePath key, in ReadOnlySpan data, IBatchContext bat return Set(key, data, batch); } + public Page DeleteByPrefix(in NibblePath prefix, IBatchContext batch) + { + if (Header.BatchId != batch.BatchId) + { + // the page is from another batch, meaning, it's readonly. Copy + var writable = batch.GetWritableCopy(page); + return new StorageFanOutPage(writable).DeleteByPrefix(prefix, batch); + } + + var map = new SlottedArray(Data.Data); + + map.DeleteByPrefix(prefix); + + var index = GetIndex(prefix); + var sliced = prefix.SliceFrom(ConsumedNibbles); + + ref var addr = ref Data.Addresses[index]; + + if (addr.IsNull) + { + return page; + } + + var child = batch.GetAt(addr); + + // Delete in child + addr = batch.GetAddress(TNext.Wrap(child).DeleteByPrefix(sliced, batch)); + + return page; + } + public void Report(IReporter reporter, IPageResolver resolver, int pageLevel, int trimmedNibbles) { var consumedNibbles = trimmedNibbles + ConsumedNibbles; From 1fdfe5989f6feca56f6fcd909b1a5347aeb0a922 Mon Sep 17 00:00:00 2001 From: scooletz Date: Thu, 22 Aug 2024 18:16:19 +0200 Subject: [PATCH 2/4] DeleteByPrefixes in RawState --- src/Paprika/Chain/Blockchain.cs | 22 ++++++++++++++++++++++ src/Paprika/Chain/IRawState.cs | 6 ++++++ src/Paprika/Data/Key.cs | 3 --- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index e2185d31..c189d406 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.CodeDom.Compiler; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -1632,6 +1633,7 @@ public async ValueTask DisposeAsync() /// private class RawState : IRawState { + private ArrayBufferWriter _prefixesToDelete = new(); private readonly Blockchain _blockchain; private readonly IDb _db; private BlockState _current; @@ -1698,6 +1700,13 @@ public void SetStorage(in Keccak address, in Keccak storage, ReadOnlySpan public void DestroyAccount(in Keccak address) => _current.DestroyAccount(address); + public void RegisterDeleteByPrefix(in Key prefix) + { + var span = _prefixesToDelete.GetSpan(prefix.MaxByteLength); + var leftover = prefix.WriteTo(span); + _prefixesToDelete.Advance(span.Length - leftover.Length); + } + public void Commit() { ThrowOnFinalized(); @@ -1714,6 +1723,8 @@ public void Commit() using var batch = _db.BeginNextBatch(); + DeleteByPrefixes(batch); + var committed = _current.CommitRaw(); committed.Apply(batch); _current.Dispose(); @@ -1727,6 +1738,17 @@ public void Commit() _current = new BlockState(Keccak.Zero, read, ancestors, _blockchain); } + private void DeleteByPrefixes(IBatch batch) + { + var prefixes = _prefixesToDelete.WrittenSpan; + while (prefixes.IsEmpty == false) + { + prefixes = Key.ReadFrom(prefixes, out var prefixToDelete); + batch.DeleteByPrefix(prefixToDelete); + } + _prefixesToDelete.ResetWrittenCount(); + } + public void Finalize(uint blockNumber) { ThrowOnFinalized(); diff --git a/src/Paprika/Chain/IRawState.cs b/src/Paprika/Chain/IRawState.cs index 18a7faad..f4a9dc57 100644 --- a/src/Paprika/Chain/IRawState.cs +++ b/src/Paprika/Chain/IRawState.cs @@ -17,6 +17,12 @@ public interface IRawState : IReadOnlyWorldState void DestroyAccount(in Keccak address); + /// + /// Registers a deletion that will be applied when is called. + /// + /// + void RegisterDeleteByPrefix(in Key prefix); + /// /// Commits the pending changes. /// diff --git a/src/Paprika/Data/Key.cs b/src/Paprika/Data/Key.cs index da838833..9b993844 100644 --- a/src/Paprika/Data/Key.cs +++ b/src/Paprika/Data/Key.cs @@ -1,9 +1,7 @@ using System.Diagnostics; -using System.IO.Hashing; using System.Numerics; using System.Runtime.CompilerServices; using Paprika.Crypto; -using Paprika.Store; namespace Paprika.Data; @@ -98,7 +96,6 @@ public static ReadOnlySpan ReadFrom(ReadOnlySpan source, out Key key public bool IsState => Type == DataType.Account || (Type == DataType.Merkle && Path.Length < NibblePath.KeccakNibbleCount); - [SkipLocalsInit] public override int GetHashCode() { From 5bebc8d90eb147f3e912efac172384f3699584d2 Mon Sep 17 00:00:00 2001 From: scooletz Date: Fri, 23 Aug 2024 10:30:19 +0200 Subject: [PATCH 3/4] RawState delete by prefix test --- src/Paprika.Tests/Chain/{RawTests.cs => RawStateTests.cs} | 2 +- src/Paprika/Chain/Blockchain.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/Paprika.Tests/Chain/{RawTests.cs => RawStateTests.cs} (99%) diff --git a/src/Paprika.Tests/Chain/RawTests.cs b/src/Paprika.Tests/Chain/RawStateTests.cs similarity index 99% rename from src/Paprika.Tests/Chain/RawTests.cs rename to src/Paprika.Tests/Chain/RawStateTests.cs index d7cc4784..68b7d4f1 100644 --- a/src/Paprika.Tests/Chain/RawTests.cs +++ b/src/Paprika.Tests/Chain/RawStateTests.cs @@ -9,7 +9,7 @@ namespace Paprika.Tests.Chain; -public class RawTests +public class RawStateTests { [Test] public async Task Raw_access_spin() diff --git a/src/Paprika/Chain/Blockchain.cs b/src/Paprika/Chain/Blockchain.cs index c189d406..c8b60f3b 100644 --- a/src/Paprika/Chain/Blockchain.cs +++ b/src/Paprika/Chain/Blockchain.cs @@ -1703,8 +1703,8 @@ public void SetStorage(in Keccak address, in Keccak storage, ReadOnlySpan public void RegisterDeleteByPrefix(in Key prefix) { var span = _prefixesToDelete.GetSpan(prefix.MaxByteLength); - var leftover = prefix.WriteTo(span); - _prefixesToDelete.Advance(span.Length - leftover.Length); + var written = prefix.WriteTo(span); + _prefixesToDelete.Advance(written.Length); } public void Commit() From a149d80659761a7b8c2fb6e0d749f24121abb9b2 Mon Sep 17 00:00:00 2001 From: scooletz Date: Fri, 23 Aug 2024 11:09:17 +0200 Subject: [PATCH 4/4] tests --- src/Paprika.Tests/Chain/RawStateTests.cs | 24 +++++++ src/Paprika.Tests/Store/PagedDbTests.cs | 85 ++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/src/Paprika.Tests/Chain/RawStateTests.cs b/src/Paprika.Tests/Chain/RawStateTests.cs index 68b7d4f1..5abd58f3 100644 --- a/src/Paprika.Tests/Chain/RawStateTests.cs +++ b/src/Paprika.Tests/Chain/RawStateTests.cs @@ -126,4 +126,28 @@ public async Task Disposal() raw.Hash.Should().Be(Keccak.EmptyTreeHash); } + + [Test] + public async Task DeleteByPrefix() + { + var account = Values.Key1; + + using var db = PagedDb.NativeMemoryDb(256 * 1024, 2); + var merkle = new ComputeMerkleBehavior(); + + await using var blockchain = new Blockchain(db, merkle); + + using var raw = blockchain.StartRaw(); + + raw.SetAccount(account, new Account(1, 1)); + raw.Commit(); + + raw.RegisterDeleteByPrefix(Key.Account(account)); + raw.Commit(); + + raw.Finalize(1); + + using var read = db.BeginReadOnlyBatch(); + read.TryGet(Key.Account(account), out _).Should().BeFalse(); + } } diff --git a/src/Paprika.Tests/Store/PagedDbTests.cs b/src/Paprika.Tests/Store/PagedDbTests.cs index 3901b853..9a0901cf 100644 --- a/src/Paprika.Tests/Store/PagedDbTests.cs +++ b/src/Paprika.Tests/Store/PagedDbTests.cs @@ -90,6 +90,91 @@ byte[] GetData() } } + [Test] + public async Task DeleteByPrefix_Accounts() + { + using var db = PagedDb.NativeMemoryDb(16 * Mb, 2); + + var keccak0 = Values.Key0; + var keccak1 = Values.Key0; + var keccak2 = Values.Key0; + var keccak3 = Values.Key0; + var prefix = Values.Key0; + + keccak0.BytesAsSpan[^1] = 0x01; + keccak1.BytesAsSpan[^1] = 0x02; + keccak2.BytesAsSpan[^1] = 0x03; + keccak3.BytesAsSpan[^1] = 0x04; + prefix.BytesAsSpan[^1] = 0x00; + + // Set data + using var batch = db.BeginNextBatch(); + + var v = new byte[] { 1 }; + batch.SetRaw(Key.Account(keccak0), v); + batch.SetRaw(Key.Account(keccak1), v); + batch.SetRaw(Key.Account(keccak2), v); + batch.SetRaw(Key.Account(keccak3), v); + + await batch.Commit(CommitOptions.FlushDataAndRoot); + + // Delete by prefix + using var batch2 = db.BeginNextBatch(); + batch.DeleteByPrefix(Key.Merkle(NibblePath.FromKey(prefix).SliceTo(NibblePath.KeccakNibbleCount - 1))); + await batch2.Commit(CommitOptions.FlushDataAndRoot); + + using var read = db.BeginReadOnlyBatch(); + + read.TryGet(Key.Account(keccak0), out _).Should().BeFalse(); + read.TryGet(Key.Account(keccak1), out _).Should().BeFalse(); + read.TryGet(Key.Account(keccak2), out _).Should().BeFalse(); + read.TryGet(Key.Account(keccak3), out _).Should().BeFalse(); + } + + [Test] + public async Task DeleteByPrefix_Storage() + { + using var db = PagedDb.NativeMemoryDb(16 * Mb, 2); + + var account = Values.Key2; + + var keccak0 = Values.Key0; + var keccak1 = Values.Key0; + var keccak2 = Values.Key0; + var keccak3 = Values.Key0; + var prefix = Values.Key0; + + keccak0.BytesAsSpan[^1] = 0x01; + keccak1.BytesAsSpan[^1] = 0x02; + keccak2.BytesAsSpan[^1] = 0x03; + keccak3.BytesAsSpan[^1] = 0x04; + prefix.BytesAsSpan[^1] = 0x00; + + // Set data + using var batch = db.BeginNextBatch(); + + var v = new byte[] { 1 }; + batch.SetRaw(Key.StorageCell(NibblePath.FromKey(account), keccak0), v); + batch.SetRaw(Key.StorageCell(NibblePath.FromKey(account), keccak1), v); + batch.SetRaw(Key.StorageCell(NibblePath.FromKey(account), keccak2), v); + batch.SetRaw(Key.StorageCell(NibblePath.FromKey(account), keccak3), v); + + await batch.Commit(CommitOptions.FlushDataAndRoot); + + // Delete by prefix + using var batch2 = db.BeginNextBatch(); + batch.DeleteByPrefix(Key.Raw(NibblePath.FromKey(account), DataType.Merkle, + NibblePath.FromKey(prefix).SliceTo(NibblePath.KeccakNibbleCount - 1))); + await batch2.Commit(CommitOptions.FlushDataAndRoot); + + using var read = db.BeginReadOnlyBatch(); + + read.TryGet(Key.StorageCell(NibblePath.FromKey(account), keccak0), out _).Should().BeFalse(); + read.TryGet(Key.StorageCell(NibblePath.FromKey(account), keccak1), out _).Should().BeFalse(); + read.TryGet(Key.StorageCell(NibblePath.FromKey(account), keccak2), out _).Should().BeFalse(); + read.TryGet(Key.StorageCell(NibblePath.FromKey(account), keccak3), out _).Should().BeFalse(); + } + [Test] public async Task Multiple_storages_per_commit() {