From 9e5465fa0b8b520430af8d28a72c17932ad6c327 Mon Sep 17 00:00:00 2001 From: Damian Orzechowski Date: Fri, 10 Apr 2026 17:51:46 +0200 Subject: [PATCH 1/6] Allow `HistoryPruner` to be callable via decorator --- .../HistoryPrunerTests.cs | 159 +++++++++++++++++- .../Nethermind.History/HistoryConfig.cs | 1 + .../Nethermind.History/HistoryPruner.cs | 31 +++- .../Nethermind.History/IHistoryConfig.cs | 5 + 4 files changed, 188 insertions(+), 8 deletions(-) diff --git a/src/Nethermind/Nethermind.History.Test/HistoryPrunerTests.cs b/src/Nethermind/Nethermind.History.Test/HistoryPrunerTests.cs index 0dd401623c46..5a1ec8d7cf87 100644 --- a/src/Nethermind/Nethermind.History.Test/HistoryPrunerTests.cs +++ b/src/Nethermind/Nethermind.History.Test/HistoryPrunerTests.cs @@ -20,6 +20,7 @@ using Nethermind.Db; using Nethermind.Logging; using Nethermind.Specs; +using Nethermind.Serialization.Rlp; using Nethermind.State.Repositories; using NSubstitute; using NUnit.Framework; @@ -260,6 +261,141 @@ public async Task Does_not_prune_when_disabled() CheckHeadPreserved(testBlockchain, blocks); } + [Test] + public async Task SetMinDeletableBlockNumber_preserves_blocks_below_minimum() + { + const int blocks = 100; + const int cutoff = 36; + const long minDeletable = 20; + + IHistoryConfig historyConfig = new HistoryConfig + { + Pruning = PruningModes.Rolling, + RetentionEpochs = 2, + PruningInterval = 0 + }; + + using BasicTestBlockchain testBlockchain = await BasicTestBlockchain.Create(BuildContainer(historyConfig)); + + List blockHashes = []; + blockHashes.Add(testBlockchain.BlockTree.Head!.Hash!); + for (int i = 0; i < blocks; i++) + { + await testBlockchain.AddBlock(); + blockHashes.Add(testBlockchain.BlockTree.Head!.Hash!); + } + + testBlockchain.BlockTree.SyncPivot = (blocks, Hash256.Zero); + + var historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); + historyPruner.SetMinDeletableBlockNumber(minDeletable); + + CheckOldestAndCutoff(minDeletable, cutoff, historyPruner); + + historyPruner.TryPruneHistory(CancellationToken.None); + + CheckGenesisPreserved(testBlockchain, blockHashes[0]); + + for (int i = 1; i < minDeletable; i++) + { + CheckBlockPreserved(testBlockchain, blockHashes, i); + } + + for (int i = (int)minDeletable; i <= blocks; i++) + { + if (i < cutoff) + { + CheckBlockPruned(testBlockchain, blockHashes, i); + } + else + { + CheckBlockPreserved(testBlockchain, blockHashes, i); + } + } + + CheckHeadPreserved(testBlockchain, blocks); + CheckOldestAndCutoff(cutoff, cutoff, historyPruner); + } + + [Test] + public async Task SetMinDeletableBlockNumber_clamps_stale_db_pointer() + { + const int blocks = 100; + const long stalePointer = 5; + const long minDeletable = 20; + + IHistoryConfig historyConfig = new HistoryConfig + { + Pruning = PruningModes.Rolling, + RetentionEpochs = 2, + PruningInterval = 0 + }; + + using BasicTestBlockchain testBlockchain = await BasicTestBlockchain.Create(BuildContainer(historyConfig)); + + for (int i = 0; i < blocks; i++) + { + await testBlockchain.AddBlock(); + } + + testBlockchain.BlockTree.SyncPivot = (blocks, Hash256.Zero); + + // Simulate a stale delete pointer in the DB (below the configured minimum) + IDb metadataDb = testBlockchain.Container.Resolve().MetadataDb; + metadataDb.Set(MetadataDbKeys.HistoryPruningDeletePointer, Rlp.Encode(stalePointer).Bytes); + + var historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); + historyPruner.SetMinDeletableBlockNumber(minDeletable); + + // Trigger pointer load — stale DB value must be clamped to minDeletable + Assert.That(historyPruner.OldestBlockHeader?.Number, Is.EqualTo(minDeletable), + "Delete pointer loaded from DB must be clamped to the configured minimum deletable block number"); + } + + [Test] + public async Task SchedulePruneHistory_passes_configured_timeout_to_scheduler() + { + const uint expectedTimeoutSeconds = 5; + + IHistoryConfig historyConfig = new HistoryConfig + { + Pruning = PruningModes.Rolling, + RetentionEpochs = 100000, + PruningTimeoutSeconds = expectedTimeoutSeconds, + PruningInterval = 0 + }; + + var scheduler = new CapturingScheduler(); + using BasicTestBlockchain testBlockchain = await BasicTestBlockchain.Create(BuildContainer(historyConfig, scheduler)); + + var historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); + historyPruner.SchedulePruneHistory(CancellationToken.None); + + await scheduler.Invoked.Task.WaitAsync(TimeSpan.FromSeconds(5)); + Assert.That(scheduler.CapturedTimeout, Is.EqualTo(TimeSpan.FromSeconds(expectedTimeoutSeconds))); + } + + [Test] + public async Task SchedulePruneHistory_passes_null_timeout_when_PruningTimeoutSeconds_is_zero() + { + IHistoryConfig historyConfig = new HistoryConfig + { + Pruning = PruningModes.Rolling, + RetentionEpochs = 100000, + PruningTimeoutSeconds = 0, + PruningInterval = 0 + }; + + var scheduler = new CapturingScheduler(); + using BasicTestBlockchain testBlockchain = await BasicTestBlockchain.Create(BuildContainer(historyConfig, scheduler)); + + var historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); + historyPruner.SchedulePruneHistory(CancellationToken.None); + + await scheduler.Invoked.Task.WaitAsync(TimeSpan.FromSeconds(5)); + Assert.That(scheduler.CapturedTimeout, Is.Null); + } + [Test] public void Can_accept_valid_config() { @@ -368,7 +504,20 @@ private static void CheckOldestAndCutoff(long oldest, long cutoff, IHistoryPrune } } - private static Action BuildContainer(IHistoryConfig historyConfig) + private sealed class CapturingScheduler : IBackgroundTaskScheduler + { + public TimeSpan? CapturedTimeout { get; private set; } + public TaskCompletionSource Invoked { get; } = new(); + + public bool TryScheduleTask(TReq request, Func fulfillFunc, TimeSpan? timeout = null, string source = null) + { + CapturedTimeout = timeout; + Invoked.TrySetResult(); + return true; + } + } + + private static Action BuildContainer(IHistoryConfig historyConfig, IBackgroundTaskScheduler scheduler = null) { // n.b. in prod MinHistoryRetentionEpochs should be 82125, however not feasible to test this ISpecProvider specProvider = new TestSpecProvider(new ReleaseSpec() { MinHistoryRetentionEpochs = 0 }); @@ -376,11 +525,17 @@ private static Action BuildContainer(IHistoryConfig historyCon // prevent pruner being triggered by empty queue IBlockProcessingQueue blockProcessingQueue = Substitute.For(); - return containerBuilder => containerBuilder + return containerBuilder => + { + containerBuilder .AddSingleton(specProvider) .AddSingleton(blockProcessingQueue) .AddSingleton(historyConfig) .AddSingleton(BlocksConfig) .AddSingleton(SyncConfig); + + if (scheduler is not null) + containerBuilder.AddSingleton(scheduler); + }; } } diff --git a/src/Nethermind/Nethermind.History/HistoryConfig.cs b/src/Nethermind/Nethermind.History/HistoryConfig.cs index e3d1db28f15d..f0f422086ce6 100644 --- a/src/Nethermind/Nethermind.History/HistoryConfig.cs +++ b/src/Nethermind/Nethermind.History/HistoryConfig.cs @@ -8,4 +8,5 @@ public class HistoryConfig : IHistoryConfig public PruningModes Pruning { get; set; } = PruningModes.Disabled; public uint RetentionEpochs { get; set; } = 82125; public uint PruningInterval { get; set; } = 8; + public uint PruningTimeoutSeconds { get; set; } = 2; } diff --git a/src/Nethermind/Nethermind.History/HistoryPruner.cs b/src/Nethermind/Nethermind.History/HistoryPruner.cs index c9ba68c3b17c..11e303af0566 100644 --- a/src/Nethermind/Nethermind.History/HistoryPruner.cs +++ b/src/Nethermind/Nethermind.History/HistoryPruner.cs @@ -52,6 +52,7 @@ public class HistoryPruner : IHistoryPruner private readonly int _deletionProgressLoggingInterval; private readonly long _ancientBarrier; private long _deletePointer = 1; + private long _minDeletableBlockNumber = 1; private BlockHeader? _deletePointerHeader; private long _lastSavedDeletePointer = 1; private long? _cutoffPointer; @@ -248,7 +249,7 @@ public BlockHeader? OldestBlockHeader private void OnBlockProcessorQueueEmpty(object? sender, EventArgs e) => SchedulePruneHistory(_processExitSource.Token); - private void SchedulePruneHistory(CancellationToken cancellationToken) + public void SchedulePruneHistory(CancellationToken cancellationToken) { if (Volatile.Read(ref _currentlyPruning) == 0) { @@ -258,6 +259,9 @@ private void SchedulePruneHistory(CancellationToken cancellationToken) { try { + TimeSpan? pruningTimeout = _historyConfig.PruningTimeoutSeconds > 0 + ? TimeSpan.FromSeconds(_historyConfig.PruningTimeoutSeconds) + : null; if (!_backgroundTaskScheduler.TryScheduleTask(1, (_, backgroundTaskToken) => { @@ -273,7 +277,7 @@ private void SchedulePruneHistory(CancellationToken cancellationToken) } return Task.CompletedTask; - }, source: "HistoryPruner")) + }, timeout: pruningTimeout, source: "HistoryPruner")) { Interlocked.Exchange(ref _currentlyPruning, 0); if (_logger.IsDebug) _logger.Debug("Failed to schedule historical block pruning (queue full). Will retry on next trigger."); @@ -289,6 +293,21 @@ private void SchedulePruneHistory(CancellationToken cancellationToken) } } + /// + /// Sets the minimum block number that may be deleted during pruning. + /// Must be called before the first pruning pass. Defaults to 1 (block 0 is never deleted). + /// Override when the chain genesis is not block 0. + /// + public void SetMinDeletableBlockNumber(long minBlockNumber) + { + _minDeletableBlockNumber = Math.Max(1, minBlockNumber); + // Only bump _deletePointer when the pointer has already been loaded from DB or discovered + // via SetDeletePointerToOldestBlock. If the pointer has not been loaded yet, + // TryLoadDeletePointer will enforce _minDeletableBlockNumber when it runs. + if (_hasLoadedDeletePointer && _deletePointer < _minDeletableBlockNumber) + _deletePointer = _minDeletableBlockNumber; + } + internal void TryPruneHistory(CancellationToken cancellationToken) { if (_blockTree.Head is null || @@ -350,7 +369,7 @@ void SkipLocalPruning() internal bool SetDeletePointerToOldestBlock() { - long? oldestBlockNumber = BlockTree.BinarySearchBlockNumber(1L, _blockTree.SyncPivot.BlockNumber, BlockExists, BlockTree.BinarySearchDirection.Down); + long? oldestBlockNumber = BlockTree.BinarySearchBlockNumber(_minDeletableBlockNumber, _blockTree.SyncPivot.BlockNumber, BlockExists, BlockTree.BinarySearchDirection.Down); if (oldestBlockNumber is not null) { @@ -434,9 +453,9 @@ private void PruneBlocksAndReceipts(ulong? cutoffTimestamp, CancellationToken ca } // should never happen - if (number == 0 || number >= _blockTree.SyncPivot.BlockNumber) + if (number < _minDeletableBlockNumber || number >= _blockTree.SyncPivot.BlockNumber) { - if (_logger.IsWarn) _logger.Warn($"Encountered unexpected block #{number} while pruning history, this block will not be deleted. Should be in range (0, {_blockTree.SyncPivot.BlockNumber})."); + if (_logger.IsWarn) _logger.Warn($"Encountered unexpected block #{number} while pruning history, this block will not be deleted. Should be in range ({_minDeletableBlockNumber}, {_blockTree.SyncPivot.BlockNumber})."); continue; } @@ -543,7 +562,7 @@ private bool TryLoadDeletePointer() } else { - UpdateDeletePointer(val.AsRlpValueContext().DecodeLong()); + UpdateDeletePointer(Math.Max(val.AsRlpValueContext().DecodeLong(), _minDeletableBlockNumber)); _lastSavedDeletePointer = _deletePointer; _hasLoadedDeletePointer = true; } diff --git a/src/Nethermind/Nethermind.History/IHistoryConfig.cs b/src/Nethermind/Nethermind.History/IHistoryConfig.cs index ee9a18271f72..0abe7062f192 100644 --- a/src/Nethermind/Nethermind.History/IHistoryConfig.cs +++ b/src/Nethermind/Nethermind.History/IHistoryConfig.cs @@ -25,6 +25,11 @@ public interface IHistoryConfig : IConfig DefaultValue = "8")] uint PruningInterval { get; set; } + [ConfigItem( + Description = "Maximum time in seconds allowed for a single history pruning pass. Set to 0 to disable the timeout.", + DefaultValue = "2")] + uint PruningTimeoutSeconds { get; set; } + // This member needs to be a method instead of a property // not to be picked up by the configuration handler bool Enabled() => Pruning != PruningModes.Disabled; From 87812c276ca6f6c5ca873ab0ba3d8e1e492f422a Mon Sep 17 00:00:00 2001 From: Damian Orzechowski Date: Fri, 17 Apr 2026 11:49:59 +0200 Subject: [PATCH 2/6] Fix style --- .../Nethermind.History.Test/HistoryPrunerTests.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Nethermind/Nethermind.History.Test/HistoryPrunerTests.cs b/src/Nethermind/Nethermind.History.Test/HistoryPrunerTests.cs index 6a374372c27d..6030bda5419a 100644 --- a/src/Nethermind/Nethermind.History.Test/HistoryPrunerTests.cs +++ b/src/Nethermind/Nethermind.History.Test/HistoryPrunerTests.cs @@ -287,7 +287,7 @@ public async Task SetMinDeletableBlockNumber_preserves_blocks_below_minimum() testBlockchain.BlockTree.SyncPivot = (blocks, Hash256.Zero); - var historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); + HistoryPruner historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); historyPruner.SetMinDeletableBlockNumber(minDeletable); CheckOldestAndCutoff(minDeletable, cutoff, historyPruner); @@ -344,10 +344,10 @@ public async Task SetMinDeletableBlockNumber_clamps_stale_db_pointer() IDb metadataDb = testBlockchain.Container.Resolve().MetadataDb; metadataDb.Set(MetadataDbKeys.HistoryPruningDeletePointer, Rlp.Encode(stalePointer).Bytes); - var historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); + HistoryPruner historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); historyPruner.SetMinDeletableBlockNumber(minDeletable); - // Trigger pointer load — stale DB value must be clamped to minDeletable + // Trigger pointer load — stale DB value must be clamped to minDeletable Assert.That(historyPruner.OldestBlockHeader?.Number, Is.EqualTo(minDeletable), "Delete pointer loaded from DB must be clamped to the configured minimum deletable block number"); } @@ -365,10 +365,10 @@ public async Task SchedulePruneHistory_passes_configured_timeout_to_scheduler() PruningInterval = 0 }; - var scheduler = new CapturingScheduler(); + CapturingScheduler scheduler = new(); using BasicTestBlockchain testBlockchain = await BasicTestBlockchain.Create(BuildContainer(historyConfig, scheduler)); - var historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); + HistoryPruner historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); historyPruner.SchedulePruneHistory(CancellationToken.None); await scheduler.Invoked.Task.WaitAsync(TimeSpan.FromSeconds(5)); @@ -386,10 +386,10 @@ public async Task SchedulePruneHistory_passes_null_timeout_when_PruningTimeoutSe PruningInterval = 0 }; - var scheduler = new CapturingScheduler(); + CapturingScheduler scheduler = new(); using BasicTestBlockchain testBlockchain = await BasicTestBlockchain.Create(BuildContainer(historyConfig, scheduler)); - var historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); + HistoryPruner historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); historyPruner.SchedulePruneHistory(CancellationToken.None); await scheduler.Invoked.Task.WaitAsync(TimeSpan.FromSeconds(5)); From a92205e77aa7e6f317a7bef56a8b9a4f9a2106c4 Mon Sep 17 00:00:00 2001 From: Damian Orzechowski Date: Fri, 17 Apr 2026 12:26:24 +0200 Subject: [PATCH 3/6] Fixes --- .../Nethermind.History/HistoryPruner.cs | 20 ++++++++++++------- .../Nethermind.History/IHistoryPruner.cs | 4 ++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Nethermind/Nethermind.History/HistoryPruner.cs b/src/Nethermind/Nethermind.History/HistoryPruner.cs index ef3a42542e88..4131159620a8 100644 --- a/src/Nethermind/Nethermind.History/HistoryPruner.cs +++ b/src/Nethermind/Nethermind.History/HistoryPruner.cs @@ -249,6 +249,10 @@ public BlockHeader? OldestBlockHeader private void OnBlockProcessorQueueEmpty(object? sender, EventArgs e) => SchedulePruneHistory(_processExitSource.Token); + /// + /// Schedules a pruning operation if one is not already running. Pruning will only be performed if the configured pruning interval has elapsed and there are blocks eligible for pruning. + /// + /// public void SchedulePruneHistory(CancellationToken cancellationToken) { if (Volatile.Read(ref _currentlyPruning) == 0) @@ -298,14 +302,16 @@ public void SchedulePruneHistory(CancellationToken cancellationToken) /// Must be called before the first pruning pass. Defaults to 1 (block 0 is never deleted). /// Override when the chain genesis is not block 0. /// + /// Minimum deletable block number. + /// Thrown if called after the delete pointer has already been loaded. public void SetMinDeletableBlockNumber(long minBlockNumber) { - _minDeletableBlockNumber = Math.Max(1, minBlockNumber); - // Only bump _deletePointer when the pointer has already been loaded from DB or discovered - // via SetDeletePointerToOldestBlock. If the pointer has not been loaded yet, - // TryLoadDeletePointer will enforce _minDeletableBlockNumber when it runs. - if (_hasLoadedDeletePointer && _deletePointer < _minDeletableBlockNumber) - _deletePointer = _minDeletableBlockNumber; + lock (_pruneLock) + { + if (_hasLoadedDeletePointer) + throw new InvalidOperationException($"{nameof(SetMinDeletableBlockNumber)} must be called before the first pruning pass."); + _minDeletableBlockNumber = Math.Max(1, minBlockNumber); + } } internal void TryPruneHistory(CancellationToken cancellationToken) @@ -455,7 +461,7 @@ private void PruneBlocksAndReceipts(ulong? cutoffTimestamp, CancellationToken ca // should never happen if (number < _minDeletableBlockNumber || number >= _blockTree.SyncPivot.BlockNumber) { - if (_logger.IsWarn) _logger.Warn($"Encountered unexpected block #{number} while pruning history, this block will not be deleted. Should be in range ({_minDeletableBlockNumber}, {_blockTree.SyncPivot.BlockNumber})."); + if (_logger.IsWarn) _logger.Warn($"Encountered unexpected block #{number} while pruning history, this block will not be deleted. Should be in range [{_minDeletableBlockNumber}, {_blockTree.SyncPivot.BlockNumber})."); continue; } diff --git a/src/Nethermind/Nethermind.History/IHistoryPruner.cs b/src/Nethermind/Nethermind.History/IHistoryPruner.cs index 62cf50cea075..13342e9df8d7 100644 --- a/src/Nethermind/Nethermind.History/IHistoryPruner.cs +++ b/src/Nethermind/Nethermind.History/IHistoryPruner.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LGPL-3.0-only using System; +using System.Threading; using Nethermind.Blockchain; using Nethermind.Core; @@ -13,4 +14,7 @@ public interface IHistoryPruner public BlockHeader? OldestBlockHeader { get; } event EventHandler NewOldestBlock; + + void SetMinDeletableBlockNumber(long minBlockNumber); + void SchedulePruneHistory(CancellationToken cancellationToken); } From 5fec9f08809f5b97b775d792f5c58ef57c6a461c Mon Sep 17 00:00:00 2001 From: Damian Orzechowski Date: Fri, 17 Apr 2026 12:39:29 +0200 Subject: [PATCH 4/6] Revert exception throw on SetMinDeletableBlockNumber --- src/Nethermind/Nethermind.History/HistoryPruner.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Nethermind/Nethermind.History/HistoryPruner.cs b/src/Nethermind/Nethermind.History/HistoryPruner.cs index 4131159620a8..abdfa5887075 100644 --- a/src/Nethermind/Nethermind.History/HistoryPruner.cs +++ b/src/Nethermind/Nethermind.History/HistoryPruner.cs @@ -299,18 +299,18 @@ public void SchedulePruneHistory(CancellationToken cancellationToken) /// /// Sets the minimum block number that may be deleted during pruning. - /// Must be called before the first pruning pass. Defaults to 1 (block 0 is never deleted). - /// Override when the chain genesis is not block 0. + /// Defaults to 1 (block 0 is never deleted). Override when the chain genesis is not block 0. /// - /// Minimum deletable block number. - /// Thrown if called after the delete pointer has already been loaded. public void SetMinDeletableBlockNumber(long minBlockNumber) { lock (_pruneLock) { - if (_hasLoadedDeletePointer) - throw new InvalidOperationException($"{nameof(SetMinDeletableBlockNumber)} must be called before the first pruning pass."); _minDeletableBlockNumber = Math.Max(1, minBlockNumber); + // Only bump _deletePointer when the pointer has already been loaded from DB or discovered + // via SetDeletePointerToOldestBlock. If the pointer has not been loaded yet, + // TryLoadDeletePointer will enforce _minDeletableBlockNumber when it runs. + if (_hasLoadedDeletePointer && _deletePointer < _minDeletableBlockNumber) + _deletePointer = _minDeletableBlockNumber; } } From 84a69ce7a0718b77b6733011941d748595db6ea4 Mon Sep 17 00:00:00 2001 From: Damian Orzechowski Date: Fri, 17 Apr 2026 13:03:55 +0200 Subject: [PATCH 5/6] Move min deletable to config --- .../HistoryPrunerTests.cs | 47 ++++++++++++++++--- .../Nethermind.History/HistoryConfig.cs | 1 + .../Nethermind.History/HistoryPruner.cs | 20 +------- .../Nethermind.History/IHistoryConfig.cs | 5 ++ .../Nethermind.History/IHistoryPruner.cs | 1 - 5 files changed, 49 insertions(+), 25 deletions(-) diff --git a/src/Nethermind/Nethermind.History.Test/HistoryPrunerTests.cs b/src/Nethermind/Nethermind.History.Test/HistoryPrunerTests.cs index 6030bda5419a..6dd487d5799e 100644 --- a/src/Nethermind/Nethermind.History.Test/HistoryPrunerTests.cs +++ b/src/Nethermind/Nethermind.History.Test/HistoryPrunerTests.cs @@ -262,7 +262,7 @@ public async Task Does_not_prune_when_disabled() } [Test] - public async Task SetMinDeletableBlockNumber_preserves_blocks_below_minimum() + public async Task MinDeletableBlockNumber_preserves_blocks_below_minimum() { const int blocks = 100; const int cutoff = 36; @@ -272,7 +272,8 @@ public async Task SetMinDeletableBlockNumber_preserves_blocks_below_minimum() { Pruning = PruningModes.Rolling, RetentionEpochs = 2, - PruningInterval = 0 + PruningInterval = 0, + MinDeletableBlockNumber = minDeletable }; using BasicTestBlockchain testBlockchain = await BasicTestBlockchain.Create(BuildContainer(historyConfig)); @@ -288,7 +289,6 @@ public async Task SetMinDeletableBlockNumber_preserves_blocks_below_minimum() testBlockchain.BlockTree.SyncPivot = (blocks, Hash256.Zero); HistoryPruner historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); - historyPruner.SetMinDeletableBlockNumber(minDeletable); CheckOldestAndCutoff(minDeletable, cutoff, historyPruner); @@ -318,7 +318,7 @@ public async Task SetMinDeletableBlockNumber_preserves_blocks_below_minimum() } [Test] - public async Task SetMinDeletableBlockNumber_clamps_stale_db_pointer() + public async Task MinDeletableBlockNumber_clamps_stale_db_pointer() { const int blocks = 100; const long stalePointer = 5; @@ -328,7 +328,8 @@ public async Task SetMinDeletableBlockNumber_clamps_stale_db_pointer() { Pruning = PruningModes.Rolling, RetentionEpochs = 2, - PruningInterval = 0 + PruningInterval = 0, + MinDeletableBlockNumber = minDeletable }; using BasicTestBlockchain testBlockchain = await BasicTestBlockchain.Create(BuildContainer(historyConfig)); @@ -345,13 +346,47 @@ public async Task SetMinDeletableBlockNumber_clamps_stale_db_pointer() metadataDb.Set(MetadataDbKeys.HistoryPruningDeletePointer, Rlp.Encode(stalePointer).Bytes); HistoryPruner historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); - historyPruner.SetMinDeletableBlockNumber(minDeletable); // Trigger pointer load — stale DB value must be clamped to minDeletable Assert.That(historyPruner.OldestBlockHeader?.Number, Is.EqualTo(minDeletable), "Delete pointer loaded from DB must be clamped to the configured minimum deletable block number"); } + [Test] + public async Task MinDeletableBlockNumber_does_not_reset_pointer_on_restart() + { + const int blocks = 100; + const long advancedPointer = 50; + const long minDeletable = 1; // default — lower than the advanced pointer + + IHistoryConfig historyConfig = new HistoryConfig + { + Pruning = PruningModes.Rolling, + RetentionEpochs = 2, + PruningInterval = 0, + MinDeletableBlockNumber = minDeletable + }; + + using BasicTestBlockchain testBlockchain = await BasicTestBlockchain.Create(BuildContainer(historyConfig)); + + for (int i = 0; i < blocks; i++) + { + await testBlockchain.AddBlock(); + } + + testBlockchain.BlockTree.SyncPivot = (blocks, Hash256.Zero); + + // Simulate a DB pointer that was advanced by a previous pruning run + IDb metadataDb = testBlockchain.Container.Resolve().MetadataDb; + metadataDb.Set(MetadataDbKeys.HistoryPruningDeletePointer, Rlp.Encode(advancedPointer).Bytes); + + HistoryPruner historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); + + // Trigger pointer load — DB value must be respected, not reset to minDeletable + Assert.That(historyPruner.OldestBlockHeader?.Number, Is.EqualTo(advancedPointer), + "Delete pointer must resume from the persisted DB value, not be reset by MinDeletableBlockNumber"); + } + [Test] public async Task SchedulePruneHistory_passes_configured_timeout_to_scheduler() { diff --git a/src/Nethermind/Nethermind.History/HistoryConfig.cs b/src/Nethermind/Nethermind.History/HistoryConfig.cs index f0f422086ce6..0832fe20a8f0 100644 --- a/src/Nethermind/Nethermind.History/HistoryConfig.cs +++ b/src/Nethermind/Nethermind.History/HistoryConfig.cs @@ -9,4 +9,5 @@ public class HistoryConfig : IHistoryConfig public uint RetentionEpochs { get; set; } = 82125; public uint PruningInterval { get; set; } = 8; public uint PruningTimeoutSeconds { get; set; } = 2; + public long MinDeletableBlockNumber { get; set; } = 1; } diff --git a/src/Nethermind/Nethermind.History/HistoryPruner.cs b/src/Nethermind/Nethermind.History/HistoryPruner.cs index abdfa5887075..3453fa920ca8 100644 --- a/src/Nethermind/Nethermind.History/HistoryPruner.cs +++ b/src/Nethermind/Nethermind.History/HistoryPruner.cs @@ -52,7 +52,7 @@ public class HistoryPruner : IHistoryPruner private readonly int _deletionProgressLoggingInterval; private readonly long _ancientBarrier; private long _deletePointer = 1; - private long _minDeletableBlockNumber = 1; + private readonly long _minDeletableBlockNumber; private BlockHeader? _deletePointerHeader; private long _lastSavedDeletePointer = 1; private long? _cutoffPointer; @@ -91,6 +91,7 @@ public HistoryPruner( _epochLength = (long)blocksConfig.SecondsPerSlot * SlotsPerEpoch; // must be changed if slot length changes _pruningInterval = historyConfig.PruningInterval * SlotsPerEpoch; _minHistoryRetentionEpochs = specProvider.GenesisSpec.MinHistoryRetentionEpochs; + _minDeletableBlockNumber = Math.Max(1, historyConfig.MinDeletableBlockNumber); CheckConfig(); @@ -297,23 +298,6 @@ public void SchedulePruneHistory(CancellationToken cancellationToken) } } - /// - /// Sets the minimum block number that may be deleted during pruning. - /// Defaults to 1 (block 0 is never deleted). Override when the chain genesis is not block 0. - /// - public void SetMinDeletableBlockNumber(long minBlockNumber) - { - lock (_pruneLock) - { - _minDeletableBlockNumber = Math.Max(1, minBlockNumber); - // Only bump _deletePointer when the pointer has already been loaded from DB or discovered - // via SetDeletePointerToOldestBlock. If the pointer has not been loaded yet, - // TryLoadDeletePointer will enforce _minDeletableBlockNumber when it runs. - if (_hasLoadedDeletePointer && _deletePointer < _minDeletableBlockNumber) - _deletePointer = _minDeletableBlockNumber; - } - } - internal void TryPruneHistory(CancellationToken cancellationToken) { if (_blockTree.Head is null || diff --git a/src/Nethermind/Nethermind.History/IHistoryConfig.cs b/src/Nethermind/Nethermind.History/IHistoryConfig.cs index 0abe7062f192..37c98a6f3cb5 100644 --- a/src/Nethermind/Nethermind.History/IHistoryConfig.cs +++ b/src/Nethermind/Nethermind.History/IHistoryConfig.cs @@ -30,6 +30,11 @@ public interface IHistoryConfig : IConfig DefaultValue = "2")] uint PruningTimeoutSeconds { get; set; } + [ConfigItem( + Description = "The minimum block number that may be deleted during pruning. Block 0 is never deleted. Override when the chain genesis is not block 0.", + DefaultValue = "1")] + long MinDeletableBlockNumber { get; set; } + // This member needs to be a method instead of a property // not to be picked up by the configuration handler bool Enabled() => Pruning != PruningModes.Disabled; diff --git a/src/Nethermind/Nethermind.History/IHistoryPruner.cs b/src/Nethermind/Nethermind.History/IHistoryPruner.cs index 13342e9df8d7..a91acec9a16c 100644 --- a/src/Nethermind/Nethermind.History/IHistoryPruner.cs +++ b/src/Nethermind/Nethermind.History/IHistoryPruner.cs @@ -15,6 +15,5 @@ public interface IHistoryPruner event EventHandler NewOldestBlock; - void SetMinDeletableBlockNumber(long minBlockNumber); void SchedulePruneHistory(CancellationToken cancellationToken); } From efd77de2f754de1cff318b1314e3a9996cb4357e Mon Sep 17 00:00:00 2001 From: "lukasz.rozmej" Date: Mon, 20 Apr 2026 13:34:20 +0200 Subject: [PATCH 6/6] cleanup tests --- .../HistoryPrunerTests.cs | 314 ++++-------------- 1 file changed, 61 insertions(+), 253 deletions(-) diff --git a/src/Nethermind/Nethermind.History.Test/HistoryPrunerTests.cs b/src/Nethermind/Nethermind.History.Test/HistoryPrunerTests.cs index 6dd487d5799e..cbc66133127c 100644 --- a/src/Nethermind/Nethermind.History.Test/HistoryPrunerTests.cs +++ b/src/Nethermind/Nethermind.History.Test/HistoryPrunerTests.cs @@ -44,152 +44,42 @@ public class HistoryPrunerTests SnapSync = true }; - [Test] - public async Task Can_prune_blocks_older_than_specified_epochs() + // Invariant: after prune, blocks [1, finalOldest) are pruned and [finalOldest, head] are preserved. + // Cutoff is driven by the pruning mode (Rolling: retention epochs vs head; UseAncientBarriers: SyncConfig barriers). + // Actual oldest can be below cutoff when the sync pivot caps how far pruning may advance. + [TestCase(PruningModes.Rolling, 2u, 100L, 36L, 36L, TestName = "Can_prune_blocks_older_than_specified_epochs")] + [TestCase(PruningModes.UseAncientBarriers, 100u, 100L, BeaconGenesisBlockNumber, BeaconGenesisBlockNumber, TestName = "Can_prune_to_ancient_barriers")] + [TestCase(PruningModes.UseAncientBarriers, 0u, 20L, BeaconGenesisBlockNumber, 20L, TestName = "Prunes_up_to_sync_pivot")] + public async Task Prune_scenario(PruningModes mode, uint retentionEpochs, long syncPivot, long expectedCutoff, long expectedFinalOldest) { const int blocks = 100; - const int cutoff = 36; IHistoryConfig historyConfig = new HistoryConfig { - Pruning = PruningModes.Rolling, - RetentionEpochs = 2, + Pruning = mode, + RetentionEpochs = retentionEpochs, PruningInterval = 0 }; - using BasicTestBlockchain testBlockchain = await BasicTestBlockchain.Create(BuildContainer(historyConfig)); - List blockHashes = []; - blockHashes.Add(testBlockchain.BlockTree.Head!.Hash!); - for (int i = 0; i < blocks; i++) - { - await testBlockchain.AddBlock(); - blockHashes.Add(testBlockchain.BlockTree.Head!.Hash!); - } - - Block head = testBlockchain.BlockTree.Head; - Assert.That(head, Is.Not.Null); - testBlockchain.BlockTree.SyncPivot = (blocks, Hash256.Zero); - + using BasicTestBlockchain testBlockchain = await CreateBlockchainWithBlocks(historyConfig, blocks, syncPivot: syncPivot, blockHashes: blockHashes); HistoryPruner historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); - CheckOldestAndCutoff(1, cutoff, historyPruner); + CheckOldestAndCutoff(1, expectedCutoff, historyPruner); historyPruner.TryPruneHistory(CancellationToken.None); CheckGenesisPreserved(testBlockchain, blockHashes[0]); for (int i = 1; i <= blocks; i++) { - if (i < cutoff) - { + if (i < expectedFinalOldest) CheckBlockPruned(testBlockchain, blockHashes, i); - } else - { CheckBlockPreserved(testBlockchain, blockHashes, i); - } - } - - CheckHeadPreserved(testBlockchain, blocks); - CheckOldestAndCutoff(cutoff, cutoff, historyPruner); - } - - [Test] - public async Task Can_prune_to_ancient_barriers() - { - const int blocks = 100; - - IHistoryConfig historyConfig = new HistoryConfig - { - Pruning = PruningModes.UseAncientBarriers, - RetentionEpochs = 100, // should have no effect - PruningInterval = 0 - }; - using BasicTestBlockchain testBlockchain = await BasicTestBlockchain.Create(BuildContainer(historyConfig)); - - List blockHashes = []; - blockHashes.Add(testBlockchain.BlockTree.Head!.Hash!); - for (int i = 0; i < blocks; i++) - { - await testBlockchain.AddBlock(); - blockHashes.Add(testBlockchain.BlockTree.Head!.Hash!); - } - - Block head = testBlockchain.BlockTree.Head; - Assert.That(head, Is.Not.Null); - testBlockchain.BlockTree.SyncPivot = (blocks, Hash256.Zero); - - HistoryPruner historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); - - CheckOldestAndCutoff(1, BeaconGenesisBlockNumber, historyPruner); - - historyPruner.TryPruneHistory(CancellationToken.None); - - CheckGenesisPreserved(testBlockchain, blockHashes[0]); - - for (int i = 1; i <= blocks; i++) - { - if (i < BeaconGenesisBlockNumber) - { - CheckBlockPruned(testBlockchain, blockHashes, i); - } - else - { - CheckBlockPreserved(testBlockchain, blockHashes, i); - } - } - - CheckHeadPreserved(testBlockchain, blocks); - CheckOldestAndCutoff(BeaconGenesisBlockNumber, BeaconGenesisBlockNumber, historyPruner); - } - - [Test] - public async Task Prunes_up_to_sync_pivot() - { - const int blocks = 100; - const long syncPivot = 20; - - IHistoryConfig historyConfig = new HistoryConfig - { - Pruning = PruningModes.UseAncientBarriers, - PruningInterval = 0 - }; - using BasicTestBlockchain testBlockchain = await BasicTestBlockchain.Create(BuildContainer(historyConfig)); - - List blockHashes = []; - blockHashes.Add(testBlockchain.BlockTree.Head!.Hash!); - for (int i = 0; i < blocks; i++) - { - await testBlockchain.AddBlock(); - blockHashes.Add(testBlockchain.BlockTree.Head!.Hash!); - } - - Block head = testBlockchain.BlockTree.Head; - Assert.That(head, Is.Not.Null); - testBlockchain.BlockTree.SyncPivot = (syncPivot, Hash256.Zero); - - HistoryPruner historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); - - CheckOldestAndCutoff(1, BeaconGenesisBlockNumber, historyPruner); - - historyPruner.TryPruneHistory(CancellationToken.None); - - CheckGenesisPreserved(testBlockchain, blockHashes[0]); - - for (int i = 1; i <= blocks; i++) - { - if (i < syncPivot) - { - CheckBlockPruned(testBlockchain, blockHashes, i); - } - else - { - CheckBlockPreserved(testBlockchain, blockHashes, i); - } } CheckHeadPreserved(testBlockchain, blocks); - CheckOldestAndCutoff(syncPivot, BeaconGenesisBlockNumber, historyPruner); + CheckOldestAndCutoff(expectedFinalOldest, expectedCutoff, historyPruner); } [Test] @@ -204,20 +94,9 @@ public async Task Can_find_oldest_block() RetentionEpochs = 2, PruningInterval = 0 }; - using BasicTestBlockchain testBlockchain = await BasicTestBlockchain.Create(BuildContainer(historyConfig)); List blockHashes = []; - blockHashes.Add(testBlockchain.BlockTree.Head!.Hash!); - for (int i = 0; i < blocks; i++) - { - await testBlockchain.AddBlock(); - blockHashes.Add(testBlockchain.BlockTree.Head!.Hash!); - } - - Block head = testBlockchain.BlockTree.Head; - Assert.That(head, Is.Not.Null); - testBlockchain.BlockTree.SyncPivot = (blocks, Hash256.Zero); - + using BasicTestBlockchain testBlockchain = await CreateBlockchainWithBlocks(historyConfig, blocks, syncPivot: blocks, blockHashes: blockHashes); HistoryPruner historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); CheckOldestAndCutoff(1, cutoff, historyPruner); @@ -238,16 +117,9 @@ public async Task Does_not_prune_when_disabled() Pruning = PruningModes.Disabled, PruningInterval = 0 }; - using BasicTestBlockchain testBlockchain = await BasicTestBlockchain.Create(BuildContainer(historyConfig)); List blockHashes = []; - blockHashes.Add(testBlockchain.BlockTree.Head!.Hash!); - for (int i = 0; i < blocks; i++) - { - await testBlockchain.AddBlock(); - blockHashes.Add(testBlockchain.BlockTree.Head!.Hash!); - } - + using BasicTestBlockchain testBlockchain = await CreateBlockchainWithBlocks(historyConfig, blocks, blockHashes: blockHashes); HistoryPruner historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); historyPruner.TryPruneHistory(CancellationToken.None); @@ -276,18 +148,8 @@ public async Task MinDeletableBlockNumber_preserves_blocks_below_minimum() MinDeletableBlockNumber = minDeletable }; - using BasicTestBlockchain testBlockchain = await BasicTestBlockchain.Create(BuildContainer(historyConfig)); - List blockHashes = []; - blockHashes.Add(testBlockchain.BlockTree.Head!.Hash!); - for (int i = 0; i < blocks; i++) - { - await testBlockchain.AddBlock(); - blockHashes.Add(testBlockchain.BlockTree.Head!.Hash!); - } - - testBlockchain.BlockTree.SyncPivot = (blocks, Hash256.Zero); - + using BasicTestBlockchain testBlockchain = await CreateBlockchainWithBlocks(historyConfig, blocks, syncPivot: blocks, blockHashes: blockHashes); HistoryPruner historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); CheckOldestAndCutoff(minDeletable, cutoff, historyPruner); @@ -317,47 +179,14 @@ public async Task MinDeletableBlockNumber_preserves_blocks_below_minimum() CheckOldestAndCutoff(cutoff, cutoff, historyPruner); } - [Test] - public async Task MinDeletableBlockNumber_clamps_stale_db_pointer() - { - const int blocks = 100; - const long stalePointer = 5; - const long minDeletable = 20; - - IHistoryConfig historyConfig = new HistoryConfig - { - Pruning = PruningModes.Rolling, - RetentionEpochs = 2, - PruningInterval = 0, - MinDeletableBlockNumber = minDeletable - }; - - using BasicTestBlockchain testBlockchain = await BasicTestBlockchain.Create(BuildContainer(historyConfig)); - - for (int i = 0; i < blocks; i++) - { - await testBlockchain.AddBlock(); - } - - testBlockchain.BlockTree.SyncPivot = (blocks, Hash256.Zero); - - // Simulate a stale delete pointer in the DB (below the configured minimum) - IDb metadataDb = testBlockchain.Container.Resolve().MetadataDb; - metadataDb.Set(MetadataDbKeys.HistoryPruningDeletePointer, Rlp.Encode(stalePointer).Bytes); - - HistoryPruner historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); - - // Trigger pointer load — stale DB value must be clamped to minDeletable - Assert.That(historyPruner.OldestBlockHeader?.Number, Is.EqualTo(minDeletable), - "Delete pointer loaded from DB must be clamped to the configured minimum deletable block number"); - } - - [Test] - public async Task MinDeletableBlockNumber_does_not_reset_pointer_on_restart() + // Pointer = max(MinDeletableBlockNumber, persisted DB value): + // stored < minDeletable → clamped up to minDeletable + // stored > minDeletable → persisted value wins (no reset on restart) + [TestCase(20L, 5L, 20L, TestName = "MinDeletableBlockNumber_clamps_stale_db_pointer")] + [TestCase(1L, 50L, 50L, TestName = "MinDeletableBlockNumber_does_not_reset_pointer_on_restart")] + public async Task MinDeletableBlockNumber_pointer_load(long minDeletable, long storedPointer, long expectedOldest) { const int blocks = 100; - const long advancedPointer = 50; - const long minDeletable = 1; // default — lower than the advanced pointer IHistoryConfig historyConfig = new HistoryConfig { @@ -367,57 +196,25 @@ public async Task MinDeletableBlockNumber_does_not_reset_pointer_on_restart() MinDeletableBlockNumber = minDeletable }; - using BasicTestBlockchain testBlockchain = await BasicTestBlockchain.Create(BuildContainer(historyConfig)); - - for (int i = 0; i < blocks; i++) - { - await testBlockchain.AddBlock(); - } - - testBlockchain.BlockTree.SyncPivot = (blocks, Hash256.Zero); + using BasicTestBlockchain testBlockchain = await CreateBlockchainWithBlocks(historyConfig, blocks, syncPivot: blocks); - // Simulate a DB pointer that was advanced by a previous pruning run IDb metadataDb = testBlockchain.Container.Resolve().MetadataDb; - metadataDb.Set(MetadataDbKeys.HistoryPruningDeletePointer, Rlp.Encode(advancedPointer).Bytes); - - HistoryPruner historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); - - // Trigger pointer load — DB value must be respected, not reset to minDeletable - Assert.That(historyPruner.OldestBlockHeader?.Number, Is.EqualTo(advancedPointer), - "Delete pointer must resume from the persisted DB value, not be reset by MinDeletableBlockNumber"); - } - - [Test] - public async Task SchedulePruneHistory_passes_configured_timeout_to_scheduler() - { - const uint expectedTimeoutSeconds = 5; - - IHistoryConfig historyConfig = new HistoryConfig - { - Pruning = PruningModes.Rolling, - RetentionEpochs = 100000, - PruningTimeoutSeconds = expectedTimeoutSeconds, - PruningInterval = 0 - }; - - CapturingScheduler scheduler = new(); - using BasicTestBlockchain testBlockchain = await BasicTestBlockchain.Create(BuildContainer(historyConfig, scheduler)); + metadataDb.Set(MetadataDbKeys.HistoryPruningDeletePointer, Rlp.Encode(storedPointer).Bytes); HistoryPruner historyPruner = (HistoryPruner)testBlockchain.Container.Resolve(); - historyPruner.SchedulePruneHistory(CancellationToken.None); - await scheduler.Invoked.Task.WaitAsync(TimeSpan.FromSeconds(5)); - Assert.That(scheduler.CapturedTimeout, Is.EqualTo(TimeSpan.FromSeconds(expectedTimeoutSeconds))); + Assert.That(historyPruner.OldestBlockHeader?.Number, Is.EqualTo(expectedOldest)); } - [Test] - public async Task SchedulePruneHistory_passes_null_timeout_when_PruningTimeoutSeconds_is_zero() + [TestCase(5u)] + [TestCase(0u)] + public async Task SchedulePruneHistory_passes_configured_timeout_to_scheduler(uint pruningTimeoutSeconds) { IHistoryConfig historyConfig = new HistoryConfig { Pruning = PruningModes.Rolling, RetentionEpochs = 100000, - PruningTimeoutSeconds = 0, + PruningTimeoutSeconds = pruningTimeoutSeconds, PruningInterval = 0 }; @@ -428,7 +225,8 @@ public async Task SchedulePruneHistory_passes_null_timeout_when_PruningTimeoutSe historyPruner.SchedulePruneHistory(CancellationToken.None); await scheduler.Invoked.Task.WaitAsync(TimeSpan.FromSeconds(5)); - Assert.That(scheduler.CapturedTimeout, Is.Null); + TimeSpan? expected = pruningTimeoutSeconds == 0 ? null : TimeSpan.FromSeconds(pruningTimeoutSeconds); + Assert.That(scheduler.CapturedTimeout, Is.EqualTo(expected)); } [Test] @@ -440,22 +238,7 @@ public void Can_accept_valid_config() RetentionEpochs = 100000, }; - IDbProvider dbProvider = Substitute.For(); - dbProvider.MetadataDb.Returns(new TestMemDb()); - - Assert.DoesNotThrow(() => new HistoryPruner( - Substitute.For(), - Substitute.For(), - Substitute.For(), - Substitute.For(), - dbProvider, - validHistoryConfig, - BlocksConfig, - SyncConfig, - new ProcessExitSource(new()), - Substitute.For(), - Substitute.For(), - LimboLogs.Instance)); + Assert.DoesNotThrow(() => CreateBareHistoryPruner(validHistoryConfig)); } [Test] @@ -468,22 +251,47 @@ public void Can_reject_invalid_config() }; ISpecProvider specProvider = new TestSpecProvider(new ReleaseSpec() { MinHistoryRetentionEpochs = 100 }); + + Assert.Throws(() => CreateBareHistoryPruner(invalidHistoryConfig, specProvider)); + } + + private static async Task CreateBlockchainWithBlocks( + IHistoryConfig historyConfig, + int blocks, + long? syncPivot = null, + List blockHashes = null, + IBackgroundTaskScheduler scheduler = null) + { + BasicTestBlockchain bc = await BasicTestBlockchain.Create(BuildContainer(historyConfig, scheduler)); + blockHashes?.Add(bc.BlockTree.Head!.Hash!); + for (int i = 0; i < blocks; i++) + { + await bc.AddBlock(); + blockHashes?.Add(bc.BlockTree.Head!.Hash!); + } + if (syncPivot is long pivot) + bc.BlockTree.SyncPivot = (pivot, Hash256.Zero); + return bc; + } + + private static HistoryPruner CreateBareHistoryPruner(IHistoryConfig historyConfig, ISpecProvider specProvider = null) + { IDbProvider dbProvider = Substitute.For(); dbProvider.MetadataDb.Returns(new TestMemDb()); - Assert.Throws(() => new HistoryPruner( + return new HistoryPruner( Substitute.For(), Substitute.For(), - specProvider, + specProvider ?? Substitute.For(), Substitute.For(), dbProvider, - invalidHistoryConfig, + historyConfig, BlocksConfig, SyncConfig, new ProcessExitSource(new()), Substitute.For(), Substitute.For(), - LimboLogs.Instance)); + LimboLogs.Instance); } private static void CheckGenesisPreserved(BasicTestBlockchain testBlockchain, Hash256 genesisHash)