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