diff --git a/src/Nethermind/Nethermind.Blockchain/ReceiptCanonicalityMonitor.cs b/src/Nethermind/Nethermind.Blockchain/ReceiptCanonicalityMonitor.cs index 4d6b55283d95..eea17931dabe 100644 --- a/src/Nethermind/Nethermind.Blockchain/ReceiptCanonicalityMonitor.cs +++ b/src/Nethermind/Nethermind.Blockchain/ReceiptCanonicalityMonitor.cs @@ -25,7 +25,7 @@ public ReceiptCanonicalityMonitor(IReceiptStorage? receiptStorage, ILogManager? { _receiptStorage = receiptStorage ?? throw new ArgumentNullException(nameof(receiptStorage)); _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); - _receiptStorage.ReceiptsInserted += OnBlockAddedToMain; + _receiptStorage.NewCanonicalReceipts += OnBlockAddedToMain; } private void OnBlockAddedToMain(object sender, BlockReplacementEventArgs e) @@ -55,7 +55,7 @@ private void TriggerReceiptInsertedEvent(Block newBlock, Block? previousBlock) public void Dispose() { - _receiptStorage.ReceiptsInserted -= OnBlockAddedToMain; + _receiptStorage.NewCanonicalReceipts -= OnBlockAddedToMain; } } } diff --git a/src/Nethermind/Nethermind.Blockchain/Receipts/IReceiptStorage.cs b/src/Nethermind/Nethermind.Blockchain/Receipts/IReceiptStorage.cs index 65633a1fa7ad..dd7caa7366b7 100644 --- a/src/Nethermind/Nethermind.Blockchain/Receipts/IReceiptStorage.cs +++ b/src/Nethermind/Nethermind.Blockchain/Receipts/IReceiptStorage.cs @@ -19,8 +19,16 @@ public interface IReceiptStorage : IReceiptFinder void RemoveReceipts(Block block); /// - /// Receipts for a block are inserted + /// Receipts for canonical chain changed. /// - event EventHandler ReceiptsInserted; + event EventHandler? NewCanonicalReceipts; + + /// + /// Receipts for any block are inserted. + /// + /// + /// This is invoked for both canonical and non-canonical blocks. + /// + event EventHandler? ReceiptsInserted; } } diff --git a/src/Nethermind/Nethermind.Blockchain/Receipts/InMemoryReceiptStorage.cs b/src/Nethermind/Nethermind.Blockchain/Receipts/InMemoryReceiptStorage.cs index 1e3eec074a49..a8824cb66a4c 100644 --- a/src/Nethermind/Nethermind.Blockchain/Receipts/InMemoryReceiptStorage.cs +++ b/src/Nethermind/Nethermind.Blockchain/Receipts/InMemoryReceiptStorage.cs @@ -18,7 +18,8 @@ public class InMemoryReceiptStorage : IReceiptStorage private readonly ConcurrentDictionary _transactions = new(); #pragma warning disable CS0067 - public event EventHandler ReceiptsInserted; + public event EventHandler? NewCanonicalReceipts; + public event EventHandler? ReceiptsInserted; #pragma warning restore CS0067 public InMemoryReceiptStorage(bool allowReceiptIterator = true, IBlockTree? blockTree = null) @@ -32,7 +33,7 @@ public InMemoryReceiptStorage(bool allowReceiptIterator = true, IBlockTree? bloc private void BlockTree_BlockAddedToMain(object? sender, BlockReplacementEventArgs e) { EnsureCanonical(e.Block); - ReceiptsInserted?.Invoke(this, e); + NewCanonicalReceipts?.Invoke(this, e); } public Hash256 FindBlockHash(Hash256 txHash) @@ -73,6 +74,8 @@ public void Insert(Block block, TxReceipt[] txReceipts, IReleaseSpec spec, bool { EnsureCanonical(block); } + + ReceiptsInserted?.Invoke(this, new(block.Header, txReceipts)); } public bool HasBlock(long blockNumber, Hash256 hash) diff --git a/src/Nethermind/Nethermind.Blockchain/Receipts/NullReceiptStorage.cs b/src/Nethermind/Nethermind.Blockchain/Receipts/NullReceiptStorage.cs index 1c06f8b3d21e..7ed7bb5637ae 100644 --- a/src/Nethermind/Nethermind.Blockchain/Receipts/NullReceiptStorage.cs +++ b/src/Nethermind/Nethermind.Blockchain/Receipts/NullReceiptStorage.cs @@ -13,7 +13,8 @@ public class NullReceiptStorage : IReceiptStorage public static NullReceiptStorage Instance { get; } = new(); #pragma warning disable CS0067 - public event EventHandler ReceiptsInserted; + public event EventHandler? NewCanonicalReceipts; + public event EventHandler? ReceiptsInserted; #pragma warning restore CS0067 public Hash256? FindBlockHash(Hash256 hash) => null; diff --git a/src/Nethermind/Nethermind.Blockchain/Receipts/PersistentReceiptStorage.cs b/src/Nethermind/Nethermind.Blockchain/Receipts/PersistentReceiptStorage.cs index 5d5715278943..5f2cbd740052 100644 --- a/src/Nethermind/Nethermind.Blockchain/Receipts/PersistentReceiptStorage.cs +++ b/src/Nethermind/Nethermind.Blockchain/Receipts/PersistentReceiptStorage.cs @@ -38,7 +38,8 @@ public class PersistentReceiptStorage : IReceiptStorage private const int CacheSize = 64; private readonly LruCache _receiptsCache = new(CacheSize, CacheSize, "receipts"); - public event EventHandler ReceiptsInserted; + public event EventHandler? NewCanonicalReceipts; + public event EventHandler? ReceiptsInserted; public PersistentReceiptStorage( IColumnsDb receiptsDb, @@ -73,7 +74,7 @@ public PersistentReceiptStorage( private void BlockTreeOnBlockAddedToMain(object? sender, BlockReplacementEventArgs e) { EnsureCanonical(e.Block); - ReceiptsInserted?.Invoke(this, e); + NewCanonicalReceipts?.Invoke(this, e); // Dont block main loop Task.Run(() => @@ -288,6 +289,8 @@ public void Insert(Block block, TxReceipt[]? txReceipts, IReleaseSpec spec, bool { EnsureCanonical(block, lastBlockNumber); } + + ReceiptsInserted?.Invoke(this, new(block.Header, txReceipts)); } public long MigratedBlockNumber diff --git a/src/Nethermind/Nethermind.Core.Test/Builders/RandomExtensions.cs b/src/Nethermind/Nethermind.Core.Test/Builders/RandomExtensions.cs new file mode 100644 index 000000000000..17135ff14fa3 --- /dev/null +++ b/src/Nethermind/Nethermind.Core.Test/Builders/RandomExtensions.cs @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using Nethermind.Crypto; + +namespace Nethermind.Core.Test.Builders; + +public static class RandomExtensions +{ + public static T NextFrom(this Random random, IReadOnlyList values) => values[random.Next(values.Count)]; + + public static T NextFrom(this ICryptoRandom random, IReadOnlyList values) => values[random.NextInt(values.Count)]; +} diff --git a/src/Nethermind/Nethermind.Core.Test/Json/ByteArrayConverterTests.cs b/src/Nethermind/Nethermind.Core.Test/Json/ByteArrayConverterTests.cs index d1413676fa33..5888c30c65e1 100644 --- a/src/Nethermind/Nethermind.Core.Test/Json/ByteArrayConverterTests.cs +++ b/src/Nethermind/Nethermind.Core.Test/Json/ByteArrayConverterTests.cs @@ -8,6 +8,8 @@ using System.Text.Json; using FluentAssertions; using Nethermind.Core.Extensions; +using Nethermind.Core.Test.Builders; +using Nethermind.Crypto; using Nethermind.Serialization.Json; using NUnit.Framework; @@ -294,6 +296,22 @@ public void Fuzz_RandomHex_SegmentationInvariant() } } + [Test] + public void Test_DictionaryKey() + { + var random = new CryptoRandom(); + var dictionary = new Dictionary + { + { Bytes.FromHexString("0x0"), null }, + { Bytes.FromHexString("0x1"), random.NextInt(int.MaxValue) }, + { Build.An.Address.TestObject.Bytes, random.NextInt(int.MaxValue) }, + { random.GenerateRandomBytes(10), random.NextInt(int.MaxValue) }, + { random.GenerateRandomBytes(32), random.NextInt(int.MaxValue) }, + }; + + TestConverter(dictionary, new ByteArrayConverter()); + } + private static ReadOnlySequence JsonForLiteral(string literal) => MakeSequence(Encoding.UTF8.GetBytes(literal)); diff --git a/src/Nethermind/Nethermind.Core.Test/Json/ConverterTestBase.cs b/src/Nethermind/Nethermind.Core.Test/Json/ConverterTestBase.cs index 7f38126cda19..8725b3750dba 100644 --- a/src/Nethermind/Nethermind.Core.Test/Json/ConverterTestBase.cs +++ b/src/Nethermind/Nethermind.Core.Test/Json/ConverterTestBase.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Collections; using System.Text.Json; using System.Text.Json.Serialization; using NUnit.Framework; @@ -10,7 +11,10 @@ namespace Nethermind.Core.Test.Json; public class ConverterTestBase { - protected void TestConverter(T? item, Func equalityComparer, JsonConverter converter) + protected void TestConverter(T? item, Func equalityComparer, JsonConverter converter) => + TestConverter(item, converter, equalityComparer); + + protected void TestConverter(TItem? item, JsonConverter converter, Func? equalityComparer = null) { var options = new JsonSerializerOptions { @@ -22,10 +26,15 @@ protected void TestConverter(T? item, Func equalityComparer, JsonCon string result = JsonSerializer.Serialize(item, options); - T? deserialized = JsonSerializer.Deserialize(result, options); + TItem? deserialized = JsonSerializer.Deserialize(result, options); #pragma warning disable CS8604 - Assert.That(equalityComparer(item, deserialized), Is.True); + if (equalityComparer is not null) + Assert.That(equalityComparer(item, deserialized), Is.True); + else if (item is IEnumerable itemE && deserialized is IEnumerable deserializedE) + Assert.That(deserializedE, Is.EquivalentTo(itemE)); + else + Assert.That(deserialized, Is.EqualTo(item)); #pragma warning restore CS8604 } } diff --git a/src/Nethermind/Nethermind.Core/Collections/DictionaryExtensions.cs b/src/Nethermind/Nethermind.Core/Collections/DictionaryExtensions.cs index e036bc568d54..ddb32f7f8bf4 100644 --- a/src/Nethermind/Nethermind.Core/Collections/DictionaryExtensions.cs +++ b/src/Nethermind/Nethermind.Core/Collections/DictionaryExtensions.cs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System; using System.Collections.Generic; using System.Runtime.InteropServices; @@ -13,4 +14,22 @@ public static void Increment(this Dictionary dictionary, TKey k ref int res = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out bool _); res++; } + + public static TValue GetOrAdd(this Dictionary dictionary, + TKey key, Func factory, + out bool exists) + where TKey : notnull + { + ref TValue? existing = ref CollectionsMarshal.GetValueRefOrAddDefault(dictionary, key, out exists); + + if (!exists) + existing = factory(key); + + return existing!; + } + + public static TValue GetOrAdd(this Dictionary dictionary, + TKey key, Func factory) + where TKey : notnull => + GetOrAdd(dictionary, key, factory, out _); } diff --git a/src/Nethermind/Nethermind.Core/Extensions/Bytes.cs b/src/Nethermind/Nethermind.Core/Extensions/Bytes.cs index 128268d8243b..e6a5f23545bf 100644 --- a/src/Nethermind/Nethermind.Core/Extensions/Bytes.cs +++ b/src/Nethermind/Nethermind.Core/Extensions/Bytes.cs @@ -262,7 +262,7 @@ public static byte[] PadLeft(this ReadOnlySpan bytes, int length, byte pad return result; } - public static byte[] PadRight(this byte[] bytes, int length) + public static byte[] PadRight(this byte[] bytes, int length, byte padding = 0) { if (bytes.Length == length) { @@ -276,6 +276,12 @@ public static byte[] PadRight(this byte[] bytes, int length) byte[] result = new byte[length]; Buffer.BlockCopy(bytes, 0, result, 0, bytes.Length); + + if (padding != 0) + { + result.AsSpan(bytes.Length, length - bytes.Length).Fill(padding); + } + return result; } diff --git a/src/Nethermind/Nethermind.Core/Extensions/SizeExtensions.cs b/src/Nethermind/Nethermind.Core/Extensions/SizeExtensions.cs index ba773225fb46..2ae4af43e7e8 100644 --- a/src/Nethermind/Nethermind.Core/Extensions/SizeExtensions.cs +++ b/src/Nethermind/Nethermind.Core/Extensions/SizeExtensions.cs @@ -67,7 +67,7 @@ public static long KiB(this int @this) return ((long)@this).KiB(); } - public static string SizeToString(this long @this, bool useSi = false, int precision = 1) + public static string SizeToString(this long @this, bool useSi = false, bool addSpace = false, int precision = 1) { string[] suf = useSi ? ["B", "KB", "MB", "GB", "TB"] : ["B", "KiB", "MiB", "GiB", "TiB"]; if (@this == 0) @@ -77,7 +77,7 @@ public static string SizeToString(this long @this, bool useSi = false, int preci long bytes = Math.Abs(@this); int place = Math.Min(suf.Length - 1, Convert.ToInt32(Math.Floor(Math.Log(bytes, useSi ? 1000 : 1024)))); double num = Math.Round(bytes / Math.Pow(useSi ? 1000 : 1024, place), precision); - return (Math.Sign(@this) * num).ToString() + suf[place]; + return string.Concat(Math.Sign(@this) * num, addSpace ? " " : "", suf[place]); } } } diff --git a/src/Nethermind/Nethermind.Core/Extensions/WaitHandleExtensions.cs b/src/Nethermind/Nethermind.Core/Extensions/WaitHandleExtensions.cs index fd0616dca645..5090c78856f1 100644 --- a/src/Nethermind/Nethermind.Core/Extensions/WaitHandleExtensions.cs +++ b/src/Nethermind/Nethermind.Core/Extensions/WaitHandleExtensions.cs @@ -23,8 +23,12 @@ public static async Task WaitOneAsync(this WaitHandle handle, int millisec millisecondsTimeout, true); tokenRegistration = cancellationToken.Register( - static state => ((TaskCompletionSource)state!).TrySetCanceled(), - tcs); + static state => + { + var (tcs, ct) = ((TaskCompletionSource tcs, CancellationToken ct))state!; + tcs.TrySetCanceled(ct); + }, + (tcs, cancellationToken)); return await tcs.Task; } diff --git a/src/Nethermind/Nethermind.Core/FakeWriteBatch.cs b/src/Nethermind/Nethermind.Core/FakeWriteBatch.cs index ce27831eb399..1fcf153998a1 100644 --- a/src/Nethermind/Nethermind.Core/FakeWriteBatch.cs +++ b/src/Nethermind/Nethermind.Core/FakeWriteBatch.cs @@ -31,5 +31,12 @@ public void Set(ReadOnlySpan key, byte[]? value, WriteFlags flags = WriteF { _storePretendingToSupportBatches.Set(key, value, flags); } + + public void Merge(ReadOnlySpan key, ReadOnlySpan value, WriteFlags flags = WriteFlags.None) + { + throw new NotSupportedException("Merging is not supported by this implementation."); + } + + public void Clear() { } } } diff --git a/src/Nethermind/Nethermind.Core/IKeyValueStore.cs b/src/Nethermind/Nethermind.Core/IKeyValueStore.cs index e613308267ed..52a6b97b317c 100644 --- a/src/Nethermind/Nethermind.Core/IKeyValueStore.cs +++ b/src/Nethermind/Nethermind.Core/IKeyValueStore.cs @@ -69,6 +69,11 @@ public interface IWriteOnlyKeyValueStore void Remove(ReadOnlySpan key) => Set(key, null); } + public interface IMergeableKeyValueStore : IWriteOnlyKeyValueStore + { + void Merge(ReadOnlySpan key, ReadOnlySpan value, WriteFlags flags = WriteFlags.None); + } + public interface ISortedKeyValueStore : IKeyValueStore { byte[]? FirstKey { get; } @@ -82,6 +87,7 @@ public interface ISortedKeyValueStore : IKeyValueStore /// public interface ISortedView : IDisposable { + public bool StartBefore(ReadOnlySpan value); public bool MoveNext(); public ReadOnlySpan CurrentKey { get; } public ReadOnlySpan CurrentValue { get; } diff --git a/src/Nethermind/Nethermind.Core/IWriteBatch.cs b/src/Nethermind/Nethermind.Core/IWriteBatch.cs index 909f184541c0..852dc132caa9 100644 --- a/src/Nethermind/Nethermind.Core/IWriteBatch.cs +++ b/src/Nethermind/Nethermind.Core/IWriteBatch.cs @@ -5,5 +5,8 @@ namespace Nethermind.Core { - public interface IWriteBatch : IDisposable, IWriteOnlyKeyValueStore; + public interface IWriteBatch : IDisposable, IWriteOnlyKeyValueStore, IMergeableKeyValueStore + { + void Clear(); + } } diff --git a/src/Nethermind/Nethermind.Core/JsonConverters/ByteArrayConverter.cs b/src/Nethermind/Nethermind.Core/JsonConverters/ByteArrayConverter.cs index 664fbb1eccda..27b535d096f2 100644 --- a/src/Nethermind/Nethermind.Core/JsonConverters/ByteArrayConverter.cs +++ b/src/Nethermind/Nethermind.Core/JsonConverters/ByteArrayConverter.cs @@ -250,4 +250,19 @@ public static void Convert( if (array is not null) ArrayPool.Shared.Return(array); } + + public override byte[] ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + byte[]? result = Convert(ref reader); + + if (result is null) + ThrowInvalidOperationException(); + + return result; + } + + public override void WriteAsPropertyName(Utf8JsonWriter writer, byte[] value, JsonSerializerOptions options) + { + Convert(writer, value, static (w, h) => w.WritePropertyName(h), skipLeadingZeros: false, addQuotations: false, addHexPrefix: true); + } } diff --git a/src/Nethermind/Nethermind.Core/ProgressLogger.cs b/src/Nethermind/Nethermind.Core/ProgressLogger.cs index f674106dab0d..589bb60f90c4 100644 --- a/src/Nethermind/Nethermind.Core/ProgressLogger.cs +++ b/src/Nethermind/Nethermind.Core/ProgressLogger.cs @@ -47,9 +47,9 @@ public void IncrementSkipped(int skipped = 1) Interlocked.Add(ref _skipped, skipped); } - public void SetMeasuringPoint() + public void SetMeasuringPoint(bool resetCompletion = true) { - UtcEndTime = null; + if (resetCompletion) UtcEndTime = null; if (UtcStartTime is not null) { @@ -169,7 +169,7 @@ public void LogProgress() _lastReportState = reportState; _logger.Info(reportString); } - SetMeasuringPoint(); + SetMeasuringPoint(resetCompletion: false); } private string DefaultFormatter() diff --git a/src/Nethermind/Nethermind.Core/Utils/ConcurrentWriteBatcher.cs b/src/Nethermind/Nethermind.Core/Utils/ConcurrentWriteBatcher.cs index 9ee5829dc20f..a6c80ed33fc7 100644 --- a/src/Nethermind/Nethermind.Core/Utils/ConcurrentWriteBatcher.cs +++ b/src/Nethermind/Nethermind.Core/Utils/ConcurrentWriteBatcher.cs @@ -40,6 +40,18 @@ public void PutSpan(ReadOnlySpan key, ReadOnlySpan value, WriteFlags ReturnWriteBatch(currentBatch); } + public void Merge(ReadOnlySpan key, ReadOnlySpan value, WriteFlags flags = WriteFlags.None) + { + IWriteBatch currentBatch = RentWriteBatch(); + currentBatch.Merge(key, value, flags); + ReturnWriteBatch(currentBatch); + } + + public void Clear() + { + throw new NotSupportedException($"{nameof(ConcurrentWriteBatcher)} can not be cancelled."); + } + public void Set(ReadOnlySpan key, byte[]? value, WriteFlags flags = WriteFlags.None) { IWriteBatch currentBatch = RentWriteBatch(); diff --git a/src/Nethermind/Nethermind.Db.Rocks/ColumnDb.cs b/src/Nethermind/Nethermind.Db.Rocks/ColumnDb.cs index ece6bb682979..f1271ebe84e2 100644 --- a/src/Nethermind/Nethermind.Db.Rocks/ColumnDb.cs +++ b/src/Nethermind/Nethermind.Db.Rocks/ColumnDb.cs @@ -11,7 +11,7 @@ namespace Nethermind.Db.Rocks; -public class ColumnDb : IDb, ISortedKeyValueStore +public class ColumnDb : IDb, ISortedKeyValueStore, IMergeableKeyValueStore { private readonly RocksDb _rocksDb; internal readonly DbOnTheRocks _mainDb; @@ -57,6 +57,11 @@ public void PutSpan(ReadOnlySpan key, ReadOnlySpan value, WriteFlags _mainDb.SetWithColumnFamily(key, _columnFamily, value, writeFlags); } + public void Merge(ReadOnlySpan key, ReadOnlySpan value, WriteFlags writeFlags = WriteFlags.None) + { + _mainDb.MergeWithColumnFamily(key, _columnFamily, value, writeFlags); + } + public KeyValuePair[] this[byte[][] keys] => _rocksDb.MultiGet(keys, keys.Select(k => _columnFamily).ToArray()); @@ -99,6 +104,11 @@ public void Dispose() _underlyingWriteBatch.Dispose(); } + public void Clear() + { + _underlyingWriteBatch.Clear(); + } + public void Set(ReadOnlySpan key, byte[]? value, WriteFlags flags = WriteFlags.None) { if (value is null) @@ -115,6 +125,11 @@ public void PutSpan(ReadOnlySpan key, ReadOnlySpan value, WriteFlags { _underlyingWriteBatch.Set(key, value, _columnDb._columnFamily, flags); } + + public void Merge(ReadOnlySpan key, ReadOnlySpan value, WriteFlags flags = WriteFlags.None) + { + _underlyingWriteBatch.Merge(key, value, _columnDb._columnFamily, flags); + } } public void Remove(ReadOnlySpan key) @@ -130,7 +145,7 @@ public bool KeyExists(ReadOnlySpan key) public void Flush(bool onlyWal) { - _mainDb.Flush(onlyWal); + _mainDb.FlushWithColumnFamily(_columnFamily); } public void Compact() diff --git a/src/Nethermind/Nethermind.Db.Rocks/ColumnsDb.cs b/src/Nethermind/Nethermind.Db.Rocks/ColumnsDb.cs index 02300a2d7e62..d5a240d1e968 100644 --- a/src/Nethermind/Nethermind.Db.Rocks/ColumnsDb.cs +++ b/src/Nethermind/Nethermind.Db.Rocks/ColumnsDb.cs @@ -61,9 +61,9 @@ private static IReadOnlyList GetEnumKeys(IReadOnlyList keys) return keys; } - protected override void BuildOptions(IRocksDbConfig dbConfig, Options options, IntPtr? sharedCache) + protected override void BuildOptions(IRocksDbConfig dbConfig, Options options, IntPtr? sharedCache, IMergeOperator? mergeOperator) { - base.BuildOptions(dbConfig, options, sharedCache); + base.BuildOptions(dbConfig, options, sharedCache, mergeOperator); options.SetCreateMissingColumnFamilies(); } @@ -130,9 +130,19 @@ public void Dispose() _writeBatch.Dispose(); } + public void Clear() + { + _writeBatch._writeBatch.Clear(); + } + public void Set(ReadOnlySpan key, byte[]? value, WriteFlags flags = WriteFlags.None) { _writeBatch._writeBatch.Set(key, value, _column._columnFamily, flags); } + + public void Merge(ReadOnlySpan key, ReadOnlySpan value, WriteFlags flags = WriteFlags.None) + { + _writeBatch._writeBatch.Merge(key, value, _column._columnFamily, flags); + } } } diff --git a/src/Nethermind/Nethermind.Db.Rocks/Config/PerTableDbConfig.cs b/src/Nethermind/Nethermind.Db.Rocks/Config/PerTableDbConfig.cs index 6524901e3578..0efa1fe7553d 100644 --- a/src/Nethermind/Nethermind.Db.Rocks/Config/PerTableDbConfig.cs +++ b/src/Nethermind/Nethermind.Db.Rocks/Config/PerTableDbConfig.cs @@ -38,7 +38,7 @@ private void EnsureConfigIsAvailable(string propertyName) foreach (var prefix in _prefixes) { string prefixed = string.Concat(prefix, propertyName); - if (type.GetProperty(prefixed, BindingFlags.Public | BindingFlags.Instance) is null) + if (GetProperty(type, prefixed) is null) { throw new InvalidConfigurationException($"Configuration {propertyName} not available with prefix {prefix}. Add {prefix}{propertyName} to {nameof(IDbConfig)}.", -1); } @@ -90,13 +90,13 @@ private static string ReadRocksdbOptions(IDbConfig dbConfig, string propertyName Type type = dbConfig.GetType(); PropertyInfo? propertyInfo; - string val = (string)type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance)!.GetValue(dbConfig)!; + string val = (string)GetProperty(type, propertyName)!.GetValue(dbConfig)!; foreach (var prefix in prefixes) { string prefixed = string.Concat(prefix, propertyName); - propertyInfo = type.GetProperty(prefixed, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); + propertyInfo = GetProperty(type, prefixed); if (propertyInfo is not null) { string? valObj = (string?)propertyInfo.GetValue(dbConfig); @@ -122,7 +122,7 @@ private static string ReadRocksdbOptions(IDbConfig dbConfig, string propertyName { string prefixed = string.Concat(prefix, propertyName); - propertyInfo = type.GetProperty(prefixed, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); + propertyInfo = GetProperty(type, prefixed); if (propertyInfo is not null) { if (propertyInfo.PropertyType.CanBeAssignedNull()) @@ -147,7 +147,7 @@ private static string ReadRocksdbOptions(IDbConfig dbConfig, string propertyName } // Use generic one even if its available - propertyInfo = type.GetProperty(propertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); + propertyInfo = GetProperty(type, propertyName); return (T?)propertyInfo?.GetValue(dbConfig); } catch (Exception e) @@ -156,4 +156,6 @@ private static string ReadRocksdbOptions(IDbConfig dbConfig, string propertyName } } + private static PropertyInfo? GetProperty(Type type, string name) => + type.GetProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); } diff --git a/src/Nethermind/Nethermind.Db.Rocks/DbOnTheRocks.cs b/src/Nethermind/Nethermind.Db.Rocks/DbOnTheRocks.cs index 474ae0d50da1..a69d62475f0c 100644 --- a/src/Nethermind/Nethermind.Db.Rocks/DbOnTheRocks.cs +++ b/src/Nethermind/Nethermind.Db.Rocks/DbOnTheRocks.cs @@ -29,7 +29,7 @@ namespace Nethermind.Db.Rocks; -public partial class DbOnTheRocks : IDb, ITunableDb, IReadOnlyNativeKeyValueStore, ISortedKeyValueStore +public partial class DbOnTheRocks : IDb, ITunableDb, IReadOnlyNativeKeyValueStore, ISortedKeyValueStore, IMergeableKeyValueStore { protected ILogger _logger; @@ -66,6 +66,10 @@ public partial class DbOnTheRocks : IDb, ITunableDb, IReadOnlyNativeKeyValueStor private readonly DbSettings _settings; + // Need to keep options from GC in case of merge operator applied, as they are used in callback + // ReSharper disable once CollectionNeverQueried.Local + private readonly List _doNotGcOptions = []; + private readonly IRocksDbConfig _perTableDbConfig; private ulong _maxBytesForLevelBase; private ulong _targetFileSizeBase; @@ -143,7 +147,7 @@ private RocksDb Init(string basePath, string dbPath, IDbConfig dbConfig, ILogMan // ReSharper disable once VirtualMemberCallInConstructor if (_logger.IsDebug) _logger.Debug($"Building options for {Name} DB"); DbOptions = new DbOptions(); - BuildOptions(_perTableDbConfig, DbOptions, sharedCache); + BuildOptions(_perTableDbConfig, DbOptions, sharedCache, _settings.MergeOperator); ColumnFamilies? columnFamilies = null; if (columnNames is not null) @@ -155,7 +159,8 @@ private RocksDb Init(string basePath, string dbPath, IDbConfig dbConfig, ILogMan ColumnFamilyOptions options = new(); IRocksDbConfig columnConfig = _rocksDbConfigFactory.GetForDatabase(Name, columnFamily); - BuildOptions(columnConfig, options, sharedCache); + IMergeOperator? mergeOperator = _settings.ColumnsMergeOperators?.GetValueOrDefault(enumColumnName); + BuildOptions(columnConfig, options, sharedCache, mergeOperator); // "default" is a special column name with rocksdb, which is what previously not specifying column goes to if (columnFamily == "Default") columnFamily = "default"; @@ -447,7 +452,7 @@ public static IDictionary ExtractOptions(string dbOptions) return asDict; } - protected virtual void BuildOptions(IRocksDbConfig dbConfig, Options options, IntPtr? sharedCache) where T : Options + protected virtual void BuildOptions(IRocksDbConfig dbConfig, Options options, IntPtr? sharedCache, IMergeOperator? mergeOperator) where T : Options { // This section is about the table factory.. and block cache apparently. // This effect the format of the SST files and usually require resync to take effect. @@ -577,6 +582,13 @@ protected virtual void BuildOptions(IRocksDbConfig dbConfig, Options optio } } + if (mergeOperator is not null) + { + options.SetMergeOperator(new MergeOperatorAdapter(mergeOperator)); + lock (_doNotGcOptions) + _doNotGcOptions.Add(options); + } + #endregion #region read-write options @@ -893,6 +905,40 @@ public void PutSpan(ReadOnlySpan key, ReadOnlySpan value, WriteFlags SetWithColumnFamily(key, null, value, writeFlags); } + public void Merge(ReadOnlySpan key, ReadOnlySpan value, WriteFlags flags = WriteFlags.None) + { + ObjectDisposedException.ThrowIf(_isDisposing, this); + + UpdateWriteMetrics(); + + try + { + _db.Merge(key, value, null, WriteFlagsToWriteOptions(flags)); + } + catch (RocksDbSharpException e) + { + CreateMarkerIfCorrupt(e); + throw; + } + } + + internal void MergeWithColumnFamily(ReadOnlySpan key, ColumnFamilyHandle? cf, ReadOnlySpan value, WriteFlags flags = WriteFlags.None) + { + ObjectDisposedException.ThrowIf(_isDisposing, this); + + UpdateWriteMetrics(); + + try + { + _db.Merge(key, value, cf, WriteFlagsToWriteOptions(flags)); + } + catch (RocksDbSharpException e) + { + CreateMarkerIfCorrupt(e); + throw; + } + } + public void DangerousReleaseMemory(in ReadOnlySpan span) { if (!span.IsNullOrEmpty()) @@ -1222,6 +1268,13 @@ private static void ReturnWriteBatch(WriteBatch batch) _reusableWriteBatch = batch; } + public void Clear() + { + ObjectDisposedException.ThrowIf(_dbOnTheRocks._isDisposed, _dbOnTheRocks); + + _rocksBatch.Clear(); + } + public void Dispose() { ObjectDisposedException.ThrowIf(_dbOnTheRocks._isDisposed, _dbOnTheRocks); @@ -1280,6 +1333,21 @@ public void PutSpan(ReadOnlySpan key, ReadOnlySpan value, WriteFlags Set(key, value, null, flags); } + public void Merge(ReadOnlySpan key, ReadOnlySpan value, WriteFlags flags = WriteFlags.None) + { + Merge(key, value, null, flags); + } + + public void Merge(ReadOnlySpan key, ReadOnlySpan value, ColumnFamilyHandle? cf = null, WriteFlags flags = WriteFlags.None) + { + ObjectDisposedException.ThrowIf(_isDisposed, this); + + _rocksBatch.Merge(key, value, cf); + _writeFlags = flags; + + if ((flags & WriteFlags.DisableWAL) != 0) FlushOnTooManyWrites(); + } + private void FlushOnTooManyWrites() { if (Interlocked.Increment(ref _writeCount) % MaxWritesOnNoWal != 0) return; @@ -1306,6 +1374,13 @@ public void Flush(bool onlyWal = false) InnerFlush(onlyWal); } + public void FlushWithColumnFamily(ColumnFamilyHandle familyHandle) + { + ObjectDisposedException.ThrowIf(_isDisposing, this); + + InnerFlush(familyHandle); + } + public virtual void Compact() { _db.CompactRange(Keccak.Zero.BytesToArray(), Keccak.MaxValue.BytesToArray()); @@ -1328,6 +1403,18 @@ private void InnerFlush(bool onlyWal) } } + private void InnerFlush(ColumnFamilyHandle columnFamilyHandle) + { + try + { + _rocksDbNative.rocksdb_flush_cf(_db.Handle, FlushOptions.DefaultFlushOptions.Handle, columnFamilyHandle.Handle); + } + catch (RocksDbSharpException e) + { + CreateMarkerIfCorrupt(e); + } + } + public void Clear() { Dispose(); @@ -1776,10 +1863,10 @@ private sealed class ManagedIterators : ThreadLocal public void ClearIterators() { if (_disposed) return; - if (Values is null) return; - foreach (IteratorHolder iterator in Values) + if (Values is not { } values) return; + foreach (IteratorHolder iterator in values) { - iterator.Dispose(); + iterator?.Dispose(); } } diff --git a/src/Nethermind/Nethermind.Db.Rocks/MergeOperatorAdapter.cs b/src/Nethermind/Nethermind.Db.Rocks/MergeOperatorAdapter.cs new file mode 100644 index 000000000000..14bf9efc98f8 --- /dev/null +++ b/src/Nethermind/Nethermind.Db.Rocks/MergeOperatorAdapter.cs @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Nethermind.Core.Collections; +using RocksDbSharp; + +namespace Nethermind.Db.Rocks; + +// Also see RocksDbSharp.MergeOperatorImpl +internal class MergeOperatorAdapter(IMergeOperator inner) : MergeOperator +{ + public string Name => inner.Name; + + // TODO: fix and return array ptr instead of copying to unmanaged memory? + private static unsafe IntPtr GetResult(ArrayPoolList? data, out IntPtr resultLength, out IntPtr success) + { + if (data is null) + { + success = Convert.ToInt32(false); + resultLength = IntPtr.Zero; + return IntPtr.Zero; + } + + using (data) + { + void* resultPtr = NativeMemory.Alloc((uint)data.Count); + var result = new Span(resultPtr, data.Count); + data.AsSpan().CopyTo(result); + + resultLength = result.Length; + + // Fixing RocksDbSharp invalid callback signature, TODO: submit an issue/PR + Unsafe.SkipInit(out success); + Unsafe.As(ref success) = 1; + + return (IntPtr)resultPtr; + } + } + + unsafe IntPtr MergeOperator.PartialMerge( + IntPtr keyPtr, + UIntPtr keyLength, + IntPtr operandsList, + IntPtr operandsListLength, + int numOperands, + out IntPtr successPtr, + out IntPtr resultLength) + { + var key = new Span((void*)keyPtr, (int)keyLength); + var enumerator = new RocksDbMergeEnumerator(new((void*)operandsList, numOperands), new((void*)operandsListLength, numOperands)); + + ArrayPoolList? result = inner.PartialMerge(key, enumerator); + return GetResult(result, out resultLength, out successPtr); + } + + unsafe IntPtr MergeOperator.FullMerge( + IntPtr keyPtr, + UIntPtr keyLength, + IntPtr existingValuePtr, + UIntPtr existingValueLength, + IntPtr operandsList, + IntPtr operandsListLength, + int numOperands, + out IntPtr successPtr, + out IntPtr resultLength) + { + var key = new ReadOnlySpan((void*)keyPtr, (int)keyLength); + bool hasExistingValue = existingValuePtr != IntPtr.Zero; + Span existingValue = hasExistingValue ? new((void*)existingValuePtr, (int)existingValueLength) : Span.Empty; + var enumerator = new RocksDbMergeEnumerator(existingValue, hasExistingValue, new((void*)operandsList, numOperands), new((void*)operandsListLength, numOperands)); + + ArrayPoolList? result = inner.FullMerge(key, enumerator); + return GetResult(result, out resultLength, out successPtr); + } + + unsafe void MergeOperator.DeleteValue(IntPtr value, UIntPtr valueLength) => NativeMemory.Free((void*)value); +} diff --git a/src/Nethermind/Nethermind.Db.Rocks/RocksdbSortedView.cs b/src/Nethermind/Nethermind.Db.Rocks/RocksdbSortedView.cs index e617000d5320..a64fe85d5b49 100644 --- a/src/Nethermind/Nethermind.Db.Rocks/RocksdbSortedView.cs +++ b/src/Nethermind/Nethermind.Db.Rocks/RocksdbSortedView.cs @@ -22,6 +22,15 @@ public void Dispose() _iterator.Dispose(); } + public bool StartBefore(ReadOnlySpan value) + { + if (_started) + throw new InvalidOperationException($"{nameof(StartBefore)} can only be called before starting iteration."); + + _iterator.SeekForPrev(value); + return _started = _iterator.Valid(); + } + public bool MoveNext() { if (!_started) diff --git a/src/Nethermind/Nethermind.Db/CompressingDb.cs b/src/Nethermind/Nethermind.Db/CompressingDb.cs index d8284f2049e9..a03e69ff1036 100644 --- a/src/Nethermind/Nethermind.Db/CompressingDb.cs +++ b/src/Nethermind/Nethermind.Db/CompressingDb.cs @@ -44,6 +44,8 @@ private class WriteBatch : IWriteBatch public void Dispose() => _wrapped.Dispose(); + public void Clear() => _wrapped.Clear(); + public void Set(ReadOnlySpan key, byte[]? value, WriteFlags flags = WriteFlags.None) => _wrapped.Set(key, Compress(value), flags); @@ -52,6 +54,11 @@ public void PutSpan(ReadOnlySpan key, ReadOnlySpan value, WriteFlags _wrapped.PutSpan(key, Compress(value, stackalloc byte[value.Length]), flags); } + public void Merge(ReadOnlySpan key, ReadOnlySpan value, WriteFlags flags = WriteFlags.None) + { + throw new InvalidOperationException("EOA compressing DB does not support merging"); + } + public bool PreferWriteByArray => _wrapped.PreferWriteByArray; public byte[]? this[ReadOnlySpan key] diff --git a/src/Nethermind/Nethermind.Db/FullPruning/FullPruningDb.cs b/src/Nethermind/Nethermind.Db/FullPruning/FullPruningDb.cs index fb2c98942716..f79c010c9889 100755 --- a/src/Nethermind/Nethermind.Db/FullPruning/FullPruningDb.cs +++ b/src/Nethermind/Nethermind.Db/FullPruning/FullPruningDb.cs @@ -103,6 +103,12 @@ private void Duplicate(IWriteOnlyKeyValueStore db, ReadOnlySpan key, ReadO _updateDuplicateWriteMetrics?.Invoke(); } + private void DuplicateMerge(IMergeableKeyValueStore db, ReadOnlySpan key, ReadOnlySpan value, WriteFlags flags) + { + db.Merge(key, value, flags); + _updateDuplicateWriteMetrics?.Invoke(); + } + // we also need to duplicate writes that are in batches public IWriteBatch StartWriteBatch() => _pruningContext is null @@ -317,11 +323,23 @@ public void Dispose() _clonedWriteBatch.Dispose(); } + public void Clear() + { + _writeBatch.Clear(); + _clonedWriteBatch.Clear(); + } + public void Set(ReadOnlySpan key, byte[]? value, WriteFlags flags = WriteFlags.None) { _writeBatch.Set(key, value, flags); _db.Duplicate(_clonedWriteBatch, key, value, flags); } + + public void Merge(ReadOnlySpan key, ReadOnlySpan value, WriteFlags flags = WriteFlags.None) + { + _writeBatch.Merge(key, value, flags); + _db.DuplicateMerge(_clonedWriteBatch, key, value, flags); + } } public void Tune(ITunableDb.TuneType type) diff --git a/src/Nethermind/Nethermind.Db/IMergeOperator.cs b/src/Nethermind/Nethermind.Db/IMergeOperator.cs new file mode 100644 index 000000000000..7df2946bd71f --- /dev/null +++ b/src/Nethermind/Nethermind.Db/IMergeOperator.cs @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using Nethermind.Core.Collections; + +namespace Nethermind.Db; + +public interface IMergeOperator +{ + string Name { get; } + ArrayPoolList? FullMerge(ReadOnlySpan key, RocksDbMergeEnumerator enumerator); + ArrayPoolList? PartialMerge(ReadOnlySpan key, RocksDbMergeEnumerator enumerator); +} diff --git a/src/Nethermind/Nethermind.Db/InMemoryWriteBatch.cs b/src/Nethermind/Nethermind.Db/InMemoryWriteBatch.cs index 7c43ec39039c..c8054ac8b6d2 100644 --- a/src/Nethermind/Nethermind.Db/InMemoryWriteBatch.cs +++ b/src/Nethermind/Nethermind.Db/InMemoryWriteBatch.cs @@ -29,10 +29,20 @@ public void Dispose() GC.SuppressFinalize(this); } + public void Clear() + { + _currentItems.Clear(); + } + public void Set(ReadOnlySpan key, byte[]? value, WriteFlags flags = WriteFlags.None) { _currentItems[key.ToArray()] = value; _writeFlags = flags; } + + public void Merge(ReadOnlySpan key, ReadOnlySpan value, WriteFlags flags = WriteFlags.None) + { + throw new NotSupportedException("Merging is not supported by this implementation."); + } } } diff --git a/src/Nethermind/Nethermind.Db/RocksDbMergeEnumerator.cs b/src/Nethermind/Nethermind.Db/RocksDbMergeEnumerator.cs new file mode 100644 index 000000000000..fa2da2060b30 --- /dev/null +++ b/src/Nethermind/Nethermind.Db/RocksDbMergeEnumerator.cs @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; + +namespace Nethermind.Db; + +/// +/// RocksDB enumerator for values of merge operation. +/// +// Interface was not used because of ref struct limitations. +public readonly ref struct RocksDbMergeEnumerator(ReadOnlySpan operandsList, ReadOnlySpan operandsListLength) +{ + private readonly ReadOnlySpan _operandsList = operandsList; + private readonly ReadOnlySpan _operandsListLength = operandsListLength; + + public Span ExistingValue { get; } + public bool HasExistingValue { get; } + public int OperandsCount => _operandsList.Length; + public int TotalCount => OperandsCount + (HasExistingValue ? 1 : 0); + + public RocksDbMergeEnumerator( + Span existingValue, bool hasExistingValue, + ReadOnlySpan operandsList, ReadOnlySpan operandsListLength + ) : this(operandsList, operandsListLength) + { + ExistingValue = existingValue; + HasExistingValue = hasExistingValue; + } + + public Span GetExistingValue() + { + return HasExistingValue ? ExistingValue : default; + } + + public unsafe Span GetOperand(int index) + { + return new((void*)_operandsList[index], (int)_operandsListLength[index]); + } + + public Span Get(int index) + { + if (index == 0 && HasExistingValue) + return ExistingValue; + + if (HasExistingValue) + index -= 1; + + return GetOperand(index); + } +} diff --git a/src/Nethermind/Nethermind.Db/RocksDbSettings.cs b/src/Nethermind/Nethermind.Db/RocksDbSettings.cs index f3d9eebd4e67..d3ee2b073f39 100644 --- a/src/Nethermind/Nethermind.Db/RocksDbSettings.cs +++ b/src/Nethermind/Nethermind.Db/RocksDbSettings.cs @@ -1,6 +1,9 @@ // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using System; +using System.Collections.Generic; + namespace Nethermind.Db { public class DbSettings @@ -17,6 +20,9 @@ public DbSettings(string name, string path) public bool DeleteOnStart { get; set; } public bool CanDeleteFolder { get; set; } = true; + public IMergeOperator? MergeOperator { get; set; } + public Dictionary? ColumnsMergeOperators { get; set; } + public DbSettings Clone(string name, string path) { DbSettings settings = (DbSettings)MemberwiseClone(); diff --git a/src/Nethermind/Nethermind.JsonRpc.Test/Modules/SubscribeModuleTests.cs b/src/Nethermind/Nethermind.JsonRpc.Test/Modules/SubscribeModuleTests.cs index e05324b2d805..64c5f12b8676 100644 --- a/src/Nethermind/Nethermind.JsonRpc.Test/Modules/SubscribeModuleTests.cs +++ b/src/Nethermind/Nethermind.JsonRpc.Test/Modules/SubscribeModuleTests.cs @@ -128,7 +128,7 @@ private JsonRpcResult GetBlockAddedToMainResult(BlockReplacementEventArgs blockR })); _blockTree.BlockAddedToMain += Raise.EventWith(new object(), blockReplacementEventArgs); - _receiptStorage.ReceiptsInserted += Raise.EventWith(new object(), blockReplacementEventArgs); + _receiptStorage.NewCanonicalReceipts += Raise.EventWith(new object(), blockReplacementEventArgs); manualResetEvent.WaitOne(TimeSpan.FromMilliseconds(1000)).Should().Be(shouldReceiveResult); subscriptionId = newHeadSubscription.Id; @@ -148,7 +148,7 @@ private List GetLogsSubscriptionResult(Filter filter, BlockReplac })); _blockTree.BlockAddedToMain += Raise.EventWith(new object(), blockEventArgs); - _receiptStorage.ReceiptsInserted += Raise.EventWith(new object(), blockEventArgs); + _receiptStorage.NewCanonicalReceipts += Raise.EventWith(new object(), blockEventArgs); semaphoreSlim.Wait(TimeSpan.FromMilliseconds(500)); subscriptionId = logsSubscription.Id; @@ -769,7 +769,7 @@ public void LogsSubscription_should_not_send_logs_of_new_txs_on_ReceiptsInserted BlockReplacementEventArgs blockEventArgs = new(block); _blockTree.BlockAddedToMain += Raise.EventWith(new object(), blockEventArgs); - _receiptStorage.ReceiptsInserted += Raise.EventWith(new object(), blockEventArgs); + _receiptStorage.NewCanonicalReceipts += Raise.EventWith(new object(), blockEventArgs); manualResetEvent.WaitOne(TimeSpan.FromMilliseconds(200)); jsonRpcResults.Count.Should().Be(1); @@ -949,7 +949,7 @@ public async Task MultipleSubscriptions_concurrent_fast_messages(int messages) { BlockReplacementEventArgs eventArgs = new(Build.A.Block.TestObject); blockTree.BlockAddedToMain += Raise.EventWith(eventArgs); - _receiptStorage.ReceiptsInserted += Raise.EventWith(new object(), eventArgs); + _receiptStorage.NewCanonicalReceipts += Raise.EventWith(new object(), eventArgs); } }); @@ -1204,7 +1204,7 @@ public void LogsSubscription_can_send_logs_with_removed_txs_when_inserted() })); _blockTree.BlockAddedToMain += Raise.EventWith(new object(), blockEventArgs); - _receiptStorage.ReceiptsInserted += Raise.EventWith(new object(), blockEventArgs); + _receiptStorage.NewCanonicalReceipts += Raise.EventWith(new object(), blockEventArgs); manualResetEvent.WaitOne(TimeSpan.FromMilliseconds(2000)); jsonRpcResults.Count.Should().Be(1); diff --git a/src/Nethermind/Nethermind.Runner.Test/Ethereum/Steps/Migrations/ReceiptMigrationTests.cs b/src/Nethermind/Nethermind.Runner.Test/Ethereum/Steps/Migrations/ReceiptMigrationTests.cs index 02a695382dfd..6fb33121c458 100644 --- a/src/Nethermind/Nethermind.Runner.Test/Ethereum/Steps/Migrations/ReceiptMigrationTests.cs +++ b/src/Nethermind/Nethermind.Runner.Test/Ethereum/Steps/Migrations/ReceiptMigrationTests.cs @@ -160,7 +160,8 @@ public void RemoveReceipts(Block block) } #pragma warning disable CS0067 - public event EventHandler ReceiptsInserted; + public event EventHandler NewCanonicalReceipts; + public event EventHandler ReceiptsInserted; #pragma warning restore CS0067 } } diff --git a/src/Nethermind/Nethermind.Trie.Test/Pruning/TreeStoreTests.cs b/src/Nethermind/Nethermind.Trie.Test/Pruning/TreeStoreTests.cs index f5267e2b0d22..ded8904a96ce 100644 --- a/src/Nethermind/Nethermind.Trie.Test/Pruning/TreeStoreTests.cs +++ b/src/Nethermind/Nethermind.Trie.Test/Pruning/TreeStoreTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using FluentAssertions; using Nethermind.Core; @@ -601,6 +602,17 @@ public void Set(ReadOnlySpan key, byte[]? value, WriteFlags flags = WriteF { _inBatched[key.ToArray()] = value; } + + public void Merge(ReadOnlySpan key, ReadOnlySpan value, WriteFlags flags = WriteFlags.None) + { + var keyArr = key.ToArray(); + _inBatched[keyArr] = (_inBatched.GetValueOrDefault(keyArr) ?? []).Concat(value.ToArray()).ToArray(); + } + + public void Clear() + { + _inBatched.Clear(); + } } }