diff --git a/src/Nethermind/Nethermind.Db/IPruningConfig.cs b/src/Nethermind/Nethermind.Db/IPruningConfig.cs index 354025bc1d1..ed3396a3d59 100755 --- a/src/Nethermind/Nethermind.Db/IPruningConfig.cs +++ b/src/Nethermind/Nethermind.Db/IPruningConfig.cs @@ -92,4 +92,7 @@ public interface IPruningConfig : IConfig [ConfigItem(Description = "Whether to enables available disk space check.", DefaultValue = "true")] bool AvailableSpaceCheckEnabled { get; set; } + + [ConfigItem(Description = "[TECHNICAL] Number of past persisted keys to keep track off for possible pruning.", DefaultValue = "1000000")] + int TrackedPastKeyCount { get; set; } } diff --git a/src/Nethermind/Nethermind.Db/MemDb.cs b/src/Nethermind/Nethermind.Db/MemDb.cs index 0baf1dce4d1..12712c4023b 100644 --- a/src/Nethermind/Nethermind.Db/MemDb.cs +++ b/src/Nethermind/Nethermind.Db/MemDb.cs @@ -141,6 +141,11 @@ public virtual void Set(ReadOnlySpan key, byte[]? value, WriteFlags flags } WritesCount++; + if (value == null) + { + _db.TryRemove(key, out _); + return; + } _db[key] = value; } } diff --git a/src/Nethermind/Nethermind.Db/PruningConfig.cs b/src/Nethermind/Nethermind.Db/PruningConfig.cs index 102720f2a6c..d6aa2f8d190 100755 --- a/src/Nethermind/Nethermind.Db/PruningConfig.cs +++ b/src/Nethermind/Nethermind.Db/PruningConfig.cs @@ -32,5 +32,6 @@ public bool Enabled public int FullPruningMinimumDelayHours { get; set; } = 240; public FullPruningCompletionBehavior FullPruningCompletionBehavior { get; set; } = FullPruningCompletionBehavior.None; public bool AvailableSpaceCheckEnabled { get; set; } = true; + public int TrackedPastKeyCount { get; set; } = 1000000; } } diff --git a/src/Nethermind/Nethermind.Init/InitializeStateDb.cs b/src/Nethermind/Nethermind.Init/InitializeStateDb.cs index bd39d3b86b1..f4ade26c64a 100644 --- a/src/Nethermind/Nethermind.Init/InitializeStateDb.cs +++ b/src/Nethermind/Nethermind.Init/InitializeStateDb.cs @@ -90,7 +90,8 @@ public Task Execute(CancellationToken cancellationToken) persistenceStrategy = persistenceStrategy.Or(triggerPersistenceStrategy); } - pruningStrategy = Prune.WhenCacheReaches(pruningConfig.CacheMb.MB()); // TODO: memory hint should define this + pruningStrategy = Prune.WhenCacheReaches(pruningConfig.CacheMb.MB()) // TODO: memory hint should define this + .TrackingPastKeys(pruningConfig.TrackedPastKeyCount); } else { diff --git a/src/Nethermind/Nethermind.Trie.Test/Pruning/TestPruningStrategy.cs b/src/Nethermind/Nethermind.Trie.Test/Pruning/TestPruningStrategy.cs index ef9fb10b020..c3f4fcfe6de 100644 --- a/src/Nethermind/Nethermind.Trie.Test/Pruning/TestPruningStrategy.cs +++ b/src/Nethermind/Nethermind.Trie.Test/Pruning/TestPruningStrategy.cs @@ -8,10 +8,13 @@ namespace Nethermind.Trie.Test.Pruning public class TestPruningStrategy : IPruningStrategy { private readonly bool _pruningEnabled; - public TestPruningStrategy(bool pruningEnabled, bool shouldPrune = false) + private readonly int _trackedPastKeyCount; + + public TestPruningStrategy(bool pruningEnabled, bool shouldPrune = false, int trackedPastKeyCount = 0) { _pruningEnabled = pruningEnabled; ShouldPruneEnabled = shouldPrune; + _trackedPastKeyCount = trackedPastKeyCount; } public bool PruningEnabled => _pruningEnabled; @@ -27,5 +30,7 @@ public bool ShouldPrune(in long currentMemory) return false; } + + public int TrackedPastKeyCount => _trackedPastKeyCount; } } diff --git a/src/Nethermind/Nethermind.Trie.Test/Pruning/TreeStoreTests.cs b/src/Nethermind/Nethermind.Trie.Test/Pruning/TreeStoreTests.cs index 06eb01cfbe6..35c20ecf02b 100644 --- a/src/Nethermind/Nethermind.Trie.Test/Pruning/TreeStoreTests.cs +++ b/src/Nethermind/Nethermind.Trie.Test/Pruning/TreeStoreTests.cs @@ -35,12 +35,22 @@ public TreeStoreTests(INodeStorage.KeyScheme scheme) _scheme = scheme; } - public TrieStore CreateTrieStore(IPruningStrategy? pruningStrategy = null, IKeyValueStoreWithBatching? kvStore = null, IPersistenceStrategy? persistenceStrategy = null) + private TrieStore CreateTrieStore( + IPruningStrategy? pruningStrategy = null, + IKeyValueStoreWithBatching? kvStore = null, + IPersistenceStrategy? persistenceStrategy = null, + long? reorgDepthOverride = null + ) { pruningStrategy ??= No.Pruning; kvStore ??= new TestMemDb(); persistenceStrategy ??= No.Persistence; - return new(new NodeStorage(kvStore, _scheme, requirePath: _scheme == INodeStorage.KeyScheme.HalfPath), pruningStrategy, persistenceStrategy, _logManager); + return new( + new NodeStorage(kvStore, _scheme, requirePath: _scheme == INodeStorage.KeyScheme.HalfPath), + pruningStrategy, + persistenceStrategy, + _logManager, + reorgDepthOverride: reorgDepthOverride); } [SetUp] @@ -811,5 +821,64 @@ public void After_commit_should_have_has_root() stateTree.Commit(0); trieStore.HasRoot(stateTree.RootHash).Should().BeTrue(); } + + public async Task Will_RemovePastKeys_OnSnapshot() + { + MemDb memDb = new(); + + using TrieStore fullTrieStore = CreateTrieStore( + kvStore: memDb, + pruningStrategy: new TestPruningStrategy(true, true, 100000), + persistenceStrategy: No.Persistence, + reorgDepthOverride: 2); + + IScopedTrieStore trieStore = fullTrieStore.GetTrieStore(null); + + for (int i = 0; i < 64; i++) + { + TrieNode node = new(NodeType.Leaf, TestItem.Keccaks[i], new byte[2]); + trieStore.CommitNode(i, new NodeCommitInfo(node, TreePath.Empty)); + trieStore.FinishBlockCommit(TrieType.State, i, node); + + // Pruning is done in background + await Task.Delay(TimeSpan.FromMilliseconds(10)); + } + + if (_scheme == INodeStorage.KeyScheme.Hash) + { + memDb.Count.Should().NotBe(1); + } + else + { + memDb.Count.Should().Be(1); + } + } + + [Test] + public async Task Will_NotRemove_ReCommittedNode() + { + MemDb memDb = new(); + + using TrieStore fullTrieStore = CreateTrieStore( + kvStore: memDb, + pruningStrategy: new TestPruningStrategy(true, true, 100000), + persistenceStrategy: No.Persistence, + reorgDepthOverride: 2); + + IScopedTrieStore trieStore = fullTrieStore.GetTrieStore(null); + + for (int i = 0; i < 64; i++) + { + TrieNode node = new(NodeType.Leaf, TestItem.Keccaks[i % 4], new byte[2]); + trieStore.CommitNode(i, new NodeCommitInfo(node, TreePath.Empty)); + node = trieStore.FindCachedOrUnknown(TreePath.Empty, node.Keccak); + trieStore.FinishBlockCommit(TrieType.State, i, node); + + // Pruning is done in background + await Task.Delay(TimeSpan.FromMilliseconds(10)); + } + + memDb.Count.Should().Be(4); + } } } diff --git a/src/Nethermind/Nethermind.Trie.Test/PruningScenariosTests.cs b/src/Nethermind/Nethermind.Trie.Test/PruningScenariosTests.cs index 4ca81876087..604806f45f7 100644 --- a/src/Nethermind/Nethermind.Trie.Test/PruningScenariosTests.cs +++ b/src/Nethermind/Nethermind.Trie.Test/PruningScenariosTests.cs @@ -76,7 +76,7 @@ public static PruningContext InMemory public static PruningContext InMemoryAlwaysPrune { [DebuggerStepThrough] - get => new(new TestPruningStrategy(true, true), No.Persistence); + get => new(new TestPruningStrategy(true, true, 1000000), No.Persistence); } public static PruningContext SetupWithPersistenceEveryEightBlocks @@ -746,5 +746,34 @@ public void StateRoot_reset_at_lower_level_and_accessed_at_just_the_right_time() .VerifyAccountBalance(3, 101); } + + [Test] + public void When_Reorg_OldValueIsNotRemoved() + { + Reorganization.MaxDepth = 2; + + PruningContext.InMemoryAlwaysPrune + .SetAccountBalance(1, 100) + .SetAccountBalance(2, 100) + .Commit() + + .SetAccountBalance(3, 100) + .SetAccountBalance(4, 100) + .Commit() + + .SaveBranchingPoint("revert_main") + + .SetAccountBalance(4, 200) + .Commit() + + .RestoreBranchingPoint("revert_main") + + .Commit() + .Commit() + .Commit() + .Commit() + + .VerifyAccountBalance(4, 100); + } } } diff --git a/src/Nethermind/Nethermind.Trie.Test/TinyTreePathTests.cs b/src/Nethermind/Nethermind.Trie.Test/TinyTreePathTests.cs new file mode 100644 index 00000000000..a2d51d1a5a1 --- /dev/null +++ b/src/Nethermind/Nethermind.Trie.Test/TinyTreePathTests.cs @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using FluentAssertions; +using Nethermind.Core.Crypto; +using NUnit.Framework; + +namespace Nethermind.Trie.Test; + +public class TinyTreePathTests +{ + [Test] + public void Should_ConvertFromAndToTreePath() + { + TreePath path = new TreePath(new ValueHash256("0123456789abcd00000000000000000000000000000000000000000000000000"), 14); + + TinyTreePath tinyPath = new TinyTreePath(path); + + tinyPath.ToTreePath().Should().Be(path); + } + + [Test] + public void When_PathIsTooLong_Should_Throw() + { + TreePath path = new TreePath(new ValueHash256("0123456789000000000000000000000000000000000000000000000000000000"), 15); + + Action act = () => new TinyTreePath(path); + act.Should().Throw(); + } +} + diff --git a/src/Nethermind/Nethermind.Trie/INodeStorage.cs b/src/Nethermind/Nethermind.Trie/INodeStorage.cs index 64a85531b75..66b7c686787 100644 --- a/src/Nethermind/Nethermind.Trie/INodeStorage.cs +++ b/src/Nethermind/Nethermind.Trie/INodeStorage.cs @@ -44,5 +44,6 @@ public enum KeyScheme public interface WriteBatch : IDisposable { void Set(Hash256? address, in TreePath path, in ValueHash256 currentNodeKeccak, byte[] toArray, WriteFlags writeFlags); + void Remove(Hash256? address, in TreePath path, in ValueHash256 currentNodeKeccak); } } diff --git a/src/Nethermind/Nethermind.Trie/NodeStorage.cs b/src/Nethermind/Nethermind.Trie/NodeStorage.cs index d987aff5bda..28bf3dc5243 100644 --- a/src/Nethermind/Nethermind.Trie/NodeStorage.cs +++ b/src/Nethermind/Nethermind.Trie/NodeStorage.cs @@ -220,5 +220,11 @@ public void Set(Hash256? address, in TreePath path, in ValueHash256 keccak, byte _writeBatch.Set(_nodeStorage.GetExpectedPath(stackalloc byte[StoragePathLength], address, path, keccak), toArray, writeFlags); } + + public void Remove(Hash256? address, in TreePath path, in ValueHash256 keccak) + { + // Only delete half path key. DO NOT delete hash based key. + _writeBatch.Remove(GetHalfPathNodeStoragePathSpan(stackalloc byte[StoragePathLength], address, path, keccak)); + } } } diff --git a/src/Nethermind/Nethermind.Trie/Pruning/ISnapshotStrategy.cs b/src/Nethermind/Nethermind.Trie/Pruning/ISnapshotStrategy.cs index ba4a9ba13ef..70272411373 100644 --- a/src/Nethermind/Nethermind.Trie/Pruning/ISnapshotStrategy.cs +++ b/src/Nethermind/Nethermind.Trie/Pruning/ISnapshotStrategy.cs @@ -7,5 +7,6 @@ public interface IPruningStrategy { bool PruningEnabled { get; } bool ShouldPrune(in long currentMemory); + int TrackedPastKeyCount { get; } } } diff --git a/src/Nethermind/Nethermind.Trie/Pruning/MemoryLimit.cs b/src/Nethermind/Nethermind.Trie/Pruning/MemoryLimit.cs index 0fbd8949d25..da2e9714c1e 100644 --- a/src/Nethermind/Nethermind.Trie/Pruning/MemoryLimit.cs +++ b/src/Nethermind/Nethermind.Trie/Pruning/MemoryLimit.cs @@ -21,5 +21,7 @@ public bool ShouldPrune(in long currentMemory) { return PruningEnabled && currentMemory >= _memoryLimit; } + + public int TrackedPastKeyCount => 0; } } diff --git a/src/Nethermind/Nethermind.Trie/Pruning/NoPruning.cs b/src/Nethermind/Nethermind.Trie/Pruning/NoPruning.cs index 50303ea515c..a1418e7c887 100644 --- a/src/Nethermind/Nethermind.Trie/Pruning/NoPruning.cs +++ b/src/Nethermind/Nethermind.Trie/Pruning/NoPruning.cs @@ -15,5 +15,7 @@ public bool ShouldPrune(in long currentMemory) { return false; } + + public int TrackedPastKeyCount => 0; } } diff --git a/src/Nethermind/Nethermind.Trie/Pruning/Prune.cs b/src/Nethermind/Nethermind.Trie/Pruning/Prune.cs index 19c26b41490..7ba5acfba9b 100644 --- a/src/Nethermind/Nethermind.Trie/Pruning/Prune.cs +++ b/src/Nethermind/Nethermind.Trie/Pruning/Prune.cs @@ -7,5 +7,10 @@ public static class Prune { public static IPruningStrategy WhenCacheReaches(long sizeInBytes) => new MemoryLimit(sizeInBytes); + + public static IPruningStrategy TrackingPastKeys(this IPruningStrategy baseStrategy, int trackedPastKeyCount) + => trackedPastKeyCount <= 0 + ? baseStrategy + : new TrackedPastKeyCountStrategy(baseStrategy, trackedPastKeyCount); } } diff --git a/src/Nethermind/Nethermind.Trie/Pruning/TinyTreePath.cs b/src/Nethermind/Nethermind.Trie/Pruning/TinyTreePath.cs new file mode 100644 index 00000000000..b0aa85ff870 --- /dev/null +++ b/src/Nethermind/Nethermind.Trie/Pruning/TinyTreePath.cs @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Nethermind.Core.Crypto; + +namespace Nethermind.Trie; + +/// +/// Like TreePath, but tiny. Fit in 8 byte, like a long. Can only represent 14 nibble. +/// +public struct TinyTreePath +{ + public const int MaxNibbleLength = 14; + + long _data; + + // Eh.. readonly? + private Span AsSpan => MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref _data, 1)); + + public TinyTreePath(in TreePath path) + { + if (path.Length > MaxNibbleLength) throw new InvalidOperationException("Unable to represent more than 14 nibble"); + Span pathSpan = path.Path.BytesAsSpan; + Span selfSpan = AsSpan; + pathSpan[..7].CopyTo(selfSpan); + selfSpan[7] = (byte)path.Length; + } + + public int Length => AsSpan[7]; + + public TreePath ToTreePath() + { + ValueHash256 rawPath = Keccak.Zero; + Span pathSpan = rawPath.BytesAsSpan; + Span selfSpan = AsSpan; + selfSpan[..7].CopyTo(pathSpan); + + return new TreePath(rawPath, selfSpan[7]); + } +} diff --git a/src/Nethermind/Nethermind.Trie/Pruning/TrackedPastKeyCountStrategy.cs b/src/Nethermind/Nethermind.Trie/Pruning/TrackedPastKeyCountStrategy.cs new file mode 100644 index 00000000000..2f21eef4666 --- /dev/null +++ b/src/Nethermind/Nethermind.Trie/Pruning/TrackedPastKeyCountStrategy.cs @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Trie.Pruning; + +public class TrackedPastKeyCountStrategy : IPruningStrategy +{ + private IPruningStrategy _baseStrategy; + private readonly int _trackedPastKeyCount; + public bool PruningEnabled => _baseStrategy.PruningEnabled; + + public TrackedPastKeyCountStrategy(IPruningStrategy baseStrategy, int trackedPastKeyCount) + { + _baseStrategy = baseStrategy; + _trackedPastKeyCount = trackedPastKeyCount; + } + + public bool ShouldPrune(in long currentMemory) + { + return _baseStrategy.ShouldPrune(in currentMemory); + } + + public int TrackedPastKeyCount => _trackedPastKeyCount; +} diff --git a/src/Nethermind/Nethermind.Trie/Pruning/TrieStore.cs b/src/Nethermind/Nethermind.Trie/Pruning/TrieStore.cs index 8453fca762b..2343bd85bbf 100644 --- a/src/Nethermind/Nethermind.Trie/Pruning/TrieStore.cs +++ b/src/Nethermind/Nethermind.Trie/Pruning/TrieStore.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Nethermind.Core; +using Nethermind.Core.Caching; using Nethermind.Core.Collections; using Nethermind.Core.Crypto; using Nethermind.Core.Extensions; @@ -247,10 +248,20 @@ public readonly void Dispose() private readonly DirtyNodesCache _dirtyNodes; + // Track some of the persisted path hash. Used to be able to remove keys when it is replaced. + // If null, disable removing key. + private LruCache<(Hash256?, TinyTreePath), ValueHash256>? _pastPathHash; + + // Track ALL of the recently re-committed persisted nodes. This is so that we don't accidentally remove + // recommitted persisted nodes (which will not get re-persisted). + private ConcurrentDictionary<(Hash256?, TinyTreePath, ValueHash256), long> _persistedLastSeens = new(); + private bool _lastPersistedReachedReorgBoundary; private Task _pruningTask = Task.CompletedTask; private readonly CancellationTokenSource _pruningTaskCancellationTokenSource = new(); + private long _reorgDepth = Reorganization.MaxDepth; + public TrieStore(IKeyValueStoreWithBatching? keyValueStore, ILogManager? logManager) : this(keyValueStore, No.Pruning, Pruning.Persist.EveryBlock, logManager) { @@ -273,7 +284,8 @@ public TrieStore( INodeStorage? nodeStorage, IPruningStrategy? pruningStrategy, IPersistenceStrategy? persistenceStrategy, - ILogManager? logManager) + ILogManager? logManager, + long? reorgDepthOverride = null) { _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); _nodeStorage = nodeStorage ?? throw new ArgumentNullException(nameof(nodeStorage)); @@ -281,6 +293,17 @@ public TrieStore( _persistenceStrategy = persistenceStrategy ?? throw new ArgumentNullException(nameof(persistenceStrategy)); _dirtyNodes = new DirtyNodesCache(this); _publicStore = new TrieKeyValueStore(this); + + if (reorgDepthOverride != null) _reorgDepth = reorgDepthOverride.Value; + + if (pruningStrategy.TrackedPastKeyCount > 0 && nodeStorage.RequirePath) + { + _pastPathHash = new(pruningStrategy.TrackedPastKeyCount, ""); + } + else + { + _pastPathHash = null; + } } public IScopedTrieStore GetTrieStore(Hash256? address) @@ -437,7 +460,7 @@ public void FinishBlockCommit(TrieType trieType, long blockNumber, Hash256? addr _currentBatch ??= _nodeStorage.StartWriteBatch(); try { - PersistBlockCommitSet(address, set, _currentBatch, writeFlags); + PersistBlockCommitSet(address, set, _currentBatch, writeFlags: writeFlags); PruneCurrentSet(); } finally @@ -592,7 +615,7 @@ private bool CanPruneCacheFurther() using ArrayPoolList candidateSets = new(_commitSetQueue.Count); while (_commitSetQueue.TryDequeue(out BlockCommitSet frontSet)) { - if (frontSet!.BlockNumber >= LatestCommittedBlockNumber - Reorganization.MaxDepth) + if (frontSet!.BlockNumber >= LatestCommittedBlockNumber - _reorgDepth) { toAddBack.Add(frontSet); } @@ -613,12 +636,28 @@ private bool CanPruneCacheFurther() _commitSetQueue.Enqueue(toAddBack[index]); } + Dictionary<(Hash256?, TinyTreePath), Hash256?>? persistedHashes = + // If its a reorg, we can't remove node as persisted node may not be canonical + _pastPathHash != null && candidateSets.Count == 1 + ? new Dictionary<(Hash256?, TinyTreePath), Hash256?>() + : null; + INodeStorage.WriteBatch writeBatch = _nodeStorage.StartWriteBatch(); for (int index = 0; index < candidateSets.Count; index++) { BlockCommitSet blockCommitSet = candidateSets[index]; if (_logger.IsDebug) _logger.Debug($"Elevated pruning for candidate {blockCommitSet.BlockNumber}"); - PersistBlockCommitSet(null, blockCommitSet, writeBatch); + PersistBlockCommitSet(null, blockCommitSet, writeBatch, persistedHashes: persistedHashes); + } + + RemovePastKeys(persistedHashes); + + foreach (KeyValuePair<(Hash256, TinyTreePath, ValueHash256), long> keyValuePair in _persistedLastSeens) + { + if (IsNoLongerNeeded(keyValuePair.Value)) + { + _persistedLastSeens.Remove(keyValuePair.Key, out _); + } } writeBatch.Dispose(); @@ -628,12 +667,51 @@ private bool CanPruneCacheFurther() } _commitSetQueue.TryPeek(out BlockCommitSet? uselessFrontSet); - if (_logger.IsDebug) _logger.Debug($"Found no candidate for elevated pruning (sets: {_commitSetQueue.Count}, earliest: {uselessFrontSet?.BlockNumber}, newest kept: {LatestCommittedBlockNumber}, reorg depth {Reorganization.MaxDepth})"); + if (_logger.IsDebug) _logger.Debug($"Found no candidate for elevated pruning (sets: {_commitSetQueue.Count}, earliest: {uselessFrontSet?.BlockNumber}, newest kept: {LatestCommittedBlockNumber}, reorg depth {_reorgDepth})"); } return false; } + private void RemovePastKeys(Dictionary<(Hash256, TinyTreePath), Hash256?>? persistedHashes) + { + if (persistedHashes == null) return; + + bool CanRemove(Hash256? address, TinyTreePath path, TreePath fullPath, ValueHash256 keccak, Hash256? currentlyPersistingKeccak) + { + // Multiple current hash that we don't keep track for simplicity. Just ignore this case. + if (currentlyPersistingKeccak == null) return false; + + // The persisted hash is the same as currently persisting hash. Do nothing. + if (currentlyPersistingKeccak == keccak) return false; + + // We have is in cache and it is still needed. + if (_dirtyNodes.TryGetValue(new DirtyNodesCache.Key(address, fullPath, keccak.ToCommitment()), out TrieNode node) && + !IsNoLongerNeeded(node)) return false; + + // We don't have it in cache, but we know it was re-committed, so if it is still needed, don't remove + if (_persistedLastSeens.TryGetValue((address, path, keccak), out long commitBlock) && + !IsNoLongerNeeded(commitBlock)) return false; + + return true; + } + + using INodeStorage.WriteBatch deleteNodeBatch = _nodeStorage.StartWriteBatch(); + foreach (KeyValuePair<(Hash256?, TinyTreePath), Hash256> keyValuePair in persistedHashes) + { + (Hash256? addr, TinyTreePath path) key = keyValuePair.Key; + if (key.path.Length > TinyTreePath.MaxNibbleLength) continue; + if (_pastPathHash.TryGet((key.addr, key.path), out ValueHash256 prevHash)) + { + TreePath fullPath = key.path.ToTreePath(); // Micro op to reduce double convert + if (CanRemove(key.addr, key.path, fullPath, prevHash, keyValuePair.Value)) + { + deleteNodeBatch.Remove(key.addr, fullPath, prevHash); + } + } + } + } + /// /// Prunes persisted branches of the current commit set root. /// @@ -668,6 +746,9 @@ private void PruneCache() if (node.IsPersisted) { if (_logger.IsTrace) _logger.Trace($"Removing persisted {node} from memory."); + + TrackPrunedPersistedNodes(key, node); + Hash256? keccak = node.Keccak; if (keccak is null) { @@ -702,7 +783,7 @@ private void PruneCache() } } - MemoryUsedByDirtyCache = newMemory; + MemoryUsedByDirtyCache = newMemory + _persistedLastSeens.Count * 48; Metrics.CachedNodesCount = _dirtyNodes.Count; stopwatch.Stop(); @@ -710,6 +791,28 @@ private void PruneCache() if (_logger.IsDebug) _logger.Debug($"Finished pruning nodes in {stopwatch.ElapsedMilliseconds}ms {MemoryUsedByDirtyCache / 1.MB()} MB, last persisted block: {LastPersistedBlockNumber} current: {LatestCommittedBlockNumber}."); } + private void TrackPrunedPersistedNodes(in DirtyNodesCache.Key key, TrieNode node) + { + if (key.Path.Length > TinyTreePath.MaxNibbleLength) return; + if (_pastPathHash == null) return; + + TinyTreePath treePath = new TinyTreePath(key.Path); + // Persisted node with LastSeen is a node that has been re-committed, likely due to processing + // recalculated to the same hash. + if (node.LastSeen != null) + { + // Update _persistedLastSeen to later value. + if (!_persistedLastSeens.TryGetValue((key.Address, treePath, key.Keccak), out long currentLastSeen) || currentLastSeen < node.LastSeen.Value) + { + _persistedLastSeens[(key.Address, treePath, key.Keccak)] = node.LastSeen.Value; + } + } + + // This persisted node is being removed from cache. Keep it in mind in case of an update to the same + // path. + _pastPathHash.Set((key.Address, treePath), key.Keccak); + } + /// /// This method is here to support testing. /// @@ -775,10 +878,37 @@ private void CreateCommitSet(long blockNumber) /// Already persisted nodes are skipped. After this action we are sure that the full state is available /// for the block represented by this commit set. /// + /// /// A commit set of a block which root is to be persisted. - private void PersistBlockCommitSet(Hash256? address, BlockCommitSet commitSet, INodeStorage.WriteBatch writeBatch, WriteFlags writeFlags = WriteFlags.None) + /// The write batch to write to + /// Track persisted hashes in this dictionary if not null + /// + private void PersistBlockCommitSet( + Hash256? address, + BlockCommitSet commitSet, + INodeStorage.WriteBatch writeBatch, + Dictionary<(Hash256?, TinyTreePath), Hash256?>? persistedHashes = null, + WriteFlags writeFlags = WriteFlags.None + ) { - void PersistNode(TrieNode tn, Hash256? address2, TreePath path) => this.PersistNode(address2, path, tn, commitSet.BlockNumber, writeFlags, writeBatch); + void PersistNode(TrieNode tn, Hash256? address2, TreePath path) + { + if (persistedHashes != null && path.Length <= TinyTreePath.MaxNibbleLength) + { + (Hash256 address2, TinyTreePath path) key = (address2, new TinyTreePath(path)); + if (persistedHashes.ContainsKey(key)) + { + // Null mark that there are multiple saved hash for this path. So we don't attempt to remove anything. + // Otherwise this would have to be a list, which is such a rare case that its not worth it to have a list. + persistedHashes[key] = null; + } + else + { + persistedHashes[key] = tn.Keccak; + } + } + this.PersistNode(address2, path, tn, commitSet.BlockNumber, writeFlags, writeBatch); + } if (_logger.IsDebug) _logger.Debug($"Persisting from root {commitSet.Root} in {commitSet.BlockNumber}"); @@ -822,18 +952,23 @@ private void PersistNode(Hash256? address, in TreePath path, TrieNode currentNod private bool IsNoLongerNeeded(TrieNode node) { - Debug.Assert(node.LastSeen.HasValue, $"Any node that is cache should have {nameof(TrieNode.LastSeen)} set."); - return node.LastSeen < LastPersistedBlockNumber - && node.LastSeen < LatestCommittedBlockNumber - Reorganization.MaxDepth; + return IsNoLongerNeeded(node.LastSeen); + } + + private bool IsNoLongerNeeded(long? lastSeen) + { + Debug.Assert(lastSeen.HasValue, $"Any node that is cache should have {nameof(TrieNode.LastSeen)} set."); + return lastSeen < LastPersistedBlockNumber + && lastSeen < LatestCommittedBlockNumber - _reorgDepth; } private void DequeueOldCommitSets() { while (_commitSetQueue.TryPeek(out BlockCommitSet blockCommitSet)) { - if (blockCommitSet.BlockNumber < LatestCommittedBlockNumber - Reorganization.MaxDepth - 1) + if (blockCommitSet.BlockNumber < LatestCommittedBlockNumber - _reorgDepth - 1) { - if (_logger.IsDebug) _logger.Debug($"Removing historical ({_commitSetQueue.Count}) {blockCommitSet.BlockNumber} < {LatestCommittedBlockNumber} - {Reorganization.MaxDepth}"); + if (_logger.IsDebug) _logger.Debug($"Removing historical ({_commitSetQueue.Count}) {blockCommitSet.BlockNumber} < {LatestCommittedBlockNumber} - {_reorgDepth}"); _commitSetQueue.TryDequeue(out _); } else @@ -882,7 +1017,7 @@ private void AnnounceReorgBoundaries() { // even after we persist a block we do not really remember it as a safe checkpoint // until max reorgs blocks after - if (LatestCommittedBlockNumber >= LastPersistedBlockNumber + Reorganization.MaxDepth) + if (LatestCommittedBlockNumber >= LastPersistedBlockNumber + _reorgDepth) { shouldAnnounceReorgBoundary = true; } @@ -906,11 +1041,11 @@ private void PersistOnShutdown() using ArrayPoolList candidateSets = new(_commitSetQueue.Count); while (_commitSetQueue.TryDequeue(out BlockCommitSet frontSet)) { - if (candidateSets.Count == 0 || candidateSets[0].BlockNumber == frontSet!.BlockNumber) + if (!frontSet.IsSealed || candidateSets.Count == 0 || candidateSets[0].BlockNumber == frontSet!.BlockNumber) { candidateSets.Add(frontSet); } - else if (frontSet!.BlockNumber < LatestCommittedBlockNumber - Reorganization.MaxDepth + else if (frontSet!.BlockNumber < LatestCommittedBlockNumber - _reorgDepth && frontSet!.BlockNumber > candidateSets[0].BlockNumber) { candidateSets.Clear(); diff --git a/src/Nethermind/Nethermind.Trie/Utils/WriteBatcher.cs b/src/Nethermind/Nethermind.Trie/Utils/WriteBatcher.cs index f4db89afc31..1ff14e28f58 100644 --- a/src/Nethermind/Nethermind.Trie/Utils/WriteBatcher.cs +++ b/src/Nethermind/Nethermind.Trie/Utils/WriteBatcher.cs @@ -52,4 +52,24 @@ public void Set(Hash256? address, in TreePath path, in ValueHash256 currentNodeK _batches.Enqueue(currentBatch); } } + + public void Remove(Hash256? address, in TreePath path, in ValueHash256 currentNodeKeccak) + { + if (_disposing) throw new InvalidOperationException("Trying to set while disposing"); + if (!_batches.TryDequeue(out INodeStorage.WriteBatch? currentBatch)) + { + currentBatch = _underlyingDb.StartWriteBatch(); + } + + currentBatch.Remove(address, path, currentNodeKeccak); + long val = Interlocked.Increment(ref _counter); + if (val % 10000 == 0) + { + currentBatch.Dispose(); + } + else + { + _batches.Enqueue(currentBatch); + } + } }