diff --git a/AElf.All.sln b/AElf.All.sln
index 34ad27a9ac..27c9488a42 100644
--- a/AElf.All.sln
+++ b/AElf.All.sln
@@ -385,6 +385,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AElf.Kernel.FeatureDisable.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AElf.Kernel.FeatureDisable.Core", "src\AElf.Kernel.FeatureDisable.Core\AElf.Kernel.FeatureDisable.Core.csproj", "{659A7C7A-44C9-424E-B4F6-D1D3656F7AD4}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AElf.Kernel.BlockPruning", "src\AElf.Kernel.BlockPruning\AElf.Kernel.BlockPruning.csproj", "{651281CD-F268-49FC-9305-B19EE65552F7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AElf.Kernel.BlockPruning.Tests", "test\AElf.Kernel.BlockPruning.Tests\AElf.Kernel.BlockPruning.Tests.csproj", "{147343C3-BFDF-4C20-A990-975BB82CB73B}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -1107,6 +1111,14 @@ Global
{659A7C7A-44C9-424E-B4F6-D1D3656F7AD4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{659A7C7A-44C9-424E-B4F6-D1D3656F7AD4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{659A7C7A-44C9-424E-B4F6-D1D3656F7AD4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {651281CD-F268-49FC-9305-B19EE65552F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {651281CD-F268-49FC-9305-B19EE65552F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {651281CD-F268-49FC-9305-B19EE65552F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {651281CD-F268-49FC-9305-B19EE65552F7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {147343C3-BFDF-4C20-A990-975BB82CB73B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {147343C3-BFDF-4C20-A990-975BB82CB73B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {147343C3-BFDF-4C20-A990-975BB82CB73B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {147343C3-BFDF-4C20-A990-975BB82CB73B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -1301,5 +1313,7 @@ Global
{A4ACE6D2-4CF8-4B52-93C9-BB8BEC0C098E} = {90B310B4-C2DB-419E-B5EE-97FA096B62CC}
{8C0D86A4-D1A7-4B61-AC44-755F5AC75D67} = {4E54480A-D155-43ED-9736-1A5BE7957211}
{659A7C7A-44C9-424E-B4F6-D1D3656F7AD4} = {90B310B4-C2DB-419E-B5EE-97FA096B62CC}
+ {651281CD-F268-49FC-9305-B19EE65552F7} = {90B310B4-C2DB-419E-B5EE-97FA096B62CC}
+ {147343C3-BFDF-4C20-A990-975BB82CB73B} = {4E54480A-D155-43ED-9736-1A5BE7957211}
EndGlobalSection
EndGlobal
diff --git a/protobuf/block_pruning.proto b/protobuf/block_pruning.proto
new file mode 100644
index 0000000000..e7b5c4f4e5
--- /dev/null
+++ b/protobuf/block_pruning.proto
@@ -0,0 +1,9 @@
+syntax = "proto3";
+
+package aelf;
+
+option csharp_namespace = "AElf.Kernel.BlockPruning";
+
+message BlockPruningInfo {
+ int64 last_pruned_block_height = 1;
+}
diff --git a/src/AElf.Kernel.BlockPruning/AElf.Kernel.BlockPruning.csproj b/src/AElf.Kernel.BlockPruning/AElf.Kernel.BlockPruning.csproj
new file mode 100644
index 0000000000..ad72a0acce
--- /dev/null
+++ b/src/AElf.Kernel.BlockPruning/AElf.Kernel.BlockPruning.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ latest
+ AElf.Kernel.BlockPruning
+ true
+ Blockchain historical data pruning module.
+
+
+
+
+
+
+
+
+ Protobuf\Proto\block_pruning.proto
+
+
+
diff --git a/src/AElf.Kernel.BlockPruning/Application/BlockPruningService.cs b/src/AElf.Kernel.BlockPruning/Application/BlockPruningService.cs
new file mode 100644
index 0000000000..652e0a91de
--- /dev/null
+++ b/src/AElf.Kernel.BlockPruning/Application/BlockPruningService.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using AElf.Kernel.BlockPruning.Domain;
+using AElf.Kernel.Blockchain.Application;
+using AElf.Kernel.Blockchain.Domain;
+using AElf.Types;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Volo.Abp.DependencyInjection;
+
+namespace AElf.Kernel.BlockPruning.Application;
+
+public class BlockPruningService : IBlockPruningService, ITransientDependency
+{
+ private readonly IBlockManager _blockManager;
+ private readonly IBlockchainService _blockchainService;
+ private readonly IChainManager _chainManager;
+ private readonly IBlockPruningInfoManager _blockPruningInfoManager;
+ private readonly BlockPruningOptions _options;
+ private readonly ITransactionBlockIndexManager _transactionBlockIndexManager;
+ private readonly ITransactionManager _transactionManager;
+ private readonly ITransactionResultManager _transactionResultManager;
+
+ public ILogger Logger { get; set; }
+
+ public BlockPruningService(IBlockchainService blockchainService,
+ IChainManager chainManager,
+ IBlockManager blockManager,
+ ITransactionManager transactionManager,
+ ITransactionResultManager transactionResultManager,
+ ITransactionBlockIndexManager transactionBlockIndexManager,
+ IBlockPruningInfoManager blockPruningInfoManager,
+ IOptionsSnapshot options)
+ {
+ _blockchainService = blockchainService;
+ _chainManager = chainManager;
+ _blockManager = blockManager;
+ _transactionManager = transactionManager;
+ _transactionResultManager = transactionResultManager;
+ _transactionBlockIndexManager = transactionBlockIndexManager;
+ _blockPruningInfoManager = blockPruningInfoManager;
+ _options = options.Value;
+
+ Logger = NullLogger.Instance;
+ }
+
+ public async Task PruneBlockchainDataAsync()
+ {
+ if (!_options.Enabled)
+ return;
+
+ var chain = await _blockchainService.GetChainAsync();
+ var pruneTargetHeight = chain.LastIrreversibleBlockHeight - _options.RetainDistance;
+ var lastPrunedHeight = await _blockPruningInfoManager.GetLastPrunedHeightAsync();
+
+ if (pruneTargetHeight <= lastPrunedHeight)
+ return;
+
+ var gap = pruneTargetHeight - lastPrunedHeight;
+ if (gap < _options.PruneThreshold)
+ {
+ Logger.LogDebug(
+ "Pruning skipped: gap {Gap} below threshold {Threshold} (target={Target}, lastPruned={LastPruned})",
+ gap, _options.PruneThreshold, pruneTargetHeight, lastPrunedHeight);
+ return;
+ }
+
+ var startHeight = Math.Max(2, lastPrunedHeight + 1);
+ if (startHeight > pruneTargetHeight)
+ return;
+
+ Logger.LogInformation(
+ "Block pruning started: from height {StartHeight} to {TargetHeight} (LIB={LIBHeight}, retain={RetainDistance})",
+ startHeight, pruneTargetHeight, chain.LastIrreversibleBlockHeight, _options.RetainDistance);
+
+ var totalPruned = 0L;
+
+ for (var batchStart = startHeight; batchStart <= pruneTargetHeight; batchStart += _options.BatchSize)
+ {
+ var batchEnd = Math.Min(batchStart + _options.BatchSize - 1, pruneTargetHeight);
+
+ var allTxIds = new List();
+ var allTxResultBlockHashes = new List();
+ var allBlockHashes = new List();
+ var heights = new List();
+ var foundBlockBodyCount = 0;
+
+ for (var height = batchStart; height <= batchEnd; height++)
+ {
+ heights.Add(height);
+ }
+
+ var chainBlockIndices = await _chainManager.GetChainBlockIndicesAsync(heights);
+
+ foreach (var chainBlockIndex in chainBlockIndices)
+ {
+ if (chainBlockIndex == null)
+ continue;
+
+ allBlockHashes.Add(chainBlockIndex.BlockHash);
+ }
+
+ var blockBodies = await _blockManager.GetBlockBodiesAsync(allBlockHashes);
+
+ for (var i = 0; i < allBlockHashes.Count; i++)
+ {
+ var blockBody = blockBodies[i];
+ if (blockBody == null)
+ continue;
+
+ foundBlockBodyCount++;
+ var blockHash = allBlockHashes[i];
+ foreach (var txId in blockBody.TransactionIds)
+ {
+ allTxIds.Add(txId);
+ allTxResultBlockHashes.Add(blockHash);
+ }
+ }
+
+ Logger.LogDebug(
+ "Pruning batch [{BatchStart}..{BatchEnd}]: found {ChainBlockIndexCount} chain block indices, loaded {BlockBodyCount} block bodies",
+ batchStart, batchEnd, allBlockHashes.Count, foundBlockBodyCount);
+
+ await _transactionResultManager.RemoveTransactionResultsAsync(allTxIds, allTxResultBlockHashes);
+ await _transactionBlockIndexManager.RemoveTransactionIndicesAsync(allTxIds);
+ await _transactionManager.RemoveTransactionsAsync(allTxIds);
+ await _blockManager.RemoveBlocksAsync(allBlockHashes);
+ await _chainManager.RemoveChainBlockLinksAsync(allBlockHashes);
+
+ await _blockPruningInfoManager.SetLastPrunedHeightAsync(batchEnd);
+
+ totalPruned += batchEnd - batchStart + 1;
+
+ Logger.LogDebug(
+ "Pruned batch [{BatchStart}..{BatchEnd}]: {BlockCount} blocks, {TxCount} transactions",
+ batchStart, batchEnd, allBlockHashes.Count, allTxIds.Count);
+
+ if (_options.BatchDelayMilliseconds > 0)
+ await Task.Delay(_options.BatchDelayMilliseconds);
+ }
+
+ Logger.LogInformation(
+ "Block pruning completed: pruned {TotalPruned} heights, new last pruned height = {PrunedHeight}",
+ totalPruned, pruneTargetHeight);
+ }
+}
diff --git a/src/AElf.Kernel.BlockPruning/Application/IBlockPruningService.cs b/src/AElf.Kernel.BlockPruning/Application/IBlockPruningService.cs
new file mode 100644
index 0000000000..eac8460493
--- /dev/null
+++ b/src/AElf.Kernel.BlockPruning/Application/IBlockPruningService.cs
@@ -0,0 +1,8 @@
+using System.Threading.Tasks;
+
+namespace AElf.Kernel.BlockPruning.Application;
+
+public interface IBlockPruningService
+{
+ Task PruneBlockchainDataAsync();
+}
diff --git a/src/AElf.Kernel.BlockPruning/BlockPruningAElfModule.cs b/src/AElf.Kernel.BlockPruning/BlockPruningAElfModule.cs
new file mode 100644
index 0000000000..0ffaef9b3c
--- /dev/null
+++ b/src/AElf.Kernel.BlockPruning/BlockPruningAElfModule.cs
@@ -0,0 +1,35 @@
+using System;
+using AElf.Modularity;
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp;
+using Volo.Abp.Modularity;
+
+namespace AElf.Kernel.BlockPruning;
+
+[DependsOn(
+ typeof(CoreKernelAElfModule)
+)]
+public class BlockPruningAElfModule : AElfModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ var configuration = context.Services.GetConfiguration();
+
+ Configure(configuration.GetSection("BlockPruning"));
+ context.Services.PostConfigure(options =>
+ {
+ options.RetainDistance = Math.Max(options.RetainDistance, BlockPruningConstants.MinRetainDistance);
+ options.BatchSize = Math.Clamp(options.BatchSize, 1, BlockPruningConstants.MaxBatchSize);
+ options.PruneThreshold = Math.Max(options.PruneThreshold, 0);
+ options.BatchDelayMilliseconds = Math.Max(options.BatchDelayMilliseconds, 0);
+ });
+
+ context.Services.AddStoreKeyPrefixProvide("bp");
+ }
+
+ public override void OnPreApplicationInitialization(ApplicationInitializationContext context)
+ {
+ var taskQueueManager = context.ServiceProvider.GetRequiredService();
+ taskQueueManager.CreateQueue(BlockPruningConstants.BlockPruningQueueName);
+ }
+}
diff --git a/src/AElf.Kernel.BlockPruning/BlockPruningConstants.cs b/src/AElf.Kernel.BlockPruning/BlockPruningConstants.cs
new file mode 100644
index 0000000000..22bd4a8ec1
--- /dev/null
+++ b/src/AElf.Kernel.BlockPruning/BlockPruningConstants.cs
@@ -0,0 +1,8 @@
+namespace AElf.Kernel.BlockPruning;
+
+public static class BlockPruningConstants
+{
+ public const string BlockPruningQueueName = "BlockPruningQueue";
+ public const long MinRetainDistance = 2419200;
+ public const int MaxBatchSize = 10000;
+}
diff --git a/src/AElf.Kernel.BlockPruning/BlockPruningOptions.cs b/src/AElf.Kernel.BlockPruning/BlockPruningOptions.cs
new file mode 100644
index 0000000000..b5222072fe
--- /dev/null
+++ b/src/AElf.Kernel.BlockPruning/BlockPruningOptions.cs
@@ -0,0 +1,10 @@
+namespace AElf.Kernel.BlockPruning;
+
+public class BlockPruningOptions
+{
+ public bool Enabled { get; set; }
+ public long RetainDistance { get; set; } = 5184000;
+ public int BatchSize { get; set; } = 100;
+ public int PruneThreshold { get; set; } = 256;
+ public int BatchDelayMilliseconds { get; set; } = 50;
+}
diff --git a/src/AElf.Kernel.BlockPruning/Domain/BlockPruningInfoManager.cs b/src/AElf.Kernel.BlockPruning/Domain/BlockPruningInfoManager.cs
new file mode 100644
index 0000000000..c1ffe1152e
--- /dev/null
+++ b/src/AElf.Kernel.BlockPruning/Domain/BlockPruningInfoManager.cs
@@ -0,0 +1,33 @@
+using System.Threading.Tasks;
+using AElf.Kernel.Blockchain.Infrastructure;
+using AElf.Kernel.Infrastructure;
+using Volo.Abp.DependencyInjection;
+
+namespace AElf.Kernel.BlockPruning.Domain;
+
+public class BlockPruningInfoManager : IBlockPruningInfoManager, ISingletonDependency
+{
+ private readonly string _key;
+ private readonly IBlockchainStore _store;
+
+ public BlockPruningInfoManager(IBlockchainStore store,
+ IStaticChainInformationProvider chainInformationProvider)
+ {
+ _store = store;
+ _key = chainInformationProvider.ChainId.ToStorageKey();
+ }
+
+ public async Task GetLastPrunedHeightAsync()
+ {
+ var value = await _store.GetAsync(_key);
+ return value?.LastPrunedBlockHeight ?? 0;
+ }
+
+ public async Task SetLastPrunedHeightAsync(long height)
+ {
+ await _store.SetAsync(_key, new BlockPruningInfo
+ {
+ LastPrunedBlockHeight = height
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/AElf.Kernel.BlockPruning/Domain/IBlockPruningInfoManager.cs b/src/AElf.Kernel.BlockPruning/Domain/IBlockPruningInfoManager.cs
new file mode 100644
index 0000000000..ccf7ba3c3b
--- /dev/null
+++ b/src/AElf.Kernel.BlockPruning/Domain/IBlockPruningInfoManager.cs
@@ -0,0 +1,11 @@
+using System.Threading.Tasks;
+
+namespace AElf.Kernel.BlockPruning.Domain;
+
+public interface IBlockPruningInfoManager
+{
+ Task GetLastPrunedHeightAsync();
+ Task SetLastPrunedHeightAsync(long height);
+}
+
+
diff --git a/src/AElf.Kernel.BlockPruning/NewIrreversibleBlockFoundEventHandler.cs b/src/AElf.Kernel.BlockPruning/NewIrreversibleBlockFoundEventHandler.cs
new file mode 100644
index 0000000000..e8c9a2acf0
--- /dev/null
+++ b/src/AElf.Kernel.BlockPruning/NewIrreversibleBlockFoundEventHandler.cs
@@ -0,0 +1,52 @@
+using System.Threading.Tasks;
+using AElf.Kernel.BlockPruning.Application;
+using AElf.Kernel.Blockchain.Events;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+using Volo.Abp.DependencyInjection;
+using Volo.Abp.EventBus;
+
+namespace AElf.Kernel.BlockPruning;
+
+public class NewIrreversibleBlockFoundEventHandler : ILocalEventHandler,
+ ITransientDependency
+{
+ private readonly IBlockPruningService _blockPruningService;
+ private readonly BlockPruningOptions _options;
+ private readonly ITaskQueueManager _taskQueueManager;
+
+ public ILogger Logger { get; set; }
+
+ public NewIrreversibleBlockFoundEventHandler(ITaskQueueManager taskQueueManager,
+ IBlockPruningService blockPruningService,
+ IOptionsSnapshot options)
+ {
+ _taskQueueManager = taskQueueManager;
+ _blockPruningService = blockPruningService;
+ _options = options.Value;
+
+ Logger = NullLogger.Instance;
+ }
+
+ public Task HandleEventAsync(NewIrreversibleBlockFoundEvent eventData)
+ {
+ if (!_options.Enabled)
+ return Task.CompletedTask;
+
+ var queue = _taskQueueManager.GetQueue(BlockPruningConstants.BlockPruningQueueName);
+ if (queue == null || queue.Size > 0)
+ {
+ Logger.LogDebug("Block pruning skipped: queue is busy (size={QueueSize})",
+ queue?.Size ?? -1);
+ return Task.CompletedTask;
+ }
+
+ Logger.LogDebug(
+ "Enqueueing block pruning task (LIB height={LIBHeight})",
+ eventData.BlockHeight);
+
+ queue.Enqueue(async () => { await _blockPruningService.PruneBlockchainDataAsync(); });
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/AElf.Kernel.Core/Blockchain/Domain/IBlockManager.cs b/src/AElf.Kernel.Core/Blockchain/Domain/IBlockManager.cs
index 0f4796bfb1..5d527bbf0a 100644
--- a/src/AElf.Kernel.Core/Blockchain/Domain/IBlockManager.cs
+++ b/src/AElf.Kernel.Core/Blockchain/Domain/IBlockManager.cs
@@ -1,4 +1,6 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
using AElf.Kernel.Blockchain.Infrastructure;
using AElf.Kernel.Infrastructure;
@@ -10,7 +12,9 @@ public interface IBlockManager
Task AddBlockBodyAsync(Hash blockHash, BlockBody blockBody);
Task GetBlockAsync(Hash blockHash);
Task GetBlockHeaderAsync(Hash blockHash);
+ Task> GetBlockBodiesAsync(IList blockHashes);
Task RemoveBlockAsync(Hash blockHash);
+ Task RemoveBlocksAsync(IList blockHashes);
Task HasBlockAsync(Hash blockHash);
}
@@ -64,6 +68,11 @@ public async Task GetBlockHeaderAsync(Hash blockHash)
return await _blockHeaderStore.GetAsync(blockHash.ToStorageKey());
}
+ public async Task> GetBlockBodiesAsync(IList blockHashes)
+ {
+ return await _blockBodyStore.GetAllAsync(blockHashes.Select(h => h.ToStorageKey()).ToList());
+ }
+
public async Task RemoveBlockAsync(Hash blockHash)
{
var blockKey = blockHash.ToStorageKey();
@@ -71,6 +80,13 @@ public async Task RemoveBlockAsync(Hash blockHash)
await _blockBodyStore.RemoveAsync(blockKey);
}
+ public async Task RemoveBlocksAsync(IList blockHashes)
+ {
+ var blockKeys = blockHashes.Select(h => h.ToStorageKey()).ToList();
+ await _blockHeaderStore.RemoveAllAsync(blockKeys);
+ await _blockBodyStore.RemoveAllAsync(blockKeys);
+ }
+
public async Task HasBlockAsync(Hash blockHash)
{
return await _blockHeaderStore.IsExistsAsync(blockHash.ToStorageKey());
@@ -80,4 +96,4 @@ private async Task GetBlockBodyAsync(Hash bodyHash)
{
return await _blockBodyStore.GetAsync(bodyHash.ToStorageKey());
}
-}
\ No newline at end of file
+}
diff --git a/src/AElf.Kernel.Core/Blockchain/Domain/IChainManager.cs b/src/AElf.Kernel.Core/Blockchain/Domain/IChainManager.cs
index 7f6ca2cb4e..2e23187d46 100644
--- a/src/AElf.Kernel.Core/Blockchain/Domain/IChainManager.cs
+++ b/src/AElf.Kernel.Core/Blockchain/Domain/IChainManager.cs
@@ -23,8 +23,10 @@ public interface IChainManager
Task GetChainBlockLinkAsync(Hash blockHash);
List GetCachedChainBlockLinks();
Task RemoveChainBlockLinkAsync(Hash blockHash);
+ Task RemoveChainBlockLinksAsync(IList blockHashes);
void CleanCachedChainBlockLinks(long height);
Task GetChainBlockIndexAsync(long blockHeight);
+ Task> GetChainBlockIndicesAsync(IList blockHeights);
Task AttachBlockToChainAsync(Chain chain, ChainBlockLink chainBlockLink);
Task SetIrreversibleBlockAsync(Chain chain, Hash irreversibleBlockHash);
Task> GetNotExecutedBlocks(Hash blockHash);
@@ -142,6 +144,17 @@ await _chainBlockLinks.RemoveAsync(ChainId.ToStorageKey() + KernelConstants.Stor
_chainBlockLinkCacheProvider.RemoveChainBlockLink(blockHash);
}
+ public async Task RemoveChainBlockLinksAsync(IList blockHashes)
+ {
+ var prefix = ChainId.ToStorageKey() + KernelConstants.StorageKeySeparator;
+ await _chainBlockLinks.RemoveAllAsync(blockHashes.Select(h => prefix + h.ToStorageKey()).ToList());
+
+ foreach (var blockHash in blockHashes)
+ {
+ _chainBlockLinkCacheProvider.RemoveChainBlockLink(blockHash);
+ }
+ }
+
public void CleanCachedChainBlockLinks(long height)
{
var chainBlockLinks = _chainBlockLinkCacheProvider.GetChainBlockLinks()
@@ -157,6 +170,12 @@ public async Task GetChainBlockIndexAsync(long blockHeight)
blockHeight.ToStorageKey());
}
+ public async Task> GetChainBlockIndicesAsync(IList blockHeights)
+ {
+ var prefix = ChainId.ToStorageKey() + KernelConstants.StorageKeySeparator;
+ return await _chainBlockIndexes.GetAllAsync(blockHeights.Select(h => prefix + h.ToStorageKey()).ToList());
+ }
+
public async Task AttachBlockToChainAsync(Chain chain, ChainBlockLink chainBlockLink)
{
var status = BlockAttachOperationStatus.None;
@@ -563,4 +582,4 @@ private async Task> GetNotLinkedKeysAsync(Chain chain, long irrever
return toCleanNotLinkedKeys;
}
-}
\ No newline at end of file
+}
diff --git a/src/AElf.Kernel.Core/Blockchain/Domain/ITransactionResultManager.cs b/src/AElf.Kernel.Core/Blockchain/Domain/ITransactionResultManager.cs
index e2f2176886..3e8de2afa3 100644
--- a/src/AElf.Kernel.Core/Blockchain/Domain/ITransactionResultManager.cs
+++ b/src/AElf.Kernel.Core/Blockchain/Domain/ITransactionResultManager.cs
@@ -1,4 +1,5 @@
using System.Linq;
+using System;
using AElf.Kernel.Blockchain.Infrastructure;
using AElf.Kernel.Infrastructure;
@@ -9,6 +10,7 @@ public interface ITransactionResultManager
Task AddTransactionResultAsync(TransactionResult transactionResult, Hash disambiguationHash);
Task AddTransactionResultsAsync(IList transactionResults, Hash disambiguationHash);
+ Task RemoveTransactionResultsAsync(IList txIds, IList disambiguationHashes);
Task GetTransactionResultAsync(Hash txId, Hash disambiguationHash);
Task> GetTransactionResultsAsync(IList txIds, Hash disambiguationHash);
Task HasTransactionResultAsync(Hash transactionId, Hash disambiguationHash);
@@ -38,6 +40,16 @@ await _transactionResultStore.SetAllAsync(
t => HashHelper.XorAndCompute(t.TransactionId, disambiguationHash).ToStorageKey(), t => t));
}
+ public async Task RemoveTransactionResultsAsync(IList txIds, IList disambiguationHashes)
+ {
+ if (txIds.Count != disambiguationHashes.Count)
+ throw new ArgumentException("txIds and disambiguationHashes count mismatch.");
+
+ var keys = txIds.Select((txId, i) => HashHelper.XorAndCompute(txId, disambiguationHashes[i]).ToStorageKey())
+ .ToList();
+ await _transactionResultStore.RemoveAllAsync(keys);
+ }
+
public async Task GetTransactionResultAsync(Hash txId, Hash disambiguationHash)
{
return await _transactionResultStore.GetAsync(HashHelper.XorAndCompute(txId, disambiguationHash)
diff --git a/src/AElf.Kernel/AElf.Kernel.csproj b/src/AElf.Kernel/AElf.Kernel.csproj
index d45c62fa75..f427e0e866 100644
--- a/src/AElf.Kernel/AElf.Kernel.csproj
+++ b/src/AElf.Kernel/AElf.Kernel.csproj
@@ -9,6 +9,7 @@
+
diff --git a/src/AElf.Kernel/KernelAElfModule.cs b/src/AElf.Kernel/KernelAElfModule.cs
index 5e27d0dd72..8667ce7604 100644
--- a/src/AElf.Kernel/KernelAElfModule.cs
+++ b/src/AElf.Kernel/KernelAElfModule.cs
@@ -1,4 +1,5 @@
using AElf.Kernel.ChainController;
+using AElf.Kernel.BlockPruning;
using AElf.Kernel.Configuration;
using AElf.Kernel.FeatureDisable;
using AElf.Kernel.Miner;
@@ -23,7 +24,8 @@ namespace AElf.Kernel;
typeof(SmartContractExecutionAElfModule),
typeof(TransactionPoolAElfModule),
typeof(ConfigurationAElfModule),
- typeof(ProposalAElfModule))
+ typeof(ProposalAElfModule),
+ typeof(BlockPruningAElfModule))
]
public class KernelAElfModule : AElfModule
{
diff --git a/src/AElf.OS.Core/Network/Extensions/BlockchainServiceExtensions.cs b/src/AElf.OS.Core/Network/Extensions/BlockchainServiceExtensions.cs
index 311686ed6e..02e0450fca 100644
--- a/src/AElf.OS.Core/Network/Extensions/BlockchainServiceExtensions.cs
+++ b/src/AElf.OS.Core/Network/Extensions/BlockchainServiceExtensions.cs
@@ -26,8 +26,9 @@ public static async Task> GetBlocksWithTransactionsA
Hash firstHash, int count)
{
var blocks = await blockchainService.GetBlocksInBestChainBranchAsync(firstHash, count);
+ var availableBlocks = blocks.TakeWhile(block => block != null).ToList();
- var list = blocks
+ var list = availableBlocks
.Select(async block =>
{
var transactions = await blockchainService.GetTransactionsAsync(block.TransactionIds);
diff --git a/test/AElf.Kernel.BlockPruning.Tests/AElf.Kernel.BlockPruning.Tests.csproj b/test/AElf.Kernel.BlockPruning.Tests/AElf.Kernel.BlockPruning.Tests.csproj
new file mode 100644
index 0000000000..be0072bdbd
--- /dev/null
+++ b/test/AElf.Kernel.BlockPruning.Tests/AElf.Kernel.BlockPruning.Tests.csproj
@@ -0,0 +1,28 @@
+
+
+ net8.0
+ AElf.Kernel.BlockPruning
+ false
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
+
+
+
+
diff --git a/test/AElf.Kernel.BlockPruning.Tests/Application/BlockPruningServiceTests.cs b/test/AElf.Kernel.BlockPruning.Tests/Application/BlockPruningServiceTests.cs
new file mode 100644
index 0000000000..17d65fec3b
--- /dev/null
+++ b/test/AElf.Kernel.BlockPruning.Tests/Application/BlockPruningServiceTests.cs
@@ -0,0 +1,311 @@
+using System.Collections.Generic;
+using AElf.Kernel.BlockPruning.Application;
+using AElf.Kernel.BlockPruning.Domain;
+using AElf.Kernel.Blockchain.Application;
+using AElf.Kernel.Blockchain.Domain;
+
+namespace AElf.Kernel.BlockPruning;
+
+///
+/// Chain layout from MockChainAsync (LIB=5, BestChainHeight=11):
+/// Height: 1(genesis) -> 2 -> 3 -> 4 -> 5(LIB) -> 6 -> 7 -> 8 -> 9 -> 10 -> 11
+///
+/// BlockPruningServiceTestModule: Enabled=true, RetainDistance=2, BatchSize=100
+/// => pruneTarget = LIB - RetainDistance = 5 - 2 = 3
+/// => prunable range: heights 2~3 (genesis at 1 is hard-protected)
+///
+public sealed class BlockPruningServiceTests : BlockPruningServiceTestBase
+{
+ private readonly IBlockchainService _blockchainService;
+ private readonly IBlockManager _blockManager;
+ private readonly IBlockPruningInfoManager _blockPruningInfoManager;
+ private readonly IBlockPruningService _blockPruningService;
+ private readonly IChainManager _chainManager;
+
+ public BlockPruningServiceTests()
+ {
+ _blockPruningService = GetRequiredService();
+ _blockPruningInfoManager = GetRequiredService();
+ _blockchainService = GetRequiredService();
+ _blockManager = GetRequiredService();
+ _chainManager = GetRequiredService();
+ }
+
+ [Fact]
+ public async Task PruneBlockchainData_CompletePruningFlow_Test()
+ {
+ var chain = await _blockchainService.GetChainAsync();
+ chain.LastIrreversibleBlockHeight.ShouldBe(5);
+
+ await _blockPruningService.PruneBlockchainDataAsync();
+
+ var lastPruned = await _blockPruningInfoManager.GetLastPrunedHeightAsync();
+ lastPruned.ShouldBe(3);
+
+ for (var h = 2L; h <= 3; h++)
+ {
+ var idx = await _chainManager.GetChainBlockIndexAsync(h);
+ idx.ShouldNotBeNull();
+ var blockHash = idx.BlockHash;
+
+ (await _blockManager.GetBlockAsync(blockHash)).ShouldBeNull();
+ (await _blockManager.GetBlockHeaderAsync(blockHash)).ShouldBeNull();
+ (await _chainManager.GetChainBlockLinkAsync(blockHash)).ShouldBeNull();
+ }
+ }
+
+ [Fact]
+ public async Task PruneBlockchainData_ChainBlockIndex_Preserved_Test()
+ {
+ await _blockPruningService.PruneBlockchainDataAsync();
+
+ for (var h = 2L; h <= 3; h++)
+ {
+ var idx = await _chainManager.GetChainBlockIndexAsync(h);
+ idx.ShouldNotBeNull();
+ idx.BlockHash.ShouldNotBeNull();
+ }
+ }
+
+ [Fact]
+ public async Task PruneBlockchainData_RetainedRange_Unaffected_Test()
+ {
+ await _blockPruningService.PruneBlockchainDataAsync();
+
+ for (var h = 4L; h <= 5; h++)
+ {
+ var idx = await _chainManager.GetChainBlockIndexAsync(h);
+ idx.ShouldNotBeNull();
+ (await _blockManager.GetBlockAsync(idx.BlockHash)).ShouldNotBeNull();
+ (await _chainManager.GetChainBlockLinkAsync(idx.BlockHash)).ShouldNotBeNull();
+ }
+ }
+
+ [Fact]
+ public async Task PruneBlockchainData_GenesisBlock_Protected_Test()
+ {
+ await _blockPruningService.PruneBlockchainDataAsync();
+
+ var chain = await _blockchainService.GetChainAsync();
+ var genesisBlock = await _blockManager.GetBlockAsync(chain.GenesisBlockHash);
+ genesisBlock.ShouldNotBeNull();
+ }
+
+ [Fact]
+ public async Task PruneBlockchainData_Idempotent_Test()
+ {
+ await _blockPruningService.PruneBlockchainDataAsync();
+ var firstPrunedHeight = await _blockPruningInfoManager.GetLastPrunedHeightAsync();
+
+ await _blockPruningService.PruneBlockchainDataAsync();
+ var secondPrunedHeight = await _blockPruningInfoManager.GetLastPrunedHeightAsync();
+
+ secondPrunedHeight.ShouldBe(firstPrunedHeight);
+ }
+}
+
+///
+/// BlockPruningBatchTestModule: Enabled=true, RetainDistance=0, BatchSize=2
+/// => pruneTarget = LIB - 0 = 5
+/// => prunable: heights 2~5, BatchSize=2 => 2 batches (2~3, 4~5)
+///
+public sealed class BlockPruningBatchTests : BlockPruningBatchTestBase
+{
+ private readonly IBlockchainService _blockchainService;
+ private readonly IBlockManager _blockManager;
+ private readonly IBlockPruningInfoManager _blockPruningInfoManager;
+ private readonly IBlockPruningService _blockPruningService;
+ private readonly IChainManager _chainManager;
+
+ public BlockPruningBatchTests()
+ {
+ _blockPruningService = GetRequiredService();
+ _blockPruningInfoManager = GetRequiredService();
+ _blockchainService = GetRequiredService();
+ _blockManager = GetRequiredService();
+ _chainManager = GetRequiredService();
+ }
+
+ [Fact]
+ public async Task PruneBlockchainData_MultipleBatches_Test()
+ {
+ await _blockPruningService.PruneBlockchainDataAsync();
+
+ var lastPruned = await _blockPruningInfoManager.GetLastPrunedHeightAsync();
+ lastPruned.ShouldBe(5);
+
+ for (var h = 2L; h <= 5; h++)
+ {
+ var idx = await _chainManager.GetChainBlockIndexAsync(h);
+ idx.ShouldNotBeNull();
+ (await _blockManager.GetBlockAsync(idx.BlockHash)).ShouldBeNull();
+ }
+ }
+
+ [Fact]
+ public async Task PruneBlockchainData_GenesisProtected_WithBatch_Test()
+ {
+ await _blockPruningService.PruneBlockchainDataAsync();
+
+ var genesisIdx = await _chainManager.GetChainBlockIndexAsync(1);
+ genesisIdx.ShouldNotBeNull();
+ (await _blockManager.GetBlockAsync(genesisIdx.BlockHash)).ShouldNotBeNull();
+ }
+
+ [Fact]
+ public async Task PruneBlockchainData_HigherBlocks_Unaffected_Test()
+ {
+ await _blockPruningService.PruneBlockchainDataAsync();
+
+ for (var h = 6L; h <= 11; h++)
+ {
+ var block = await _blockchainService.GetBlockByHeightInBestChainBranchAsync(h);
+ block.ShouldNotBeNull();
+ }
+ }
+}
+
+///
+/// BlockPruningThresholdTestModule: Enabled=true, RetainDistance=2, PruneThreshold=100
+/// => pruneTarget = 5 - 2 = 3, gap = 3 < 100 => pruning skipped
+///
+public sealed class BlockPruningThresholdTests : BlockPruningThresholdTestBase
+{
+ private readonly IBlockPruningInfoManager _blockPruningInfoManager;
+ private readonly IBlockPruningService _blockPruningService;
+ private readonly IBlockManager _blockManager;
+ private readonly IChainManager _chainManager;
+
+ public BlockPruningThresholdTests()
+ {
+ _blockPruningService = GetRequiredService();
+ _blockPruningInfoManager = GetRequiredService();
+ _blockManager = GetRequiredService();
+ _chainManager = GetRequiredService();
+ }
+
+ [Fact]
+ public async Task PruneBlockchainData_BelowThreshold_ShouldSkip_Test()
+ {
+ await _blockPruningService.PruneBlockchainDataAsync();
+
+ var lastPruned = await _blockPruningInfoManager.GetLastPrunedHeightAsync();
+ lastPruned.ShouldBe(0);
+
+ for (var h = 2L; h <= 3; h++)
+ {
+ var idx = await _chainManager.GetChainBlockIndexAsync(h);
+ idx.ShouldNotBeNull();
+ (await _blockManager.GetBlockAsync(idx.BlockHash)).ShouldNotBeNull();
+ }
+ }
+}
+
+///
+/// Fault injection: simulate crash-recovery scenarios where block data is partially deleted.
+/// Uses BlockPruningServiceTestModule: Enabled=true, RetainDistance=2, pruneTarget=3
+///
+public sealed class BlockPruningFaultInjectionTests : BlockPruningServiceTestBase
+{
+ private readonly IBlockManager _blockManager;
+ private readonly IBlockPruningInfoManager _blockPruningInfoManager;
+ private readonly IBlockPruningService _blockPruningService;
+ private readonly IChainManager _chainManager;
+
+ public BlockPruningFaultInjectionTests()
+ {
+ _blockPruningService = GetRequiredService();
+ _blockPruningInfoManager = GetRequiredService();
+ _blockManager = GetRequiredService();
+ _chainManager = GetRequiredService();
+ }
+
+ [Fact]
+ public async Task PruneBlockchainData_BlockAlreadyDeleted_ShouldRecover_Test()
+ {
+ var idx = await _chainManager.GetChainBlockIndexAsync(2);
+ idx.ShouldNotBeNull();
+ await _blockManager.RemoveBlockAsync(idx.BlockHash);
+
+ await _blockPruningService.PruneBlockchainDataAsync();
+
+ var lastPruned = await _blockPruningInfoManager.GetLastPrunedHeightAsync();
+ lastPruned.ShouldBe(3);
+ }
+
+ [Fact]
+ public async Task PruneBlockchainData_AllBlocksAlreadyDeleted_ShouldComplete_Test()
+ {
+ for (var h = 2L; h <= 3; h++)
+ {
+ var idx = await _chainManager.GetChainBlockIndexAsync(h);
+ if (idx != null)
+ await _blockManager.RemoveBlockAsync(idx.BlockHash);
+ }
+
+ await _blockPruningService.PruneBlockchainDataAsync();
+
+ var lastPruned = await _blockPruningInfoManager.GetLastPrunedHeightAsync();
+ lastPruned.ShouldBe(3);
+ }
+
+ [Fact]
+ public async Task PruneBlockchainData_ChainBlockLinkAlreadyDeleted_ShouldComplete_Test()
+ {
+ var idx = await _chainManager.GetChainBlockIndexAsync(2);
+ idx.ShouldNotBeNull();
+ await _chainManager.RemoveChainBlockLinkAsync(idx.BlockHash);
+
+ await _blockPruningService.PruneBlockchainDataAsync();
+
+ var lastPruned = await _blockPruningInfoManager.GetLastPrunedHeightAsync();
+ lastPruned.ShouldBe(3);
+ }
+
+ [Fact]
+ public async Task PruneBlockchainData_ResumeAfterPartialPrune_ShouldConverge_Test()
+ {
+ await _blockPruningService.PruneBlockchainDataAsync();
+ var firstPruned = await _blockPruningInfoManager.GetLastPrunedHeightAsync();
+ firstPruned.ShouldBe(3);
+
+ await _blockPruningInfoManager.SetLastPrunedHeightAsync(1);
+
+ await _blockPruningService.PruneBlockchainDataAsync();
+
+ var secondPruned = await _blockPruningInfoManager.GetLastPrunedHeightAsync();
+ secondPruned.ShouldBe(3);
+ }
+}
+
+public sealed class BlockPruningDisabledTests : BlockPruningDisabledTestBase
+{
+ private readonly IBlockManager _blockManager;
+ private readonly IBlockPruningInfoManager _blockPruningInfoManager;
+ private readonly IBlockPruningService _blockPruningService;
+ private readonly IChainManager _chainManager;
+
+ public BlockPruningDisabledTests()
+ {
+ _blockPruningService = GetRequiredService();
+ _blockPruningInfoManager = GetRequiredService();
+ _blockManager = GetRequiredService();
+ _chainManager = GetRequiredService();
+ }
+
+ [Fact]
+ public async Task PruneBlockchainData_Disabled_NoOp_Test()
+ {
+ await _blockPruningService.PruneBlockchainDataAsync();
+
+ var lastPruned = await _blockPruningInfoManager.GetLastPrunedHeightAsync();
+ lastPruned.ShouldBe(0);
+
+ for (var h = 2L; h <= 5; h++)
+ {
+ var idx = await _chainManager.GetChainBlockIndexAsync(h);
+ idx.ShouldNotBeNull();
+ (await _blockManager.GetBlockAsync(idx.BlockHash)).ShouldNotBeNull();
+ }
+ }
+}
diff --git a/test/AElf.Kernel.BlockPruning.Tests/BlockPruningOptionsTests.cs b/test/AElf.Kernel.BlockPruning.Tests/BlockPruningOptionsTests.cs
new file mode 100644
index 0000000000..69b6274e9d
--- /dev/null
+++ b/test/AElf.Kernel.BlockPruning.Tests/BlockPruningOptionsTests.cs
@@ -0,0 +1,88 @@
+using Microsoft.Extensions.Options;
+using Volo.Abp.Modularity;
+
+namespace AElf.Kernel.BlockPruning;
+
+public sealed class BlockPruningOptionsTests : BlockPruningTestBase
+{
+ private readonly BlockPruningOptions _options;
+
+ public BlockPruningOptionsTests()
+ {
+ _options = GetRequiredService>().Value;
+ }
+
+ [Fact]
+ public void RetainDistance_Default_ShouldBeAtLeastMinRetainDistance()
+ {
+ _options.RetainDistance.ShouldBeGreaterThanOrEqualTo(BlockPruningConstants.MinRetainDistance);
+ }
+
+ [Fact]
+ public void BatchSize_ShouldBeAtLeast1()
+ {
+ _options.BatchSize.ShouldBeGreaterThanOrEqualTo(1);
+ }
+
+ [Fact]
+ public void PruneThreshold_ShouldBeAtLeast0()
+ {
+ _options.PruneThreshold.ShouldBeGreaterThanOrEqualTo(0);
+ }
+
+ [Fact]
+ public void BatchDelayMilliseconds_ShouldBeAtLeast0()
+ {
+ _options.BatchDelayMilliseconds.ShouldBeGreaterThanOrEqualTo(0);
+ }
+}
+
+public sealed class BlockPruningOptionsCorrectionTests : BlockPruningConfigCorrectionTestBase
+{
+ private readonly BlockPruningOptions _options;
+
+ public BlockPruningOptionsCorrectionTests()
+ {
+ _options = GetRequiredService>().Value;
+ }
+
+ [Fact]
+ public void RetainDistance_BelowMin_ShouldBeCorrectedToMin()
+ {
+ _options.RetainDistance.ShouldBe(BlockPruningConstants.MinRetainDistance);
+ }
+
+ [Fact]
+ public void BatchSize_Zero_ShouldBeCorrectedTo1()
+ {
+ _options.BatchSize.ShouldBe(1);
+ }
+
+ [Fact]
+ public void PruneThreshold_Negative_ShouldBeCorrectedTo0()
+ {
+ _options.PruneThreshold.ShouldBe(0);
+ }
+
+ [Fact]
+ public void BatchDelayMilliseconds_Negative_ShouldBeCorrectedTo0()
+ {
+ _options.BatchDelayMilliseconds.ShouldBe(0);
+ }
+}
+
+public sealed class BlockPruningBatchSizeUpperLimitTests : BlockPruningBatchSizeUpperLimitTestBase
+{
+ private readonly BlockPruningOptions _options;
+
+ public BlockPruningBatchSizeUpperLimitTests()
+ {
+ _options = GetRequiredService>().Value;
+ }
+
+ [Fact]
+ public void BatchSize_AboveMax_ShouldBeClampedToMax()
+ {
+ _options.BatchSize.ShouldBe(BlockPruningConstants.MaxBatchSize);
+ }
+}
diff --git a/test/AElf.Kernel.BlockPruning.Tests/BlockPruningTestBase.cs b/test/AElf.Kernel.BlockPruning.Tests/BlockPruningTestBase.cs
new file mode 100644
index 0000000000..d290dcdc8c
--- /dev/null
+++ b/test/AElf.Kernel.BlockPruning.Tests/BlockPruningTestBase.cs
@@ -0,0 +1,31 @@
+using AElf.TestBase;
+
+namespace AElf.Kernel.BlockPruning;
+
+public class BlockPruningTestBase : AElfIntegratedTest
+{
+}
+
+public class BlockPruningServiceTestBase : AElfIntegratedTest
+{
+}
+
+public class BlockPruningDisabledTestBase : AElfIntegratedTest
+{
+}
+
+public class BlockPruningBatchTestBase : AElfIntegratedTest
+{
+}
+
+public class BlockPruningThresholdTestBase : AElfIntegratedTest
+{
+}
+
+public class BlockPruningConfigCorrectionTestBase : AElfIntegratedTest
+{
+}
+
+public class BlockPruningBatchSizeUpperLimitTestBase : AElfIntegratedTest
+{
+}
diff --git a/test/AElf.Kernel.BlockPruning.Tests/BlockPruningTestModule.cs b/test/AElf.Kernel.BlockPruning.Tests/BlockPruningTestModule.cs
new file mode 100644
index 0000000000..b41850305a
--- /dev/null
+++ b/test/AElf.Kernel.BlockPruning.Tests/BlockPruningTestModule.cs
@@ -0,0 +1,131 @@
+using AElf.Modularity;
+using Microsoft.Extensions.DependencyInjection;
+using Volo.Abp;
+using Volo.Abp.EventBus;
+using Volo.Abp.Modularity;
+using Volo.Abp.Threading;
+
+namespace AElf.Kernel.BlockPruning;
+
+[DependsOn(
+ typeof(AbpEventBusModule),
+ typeof(BlockPruningAElfModule),
+ typeof(TestBaseKernelAElfModule))]
+public class BlockPruningTestModule : AElfModule
+{
+ // public override void ConfigureServices(ServiceConfigurationContext context)
+ // {
+ // context.Services.AddSingleton();
+ // }
+
+ public override void OnApplicationInitialization(ApplicationInitializationContext context)
+ {
+ var kernelTestHelper = context.ServiceProvider.GetService();
+ AsyncHelper.RunSync(() => kernelTestHelper!.MockChainAsync());
+ }
+}
+
+///
+/// LIB=5, RetainDistance=2 -> pruneTarget=3, prunable range: heights 2~3
+///
+[DependsOn(typeof(BlockPruningTestModule))]
+public class BlockPruningServiceTestModule : AElfModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.PostConfigure(options =>
+ {
+ options.Enabled = true;
+ options.RetainDistance = 2;
+ options.BatchSize = 100;
+ options.PruneThreshold = 0;
+ options.BatchDelayMilliseconds = 0;
+ });
+ }
+}
+
+[DependsOn(typeof(BlockPruningTestModule))]
+public class BlockPruningDisabledTestModule : AElfModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.PostConfigure(options =>
+ {
+ options.Enabled = false;
+ options.RetainDistance = 2;
+ options.BatchSize = 100;
+ options.PruneThreshold = 0;
+ options.BatchDelayMilliseconds = 0;
+ });
+ }
+}
+
+///
+/// LIB=5, RetainDistance=0 -> pruneTarget=5, prunable range: heights 2~5. BatchSize=2.
+///
+[DependsOn(typeof(BlockPruningTestModule))]
+public class BlockPruningBatchTestModule : AElfModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.PostConfigure(options =>
+ {
+ options.Enabled = true;
+ options.RetainDistance = 0;
+ options.BatchSize = 2;
+ options.PruneThreshold = 0;
+ options.BatchDelayMilliseconds = 0;
+ });
+ }
+}
+
+///
+/// LIB=5, RetainDistance=2, PruneThreshold=100
+/// => pruneTarget = 5 - 2 = 3, gap = 3 - 0 = 3 < 100 => pruning skipped
+///
+[DependsOn(typeof(BlockPruningTestModule))]
+public class BlockPruningThresholdTestModule : AElfModule
+{
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ context.Services.PostConfigure(options =>
+ {
+ options.Enabled = true;
+ options.RetainDistance = 2;
+ options.BatchSize = 100;
+ options.PruneThreshold = 100;
+ options.BatchDelayMilliseconds = 0;
+ });
+ }
+}
+
+///
+/// Verify PostConfigure corrects invalid values.
+/// BlockPruningConfigCorrectionTestModule sets below-minimum values to validate correction.
+///
+[DependsOn(typeof(BlockPruningTestModule))]
+public class BlockPruningConfigCorrectionTestModule : AElf.Modularity.AElfModule
+{
+ public override void ConfigureServices(Volo.Abp.Modularity.ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.RetainDistance = 100;
+ options.BatchSize = 0;
+ options.PruneThreshold = -1;
+ options.BatchDelayMilliseconds = -1;
+ });
+ }
+}
+
+[DependsOn(typeof(BlockPruningTestModule))]
+public class BlockPruningBatchSizeUpperLimitTestModule : AElf.Modularity.AElfModule
+{
+ public override void ConfigureServices(Volo.Abp.Modularity.ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.BatchSize = 999999;
+ });
+ }
+}
diff --git a/test/AElf.Kernel.BlockPruning.Tests/Domain/BlockPruningInfoManagerTests.cs b/test/AElf.Kernel.BlockPruning.Tests/Domain/BlockPruningInfoManagerTests.cs
new file mode 100644
index 0000000000..4fda5b57cc
--- /dev/null
+++ b/test/AElf.Kernel.BlockPruning.Tests/Domain/BlockPruningInfoManagerTests.cs
@@ -0,0 +1,39 @@
+using AElf.Kernel.BlockPruning.Domain;
+
+namespace AElf.Kernel.BlockPruning;
+
+public sealed class BlockPruningInfoManagerTests : BlockPruningTestBase
+{
+ private readonly IBlockPruningInfoManager _blockPruningInfoManager;
+
+ public BlockPruningInfoManagerTests()
+ {
+ _blockPruningInfoManager = GetRequiredService();
+ }
+
+ [Fact]
+ public async Task GetLastPrunedHeight_Initial_ShouldReturn0()
+ {
+ var height = await _blockPruningInfoManager.GetLastPrunedHeightAsync();
+ height.ShouldBe(0);
+ }
+
+ [Fact]
+ public async Task SetAndGet_ShouldReturnCorrectValue()
+ {
+ await _blockPruningInfoManager.SetLastPrunedHeightAsync(1000);
+ var height = await _blockPruningInfoManager.GetLastPrunedHeightAsync();
+ height.ShouldBe(1000);
+ }
+
+ [Fact]
+ public async Task MultipleUpdates_ShouldReturnLatest()
+ {
+ await _blockPruningInfoManager.SetLastPrunedHeightAsync(100);
+ await _blockPruningInfoManager.SetLastPrunedHeightAsync(200);
+ await _blockPruningInfoManager.SetLastPrunedHeightAsync(300);
+
+ var height = await _blockPruningInfoManager.GetLastPrunedHeightAsync();
+ height.ShouldBe(300);
+ }
+}
diff --git a/test/AElf.Kernel.BlockPruning.Tests/GlobalUsings.cs b/test/AElf.Kernel.BlockPruning.Tests/GlobalUsings.cs
new file mode 100644
index 0000000000..e5ec340df8
--- /dev/null
+++ b/test/AElf.Kernel.BlockPruning.Tests/GlobalUsings.cs
@@ -0,0 +1,5 @@
+global using Xunit;
+global using Google.Protobuf;
+global using Shouldly;
+global using AElf.Types;
+global using System.Threading.Tasks;
diff --git a/test/AElf.Kernel.BlockPruning.Tests/NewIrreversibleBlockFoundEventHandlerTests.cs b/test/AElf.Kernel.BlockPruning.Tests/NewIrreversibleBlockFoundEventHandlerTests.cs
new file mode 100644
index 0000000000..45821aa1b3
--- /dev/null
+++ b/test/AElf.Kernel.BlockPruning.Tests/NewIrreversibleBlockFoundEventHandlerTests.cs
@@ -0,0 +1,88 @@
+using System.Diagnostics;
+using AElf.Kernel.BlockPruning.Domain;
+using AElf.Kernel.Blockchain.Events;
+
+namespace AElf.Kernel.BlockPruning;
+
+public sealed class EventHandlerEnabledTests : BlockPruningServiceTestBase
+{
+ private readonly IBlockPruningInfoManager _blockPruningInfoManager;
+ private readonly ITaskQueueManager _taskQueueManager;
+ private readonly NewIrreversibleBlockFoundEventHandler _eventHandler;
+
+ public EventHandlerEnabledTests()
+ {
+ _taskQueueManager = GetRequiredService();
+ _blockPruningInfoManager = GetRequiredService();
+ _eventHandler = GetRequiredService();
+ }
+
+ [Fact]
+ public async Task HandleEvent_ShouldEnqueueTask()
+ {
+ var evt = new NewIrreversibleBlockFoundEvent
+ {
+ BlockHeight = 10,
+ BlockHash = HashHelper.ComputeFrom("block10")
+ };
+
+ await _eventHandler.HandleEventAsync(evt);
+
+ var queue = _taskQueueManager.GetQueue(BlockPruningConstants.BlockPruningQueueName);
+ queue.ShouldNotBeNull();
+ }
+
+ [Fact]
+ public async Task HandleEvent_PruningExecuted_Test()
+ {
+ var evt = new NewIrreversibleBlockFoundEvent
+ {
+ BlockHeight = 10,
+ BlockHash = HashHelper.ComputeFrom("block10")
+ };
+
+ await _eventHandler.HandleEventAsync(evt);
+
+ var sw = Stopwatch.StartNew();
+ long height = 0;
+ while (sw.ElapsedMilliseconds < 5000)
+ {
+ height = await _blockPruningInfoManager.GetLastPrunedHeightAsync();
+ if (height > 0) break;
+ await Task.Delay(100);
+ }
+
+ height.ShouldBeGreaterThan(0);
+ }
+}
+
+public sealed class EventHandlerDisabledTests : BlockPruningDisabledTestBase
+{
+ private readonly IBlockPruningInfoManager _blockPruningInfoManager;
+ private readonly ITaskQueueManager _taskQueueManager;
+ private readonly NewIrreversibleBlockFoundEventHandler _eventHandler;
+
+ public EventHandlerDisabledTests()
+ {
+ _taskQueueManager = GetRequiredService();
+ _blockPruningInfoManager = GetRequiredService();
+ _eventHandler = GetRequiredService();
+ }
+
+ [Fact]
+ public async Task HandleEvent_Disabled_ShouldNotEnqueue()
+ {
+ var evt = new NewIrreversibleBlockFoundEvent
+ {
+ BlockHeight = 10,
+ BlockHash = HashHelper.ComputeFrom("block10")
+ };
+
+ await _eventHandler.HandleEventAsync(evt);
+
+ await Task.Delay(500);
+
+ var height = await _blockPruningInfoManager.GetLastPrunedHeightAsync();
+ height.ShouldBe(0);
+ }
+}
diff --git a/test/AElf.Kernel.Core.Tests/Blockchain/Domain/BlockManagerTests.cs b/test/AElf.Kernel.Core.Tests/Blockchain/Domain/BlockManagerTests.cs
index 4bf0da80a9..f5bf7bf5aa 100644
--- a/test/AElf.Kernel.Core.Tests/Blockchain/Domain/BlockManagerTests.cs
+++ b/test/AElf.Kernel.Core.Tests/Blockchain/Domain/BlockManagerTests.cs
@@ -1,4 +1,6 @@
-namespace AElf.Kernel.Blockchain.Domain;
+using System.Collections.Generic;
+
+namespace AElf.Kernel.Blockchain.Domain;
[Trait("Category", AElfBlockchainModule)]
public sealed class BlockManagerTests : AElfKernelTestBase
@@ -40,4 +42,111 @@ public async Task GetBlock_Header_And_Body_Test()
(await _blockManager.HasBlockAsync(blockHash)).ShouldBeFalse();
}
+
+ [Fact]
+ public async Task RemoveBlocksAsync_BatchDelete_Test()
+ {
+ var blocks = new List();
+ var hashes = new List();
+ for (var i = 0; i < 3; i++)
+ {
+ var block = _kernelTestHelper.GenerateBlock(i, i == 0 ? Hash.Empty : blocks[i - 1].GetHash());
+ var hash = block.GetHash();
+ await _blockManager.AddBlockHeaderAsync(block.Header);
+ await _blockManager.AddBlockBodyAsync(hash, block.Body);
+ blocks.Add(block);
+ hashes.Add(hash);
+ }
+
+ foreach (var h in hashes) (await _blockManager.GetBlockAsync(h)).ShouldNotBeNull();
+
+ await _blockManager.RemoveBlocksAsync(hashes);
+
+ foreach (var h in hashes) (await _blockManager.GetBlockAsync(h)).ShouldBeNull();
+ }
+
+ [Fact]
+ public async Task GetBlockBodiesAsync_Should_Preserve_Input_Order_And_Missing_Entries_Test()
+ {
+ var blockA = _kernelTestHelper.GenerateBlock(0, Hash.Empty);
+ var blockB = _kernelTestHelper.GenerateBlock(1, blockA.GetHash());
+ var hashA = blockA.GetHash();
+ var hashB = blockB.GetHash();
+ var fakeHash = HashHelper.ComputeFrom("missing-block");
+
+ await _blockManager.AddBlockBodyAsync(hashA, blockA.Body);
+ await _blockManager.AddBlockBodyAsync(hashB, blockB.Body);
+
+ var blockBodies = await _blockManager.GetBlockBodiesAsync(new List { hashB, fakeHash, hashA });
+
+ blockBodies.Count.ShouldBe(3);
+ blockBodies[0].ShouldBe(blockB.Body);
+ blockBodies[1].ShouldBeNull();
+ blockBodies[2].ShouldBe(blockA.Body);
+ }
+
+ [Fact]
+ public async Task RemoveBlocksAsync_PartialDelete_Test()
+ {
+ var blockA = _kernelTestHelper.GenerateBlock(0, Hash.Empty);
+ var blockB = _kernelTestHelper.GenerateBlock(1, blockA.GetHash());
+ var blockC = _kernelTestHelper.GenerateBlock(2, blockB.GetHash());
+ var hashA = blockA.GetHash();
+ var hashB = blockB.GetHash();
+ var hashC = blockC.GetHash();
+
+ await _blockManager.AddBlockHeaderAsync(blockA.Header);
+ await _blockManager.AddBlockBodyAsync(hashA, blockA.Body);
+ await _blockManager.AddBlockHeaderAsync(blockB.Header);
+ await _blockManager.AddBlockBodyAsync(hashB, blockB.Body);
+ await _blockManager.AddBlockHeaderAsync(blockC.Header);
+ await _blockManager.AddBlockBodyAsync(hashC, blockC.Body);
+
+ await _blockManager.RemoveBlocksAsync(new List { hashA, hashB });
+
+ (await _blockManager.GetBlockAsync(hashA)).ShouldBeNull();
+ (await _blockManager.GetBlockAsync(hashB)).ShouldBeNull();
+ (await _blockManager.GetBlockAsync(hashC)).ShouldNotBeNull();
+ }
+
+ [Fact]
+ public async Task RemoveBlocksAsync_HeaderAndBody_Both_Deleted_Test()
+ {
+ var block = _kernelTestHelper.GenerateBlock(0, Hash.Empty);
+ var hash = block.GetHash();
+ await _blockManager.AddBlockHeaderAsync(block.Header);
+ await _blockManager.AddBlockBodyAsync(hash, block.Body);
+
+ await _blockManager.RemoveBlocksAsync(new List { hash });
+
+ (await _blockManager.GetBlockHeaderAsync(hash)).ShouldBeNull();
+ (await _blockManager.GetBlockAsync(hash)).ShouldBeNull();
+ }
+
+ [Fact]
+ public async Task RemoveBlocksAsync_EmptyList_Test()
+ {
+ await _blockManager.RemoveBlocksAsync(new List());
+ }
+
+ [Fact]
+ public async Task RemoveBlocksAsync_NonExistent_Test()
+ {
+ var fakeHash = HashHelper.ComputeFrom("nonexistent");
+ await _blockManager.RemoveBlocksAsync(new List { fakeHash });
+ }
+
+ [Fact]
+ public async Task RemoveBlocksAsync_Mixed_Test()
+ {
+ var block = _kernelTestHelper.GenerateBlock(0, Hash.Empty);
+ var hash = block.GetHash();
+ await _blockManager.AddBlockHeaderAsync(block.Header);
+ await _blockManager.AddBlockBodyAsync(hash, block.Body);
+
+ var fakeHash = HashHelper.ComputeFrom("nonexistent");
+ await _blockManager.RemoveBlocksAsync(new List { hash, fakeHash });
+
+ (await _blockManager.GetBlockAsync(hash)).ShouldBeNull();
+ }
}
\ No newline at end of file
diff --git a/test/AElf.Kernel.Core.Tests/Blockchain/Domain/ChainManagerTests.cs b/test/AElf.Kernel.Core.Tests/Blockchain/Domain/ChainManagerTests.cs
index 1fe4a8eeb5..8e75071b53 100644
--- a/test/AElf.Kernel.Core.Tests/Blockchain/Domain/ChainManagerTests.cs
+++ b/test/AElf.Kernel.Core.Tests/Blockchain/Domain/ChainManagerTests.cs
@@ -83,6 +83,40 @@ public async Task Create_Chain_ThrowInvalidOperationException()
await _chainManager.CreateAsync(_blocks[1]).ShouldThrowAsync();
}
+ [Fact]
+ public async Task GetChainBlockIndicesAsync_Should_Preserve_Input_Order_And_Missing_Entries_Test()
+ {
+ var chain = await _chainManager.CreateAsync(_genesis);
+
+ await _chainManager.AttachBlockToChainAsync(chain, new ChainBlockLink
+ {
+ Height = 1.BlockHeight(),
+ BlockHash = _blocks[1],
+ PreviousBlockHash = _genesis
+ });
+
+ await _chainManager.AttachBlockToChainAsync(chain, new ChainBlockLink
+ {
+ Height = 2.BlockHeight(),
+ BlockHash = _blocks[2],
+ PreviousBlockHash = _blocks[1]
+ });
+
+ (await _chainManager.SetIrreversibleBlockAsync(chain, _blocks[2])).ShouldBeTrue();
+
+ var chainBlockIndices = await _chainManager.GetChainBlockIndicesAsync(new List
+ {
+ 2.BlockHeight(),
+ 3.BlockHeight(),
+ 0.BlockHeight()
+ });
+
+ chainBlockIndices.Count.ShouldBe(3);
+ chainBlockIndices[0].BlockHash.ShouldBe(_blocks[2]);
+ chainBlockIndices[1].ShouldBeNull();
+ chainBlockIndices[2].BlockHash.ShouldBe(_genesis);
+ }
+
[Fact]
public async Task LIB_Blocks_Test()
{
@@ -1130,4 +1164,115 @@ await _chainManager.SetChainBlockLinkAsync(new ChainBlockLink
(await _chainManager.GetChainBlockLinkAsync(_blocks[1])).ShouldBe(chainBlockLink);
}
+
+ [Fact]
+ public async Task RemoveChainBlockLinksAsync_BatchDelete_Test()
+ {
+ var chain = await _chainManager.CreateAsync(_genesis);
+
+ var linkA = new ChainBlockLink
+ {
+ Height = 1.BlockHeight(), BlockHash = _blocks[1], PreviousBlockHash = _genesis
+ };
+ var linkB = new ChainBlockLink
+ {
+ Height = 2.BlockHeight(), BlockHash = _blocks[2], PreviousBlockHash = _blocks[1]
+ };
+ var linkC = new ChainBlockLink
+ {
+ Height = 3.BlockHeight(), BlockHash = _blocks[3], PreviousBlockHash = _blocks[2]
+ };
+
+ await _chainManager.AttachBlockToChainAsync(chain, linkA);
+ await _chainManager.AttachBlockToChainAsync(chain, linkB);
+ await _chainManager.AttachBlockToChainAsync(chain, linkC);
+
+ (await _chainManager.GetChainBlockLinkAsync(_blocks[1])).ShouldNotBeNull();
+ (await _chainManager.GetChainBlockLinkAsync(_blocks[2])).ShouldNotBeNull();
+ (await _chainManager.GetChainBlockLinkAsync(_blocks[3])).ShouldNotBeNull();
+
+ await _chainManager.RemoveChainBlockLinksAsync(new List { _blocks[1], _blocks[2], _blocks[3] });
+
+ (await _chainManager.GetChainBlockLinkAsync(_blocks[1])).ShouldBeNull();
+ (await _chainManager.GetChainBlockLinkAsync(_blocks[2])).ShouldBeNull();
+ (await _chainManager.GetChainBlockLinkAsync(_blocks[3])).ShouldBeNull();
+ }
+
+ [Fact]
+ public async Task RemoveChainBlockLinksAsync_PartialDelete_Test()
+ {
+ var chain = await _chainManager.CreateAsync(_genesis);
+
+ await _chainManager.AttachBlockToChainAsync(chain, new ChainBlockLink
+ {
+ Height = 1.BlockHeight(), BlockHash = _blocks[1], PreviousBlockHash = _genesis
+ });
+ await _chainManager.AttachBlockToChainAsync(chain, new ChainBlockLink
+ {
+ Height = 2.BlockHeight(), BlockHash = _blocks[2], PreviousBlockHash = _blocks[1]
+ });
+ await _chainManager.AttachBlockToChainAsync(chain, new ChainBlockLink
+ {
+ Height = 3.BlockHeight(), BlockHash = _blocks[3], PreviousBlockHash = _blocks[2]
+ });
+
+ await _chainManager.RemoveChainBlockLinksAsync(new List { _blocks[1], _blocks[2] });
+
+ (await _chainManager.GetChainBlockLinkAsync(_blocks[1])).ShouldBeNull();
+ (await _chainManager.GetChainBlockLinkAsync(_blocks[2])).ShouldBeNull();
+ (await _chainManager.GetChainBlockLinkAsync(_blocks[3])).ShouldNotBeNull();
+ }
+
+ [Fact]
+ public async Task RemoveChainBlockLinksAsync_EmptyList_Test()
+ {
+ await _chainManager.RemoveChainBlockLinksAsync(new List());
+ }
+
+ [Fact]
+ public async Task RemoveChainBlockLinksAsync_NonExistent_Test()
+ {
+ await _chainManager.RemoveChainBlockLinksAsync(new List { HashHelper.ComputeFrom("fake") });
+ }
+
+ [Fact]
+ public async Task RemoveChainBlockLinksAsync_Mixed_Test()
+ {
+ var chain = await _chainManager.CreateAsync(_genesis);
+
+ await _chainManager.AttachBlockToChainAsync(chain, new ChainBlockLink
+ {
+ Height = 1.BlockHeight(), BlockHash = _blocks[1], PreviousBlockHash = _genesis
+ });
+
+ var fakeHash = HashHelper.ComputeFrom("fake");
+ await _chainManager.RemoveChainBlockLinksAsync(new List { _blocks[1], fakeHash });
+
+ (await _chainManager.GetChainBlockLinkAsync(_blocks[1])).ShouldBeNull();
+ }
+
+ [Fact]
+ public async Task RemoveChainBlockLinksAsync_CacheConsistency_Test()
+ {
+ var chain = await _chainManager.CreateAsync(_genesis);
+
+ await _chainManager.AttachBlockToChainAsync(chain, new ChainBlockLink
+ {
+ Height = 1.BlockHeight(), BlockHash = _blocks[1], PreviousBlockHash = _genesis
+ });
+ await _chainManager.AttachBlockToChainAsync(chain, new ChainBlockLink
+ {
+ Height = 2.BlockHeight(), BlockHash = _blocks[2], PreviousBlockHash = _blocks[1]
+ });
+
+ _chainBlockLinkCacheProvider.GetChainBlockLink(_blocks[1]).ShouldNotBeNull();
+ _chainBlockLinkCacheProvider.GetChainBlockLink(_blocks[2]).ShouldNotBeNull();
+
+ await _chainManager.RemoveChainBlockLinksAsync(new List { _blocks[1], _blocks[2] });
+
+ (await _chainManager.GetChainBlockLinkAsync(_blocks[1])).ShouldBeNull();
+ (await _chainManager.GetChainBlockLinkAsync(_blocks[2])).ShouldBeNull();
+ _chainBlockLinkCacheProvider.GetChainBlockLink(_blocks[1]).ShouldBeNull();
+ _chainBlockLinkCacheProvider.GetChainBlockLink(_blocks[2]).ShouldBeNull();
+ }
}
\ No newline at end of file
diff --git a/test/AElf.Kernel.Core.Tests/Blockchain/Domain/TransactionResultManagerTests.cs b/test/AElf.Kernel.Core.Tests/Blockchain/Domain/TransactionResultManagerTests.cs
new file mode 100644
index 0000000000..7bd65545eb
--- /dev/null
+++ b/test/AElf.Kernel.Core.Tests/Blockchain/Domain/TransactionResultManagerTests.cs
@@ -0,0 +1,106 @@
+using System;
+using System.Collections.Generic;
+
+namespace AElf.Kernel.Blockchain.Domain;
+
+[Trait("Category", AElfBlockchainModule)]
+public sealed class TransactionResultManagerTests : AElfKernelTestBase
+{
+ private readonly KernelTestHelper _kernelTestHelper;
+ private readonly ITransactionResultManager _transactionResultManager;
+
+ public TransactionResultManagerTests()
+ {
+ _transactionResultManager = GetRequiredService();
+ _kernelTestHelper = GetRequiredService();
+ }
+
+ [Fact]
+ public async Task RemoveTransactionResultsAsync_BatchDelete_Test()
+ {
+ var blockHash = HashHelper.ComputeFrom("block1");
+ var txIds = new List();
+
+ for (var i = 0; i < 3; i++)
+ {
+ var tx = _kernelTestHelper.GenerateTransaction();
+ var txId = tx.GetHash();
+ txIds.Add(txId);
+ var result = _kernelTestHelper.GenerateTransactionResult(tx, TransactionResultStatus.Mined);
+ await _transactionResultManager.AddTransactionResultAsync(result, blockHash);
+ }
+
+ foreach (var txId in txIds)
+ (await _transactionResultManager.GetTransactionResultAsync(txId, blockHash)).ShouldNotBeNull();
+
+ var blockHashes = new List { blockHash, blockHash, blockHash };
+ await _transactionResultManager.RemoveTransactionResultsAsync(txIds, blockHashes);
+
+ foreach (var txId in txIds)
+ (await _transactionResultManager.GetTransactionResultAsync(txId, blockHash)).ShouldBeNull();
+ }
+
+ [Fact]
+ public async Task RemoveTransactionResultsAsync_XorKey_Correctness_Test()
+ {
+ var tx = _kernelTestHelper.GenerateTransaction();
+ var txId = tx.GetHash();
+ var blockHash1 = HashHelper.ComputeFrom("blockA");
+ var blockHash2 = HashHelper.ComputeFrom("blockB");
+
+ var result1 = _kernelTestHelper.GenerateTransactionResult(tx, TransactionResultStatus.Mined);
+ var result2 = _kernelTestHelper.GenerateTransactionResult(tx, TransactionResultStatus.Mined);
+
+ await _transactionResultManager.AddTransactionResultAsync(result1, blockHash1);
+ await _transactionResultManager.AddTransactionResultAsync(result2, blockHash2);
+
+ await _transactionResultManager.RemoveTransactionResultsAsync(
+ new List { txId }, new List { blockHash1 });
+
+ (await _transactionResultManager.GetTransactionResultAsync(txId, blockHash1)).ShouldBeNull();
+ (await _transactionResultManager.GetTransactionResultAsync(txId, blockHash2)).ShouldNotBeNull();
+ }
+
+ [Fact]
+ public async Task RemoveTransactionResultsAsync_EmptyList_Test()
+ {
+ await _transactionResultManager.RemoveTransactionResultsAsync(new List(), new List());
+ }
+
+ [Fact]
+ public async Task RemoveTransactionResultsAsync_NonExistent_Test()
+ {
+ var fakeTxId = HashHelper.ComputeFrom("fakeTx");
+ var fakeBlockHash = HashHelper.ComputeFrom("fakeBlock");
+ await _transactionResultManager.RemoveTransactionResultsAsync(
+ new List { fakeTxId }, new List { fakeBlockHash });
+ }
+
+ [Fact]
+ public async Task RemoveTransactionResultsAsync_Mixed_Test()
+ {
+ var blockHash = HashHelper.ComputeFrom("block1");
+ var tx = _kernelTestHelper.GenerateTransaction();
+ var txId = tx.GetHash();
+ var result = _kernelTestHelper.GenerateTransactionResult(tx, TransactionResultStatus.Mined);
+ await _transactionResultManager.AddTransactionResultAsync(result, blockHash);
+
+ var fakeTxId = HashHelper.ComputeFrom("nonexistentTx");
+
+ await _transactionResultManager.RemoveTransactionResultsAsync(
+ new List { txId, fakeTxId },
+ new List { blockHash, blockHash });
+
+ (await _transactionResultManager.GetTransactionResultAsync(txId, blockHash)).ShouldBeNull();
+ }
+
+ [Fact]
+ public async Task RemoveTransactionResultsAsync_CountMismatch_Test()
+ {
+ var txIds = new List { HashHelper.ComputeFrom("tx1"), HashHelper.ComputeFrom("tx2") };
+ var blockHashes = new List { HashHelper.ComputeFrom("block1") };
+
+ await Assert.ThrowsAsync(
+ () => _transactionResultManager.RemoveTransactionResultsAsync(txIds, blockHashes));
+ }
+}
diff --git a/test/AElf.OS.Core.Tests/Network/Extensions/BlockchainServiceExtensionsTests.cs b/test/AElf.OS.Core.Tests/Network/Extensions/BlockchainServiceExtensionsTests.cs
new file mode 100644
index 0000000000..1dbbac1007
--- /dev/null
+++ b/test/AElf.OS.Core.Tests/Network/Extensions/BlockchainServiceExtensionsTests.cs
@@ -0,0 +1,91 @@
+using System.Threading.Tasks;
+using AElf.Kernel.Blockchain.Application;
+using AElf.Kernel.Blockchain.Domain;
+using Shouldly;
+using Xunit;
+
+namespace AElf.OS.Network.Extensions;
+
+public class BlockchainServiceExtensionsTests : OSCoreWithChainTestBase
+{
+ private readonly IBlockchainService _blockchainService;
+ private readonly IBlockManager _blockManager;
+
+ public BlockchainServiceExtensionsTests()
+ {
+ _blockchainService = GetRequiredService();
+ _blockManager = GetRequiredService();
+ }
+
+ [Fact]
+ public async Task GetBlocksWithTransactions_AllBlocksExist_Test()
+ {
+ var chain = await _blockchainService.GetChainAsync();
+ var genesisHash = chain.GenesisBlockHash;
+
+ var result = await _blockchainService.GetBlocksWithTransactionsAsync(genesisHash, 3);
+
+ result.ShouldNotBeNull();
+ result.Count.ShouldBe(3);
+ foreach (var bwt in result)
+ {
+ bwt.Header.ShouldNotBeNull();
+ }
+ }
+
+ [Fact]
+ public async Task GetBlocksWithTransactions_MiddleBlockDeleted_ShouldTruncate_Test()
+ {
+ var chain = await _blockchainService.GetChainAsync();
+ var genesisHash = chain.GenesisBlockHash;
+
+ var blocks = await _blockchainService.GetBlocksInBestChainBranchAsync(genesisHash, 5);
+ blocks.Count.ShouldBeGreaterThanOrEqualTo(5);
+
+ await _blockManager.RemoveBlockAsync(blocks[2].GetHash());
+
+ var result = await _blockchainService.GetBlocksWithTransactionsAsync(genesisHash, 5);
+
+ result.ShouldNotBeNull();
+ result.Count.ShouldBe(2);
+ }
+
+ [Fact]
+ public async Task GetBlocksWithTransactions_FirstBlockDeleted_ShouldReturnEmpty_Test()
+ {
+ var chain = await _blockchainService.GetChainAsync();
+ var genesisHash = chain.GenesisBlockHash;
+
+ var blocks = await _blockchainService.GetBlocksInBestChainBranchAsync(genesisHash, 3);
+ blocks.Count.ShouldBeGreaterThanOrEqualTo(3);
+
+ await _blockManager.RemoveBlockAsync(blocks[0].GetHash());
+
+ var result = await _blockchainService.GetBlocksWithTransactionsAsync(genesisHash, 3);
+
+ result.ShouldNotBeNull();
+ result.Count.ShouldBe(0);
+ }
+
+ [Fact]
+ public async Task GetBlockWithTransactionsByHash_BlockExists_Test()
+ {
+ var chain = await _blockchainService.GetChainAsync();
+ var block = await _blockchainService.GetBlockByHashAsync(chain.GenesisBlockHash);
+ block.ShouldNotBeNull();
+
+ var result = await _blockchainService.GetBlockWithTransactionsByHashAsync(chain.GenesisBlockHash);
+
+ result.ShouldNotBeNull();
+ result.Header.ShouldNotBeNull();
+ }
+
+ [Fact]
+ public async Task GetBlockWithTransactionsByHash_BlockNotExist_ShouldReturnNull_Test()
+ {
+ var result = await _blockchainService.GetBlockWithTransactionsByHashAsync(
+ HashHelper.ComputeFrom("nonexistent"));
+
+ result.ShouldBeNull();
+ }
+}
diff --git a/test/AElf.OS.Core.Tests/OSCoreTestBase.cs b/test/AElf.OS.Core.Tests/OSCoreTestBase.cs
index 447dbb0595..c6a7ff9a7c 100644
--- a/test/AElf.OS.Core.Tests/OSCoreTestBase.cs
+++ b/test/AElf.OS.Core.Tests/OSCoreTestBase.cs
@@ -1,4 +1,4 @@
-using AElf.OS.Network;
+using AElf.OS.Network;
using AElf.TestBase;
namespace AElf.OS;
@@ -25,4 +25,8 @@ public class PeerInvalidTransactionTestBase : AElfIntegratedTest
{
+}
+
+public class OSCoreWithChainTestBase : AElfIntegratedTest
+{
}
\ No newline at end of file