-
Notifications
You must be signed in to change notification settings - Fork 696
fix(merge): bound dangling block cache growth #11426
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
895a27f
68d2b3e
79867ea
019f1eb
73f1df1
13caefd
d358e02
595626a
968a147
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited | ||
| // SPDX-License-Identifier: LGPL-3.0-only | ||
|
|
||
| using FluentAssertions; | ||
| using Nethermind.Core; | ||
| using Nethermind.Core.Crypto; | ||
| using Nethermind.Core.Test.Builders; | ||
| using Nethermind.Merge.Plugin.Handlers; | ||
| using NUnit.Framework; | ||
|
|
||
| namespace Nethermind.Merge.Plugin.Test; | ||
|
|
||
| [TestFixture] | ||
| public class BlockCacheServiceTest | ||
| { | ||
| [Test] | ||
| public void prunes_highest_unprotected_block_and_returns_false_when_added_block_is_pruned() | ||
| { | ||
| BlockCacheService blockCacheService = new(2); | ||
| Block block1 = Build.A.Block.WithNumber(1).TestObject; | ||
| Block block2 = Build.A.Block.WithNumber(2).TestObject; | ||
| Block block3 = Build.A.Block.WithNumber(3).TestObject; | ||
| Hash256 block1Hash = block1.GetOrCalculateHash(); | ||
|
Check failure on line 23 in src/Nethermind/Nethermind.Merge.Plugin.Test/BlockCacheServiceTest.cs
|
||
| Hash256 block2Hash = block2.GetOrCalculateHash(); | ||
|
Check failure on line 24 in src/Nethermind/Nethermind.Merge.Plugin.Test/BlockCacheServiceTest.cs
|
||
| Hash256 block3Hash = block3.GetOrCalculateHash(); | ||
|
Check failure on line 25 in src/Nethermind/Nethermind.Merge.Plugin.Test/BlockCacheServiceTest.cs
|
||
|
|
||
| blockCacheService.TryAddBlock(block1).Should().BeTrue(); | ||
| blockCacheService.TryAddBlock(block2).Should().BeTrue(); | ||
| blockCacheService.TryAddBlock(block3).Should().BeFalse(); | ||
|
|
||
| blockCacheService.BlockCache.Should().HaveCount(2); | ||
| blockCacheService.BlockCache.ContainsKey(block1Hash).Should().BeTrue(); | ||
| blockCacheService.BlockCache.ContainsKey(block2Hash).Should().BeTrue(); | ||
| blockCacheService.BlockCache.ContainsKey(block3Hash).Should().BeFalse(); | ||
| } | ||
|
|
||
| [Test] | ||
| public void preserves_head_and_finalized_blocks_when_pruning() | ||
| { | ||
| BlockCacheService blockCacheService = new(2); | ||
| Block finalizedBlock = Build.A.Block.WithNumber(1).TestObject; | ||
| Block block2 = Build.A.Block.WithNumber(2).TestObject; | ||
| Block headBlock = Build.A.Block.WithNumber(3).TestObject; | ||
| Hash256 finalizedHash = finalizedBlock.GetOrCalculateHash(); | ||
|
Check failure on line 44 in src/Nethermind/Nethermind.Merge.Plugin.Test/BlockCacheServiceTest.cs
|
||
| Hash256 block2Hash = block2.GetOrCalculateHash(); | ||
|
Check failure on line 45 in src/Nethermind/Nethermind.Merge.Plugin.Test/BlockCacheServiceTest.cs
|
||
| Hash256 headHash = headBlock.GetOrCalculateHash(); | ||
|
Check failure on line 46 in src/Nethermind/Nethermind.Merge.Plugin.Test/BlockCacheServiceTest.cs
|
||
| blockCacheService.FinalizedHash = finalizedHash; | ||
| blockCacheService.HeadBlockHash = headHash; | ||
|
|
||
| blockCacheService.TryAddBlock(finalizedBlock).Should().BeTrue(); | ||
| blockCacheService.TryAddBlock(block2).Should().BeTrue(); | ||
| blockCacheService.TryAddBlock(headBlock).Should().BeTrue(); | ||
|
|
||
| blockCacheService.BlockCache.Should().HaveCount(2); | ||
| blockCacheService.BlockCache.ContainsKey(finalizedHash).Should().BeTrue(); | ||
| blockCacheService.BlockCache.ContainsKey(block2Hash).Should().BeFalse(); | ||
| blockCacheService.BlockCache.ContainsKey(headHash).Should().BeTrue(); | ||
| } | ||
|
|
||
| [Test] | ||
| public void preserves_protected_blocks_when_all_candidates_are_protected() | ||
| { | ||
| BlockCacheService blockCacheService = new(1); | ||
| Block finalizedBlock = Build.A.Block.WithNumber(1).TestObject; | ||
| Block headBlock = Build.A.Block.WithNumber(2).TestObject; | ||
| Hash256 finalizedHash = finalizedBlock.GetOrCalculateHash(); | ||
|
Check failure on line 66 in src/Nethermind/Nethermind.Merge.Plugin.Test/BlockCacheServiceTest.cs
|
||
| Hash256 headHash = headBlock.GetOrCalculateHash(); | ||
|
Check failure on line 67 in src/Nethermind/Nethermind.Merge.Plugin.Test/BlockCacheServiceTest.cs
|
||
| blockCacheService.FinalizedHash = finalizedHash; | ||
| blockCacheService.HeadBlockHash = headHash; | ||
|
|
||
| blockCacheService.TryAddBlock(finalizedBlock).Should().BeTrue(); | ||
| blockCacheService.TryAddBlock(headBlock).Should().BeTrue(); | ||
|
|
||
| blockCacheService.BlockCache.Should().HaveCount(2); | ||
| blockCacheService.BlockCache.ContainsKey(finalizedHash).Should().BeTrue(); | ||
| blockCacheService.BlockCache.ContainsKey(headHash).Should().BeTrue(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,105 @@ | ||
| // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited | ||
| // SPDX-License-Identifier: LGPL-3.0-only | ||
|
|
||
| using System; | ||
| using System.Collections.Concurrent; | ||
| using System.Collections.Generic; | ||
| using Nethermind.Core; | ||
| using Nethermind.Core.Crypto; | ||
| using Nethermind.Crypto; | ||
|
|
||
| namespace Nethermind.Merge.Plugin.Handlers; | ||
|
|
||
| /// <summary> | ||
| /// Stores payload blocks while their ancestry is resolved during merge sync. | ||
| /// </summary> | ||
| public class BlockCacheService : IBlockCacheService | ||
| { | ||
| public ConcurrentDictionary<Hash256AsKey, Block> BlockCache { get; } = new(); | ||
| private readonly int _maxCachedBlocks; | ||
| private readonly ConcurrentDictionary<Hash256AsKey, Block> _blockCache = new(); | ||
|
|
||
| /// <summary> | ||
| /// Initializes a block cache with the default merge sync bound. | ||
| /// </summary> | ||
| public BlockCacheService() | ||
| : this((int)(Reorganization.MaxDepth * 2 + 16)) | ||
| { | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Initializes a block cache with the provided maximum number of cached blocks. | ||
| /// </summary> | ||
| /// <param name="maxCachedBlocks">Maximum number of cached blocks before pruning unprotected entries.</param> | ||
| public BlockCacheService(int maxCachedBlocks) | ||
| { | ||
| ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxCachedBlocks); | ||
| _maxCachedBlocks = maxCachedBlocks; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public IReadOnlyDictionary<Hash256AsKey, Block> BlockCache => _blockCache; | ||
|
|
||
| /// <inheritdoc /> | ||
| public Hash256? FinalizedHash { get; set; } | ||
|
|
||
| /// <inheritdoc /> | ||
| public Hash256? HeadBlockHash { get; set; } | ||
|
|
||
| /// <inheritdoc /> | ||
| public bool TryAddBlock(Block block) | ||
| { | ||
| Hash256 blockHash = block.GetOrCalculateHash(); | ||
| if (!_blockCache.TryAdd(blockHash, block)) | ||
| { | ||
| return false; | ||
| } | ||
|
|
||
| bool blockRemainsCached = true; | ||
| // The cache bound is small, so pruning scans it instead of maintaining a second ordered index. | ||
| while (_blockCache.Count > _maxCachedBlocks && | ||
| TryGetHighestNumberedUnprotectedBlock(out Hash256AsKey blockHashToRemove)) | ||
| { | ||
| bool removed = _blockCache.TryRemove(blockHashToRemove, out _); | ||
| if (removed && Equals(blockHashToRemove.Value, blockHash)) | ||
| { | ||
| blockRemainsCached = false; | ||
| } | ||
| } | ||
|
|
||
| return blockRemainsCached; | ||
| } | ||
|
|
||
| /// <inheritdoc /> | ||
| public bool TryRemoveBlock(Hash256 blockHash) => _blockCache.TryRemove(blockHash, out _); | ||
|
|
||
| /// <inheritdoc /> | ||
| public void Clear() => _blockCache.Clear(); | ||
|
|
||
| private bool TryGetHighestNumberedUnprotectedBlock(out Hash256AsKey blockHash) | ||
| { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Medium — Pruning strategy evicts the wrong end of the chain for beacon sync
Consider evicting the highest-numbered block instead (furthest from the sync anchor), or tracking insertion order via a parallel |
||
| Hash256AsKey highestNumberedHash = default; | ||
| long highestBlockNumber = long.MinValue; | ||
| bool foundBlock = false; | ||
|
|
||
| foreach (KeyValuePair<Hash256AsKey, Block> cachedBlock in _blockCache) | ||
| { | ||
| if (IsProtected(cachedBlock.Key)) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| if (!foundBlock || cachedBlock.Value.Number > highestBlockNumber) | ||
| { | ||
| highestNumberedHash = cachedBlock.Key; | ||
| highestBlockNumber = cachedBlock.Value.Number; | ||
| foundBlock = true; | ||
| } | ||
| } | ||
|
|
||
| blockHash = highestNumberedHash; | ||
| return foundBlock; | ||
| } | ||
|
|
||
| private bool IsProtected(Hash256AsKey blockHash) => | ||
| Equals(blockHash.Value, FinalizedHash) || Equals(blockHash.Value, HeadBlockHash); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,45 @@ | ||
| // SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited | ||
| // SPDX-License-Identifier: LGPL-3.0-only | ||
|
|
||
| using System.Collections.Concurrent; | ||
| using System.Collections.Generic; | ||
| using Nethermind.Core; | ||
| using Nethermind.Core.Crypto; | ||
|
|
||
| namespace Nethermind.Merge.Plugin.Handlers; | ||
|
|
||
| public interface IBlockCacheService | ||
| { | ||
| public ConcurrentDictionary<Hash256AsKey, Block> BlockCache { get; } | ||
| /// <summary> | ||
| /// Blocks cached while payload ancestry is resolved during merge sync. | ||
| /// </summary> | ||
| public IReadOnlyDictionary<Hash256AsKey, Block> BlockCache { get; } | ||
|
|
||
| /// <summary> | ||
| /// Finalized block hash protected from cache pruning. | ||
| /// </summary> | ||
| Hash256? FinalizedHash { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Head block hash protected from cache pruning. | ||
| /// </summary> | ||
| Hash256? HeadBlockHash { get; set; } | ||
|
|
||
| /// <summary> | ||
| /// Adds a block to the bounded cache. | ||
| /// </summary> | ||
| /// <param name="block">Block to add.</param> | ||
| /// <returns><see langword="true"/> if the block remains cached; otherwise <see langword="false"/>.</returns> | ||
| bool TryAddBlock(Block block); | ||
|
|
||
| /// <summary> | ||
| /// Removes a block from the cache. | ||
| /// </summary> | ||
| /// <param name="blockHash">Hash of the block to remove.</param> | ||
| /// <returns><see langword="true"/> if the block was removed; otherwise <see langword="false"/>.</returns> | ||
| bool TryRemoveBlock(Hash256 blockHash); | ||
|
|
||
| /// <summary> | ||
| /// Removes all cached blocks. | ||
| /// </summary> | ||
| void Clear(); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@LukaszRozmej isn't this going to make cache less effective when we support longer reorgs soon? Where is this number coming from?
Also how frequently can we have incoming chain from CL bigger than 144 blocks? I think this number needs more thought for all the chains we support - maybe for ethereum mainnet is fine.
Maybe change approach and evict by fork? Maybe make it configurable per chain?
@claude any ideas?