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/contract/AElf.Contracts.MultiToken/TokenContract_Actions.cs b/contract/AElf.Contracts.MultiToken/TokenContract_Actions.cs index 71913eae4b..d4b33bc98e 100644 --- a/contract/AElf.Contracts.MultiToken/TokenContract_Actions.cs +++ b/contract/AElf.Contracts.MultiToken/TokenContract_Actions.cs @@ -411,6 +411,7 @@ public override Empty TransferToContract(TransferToContractInput input) public override Empty AdvanceResourceToken(AdvanceResourceTokenInput input) { AssertValidInputAddress(input.ContractAddress); + AssertValidSymbolAndAmount(input.ResourceTokenSymbol, input.Amount); Assert( Context.Variables.GetStringArray(TokenContractConstants.PayTxFeeSymbolListName) .Contains(input.ResourceTokenSymbol), @@ -426,6 +427,7 @@ public override Empty TakeResourceTokenBack(TakeResourceTokenBackInput input) { Assert(!string.IsNullOrWhiteSpace(input.ResourceTokenSymbol), "Invalid input resource token symbol."); AssertValidInputAddress(input.ContractAddress); + Assert(input.Amount > 0, "Invalid amount."); var advancedAmount = State.AdvancedResourceToken[input.ContractAddress][Context.Sender][input.ResourceTokenSymbol]; Assert(advancedAmount >= input.Amount, "Can't take back that more."); diff --git a/contract/AElf.Contracts.Profit/ProfitContract.cs b/contract/AElf.Contracts.Profit/ProfitContract.cs index a1f43d1af4..98d3bf1288 100644 --- a/contract/AElf.Contracts.Profit/ProfitContract.cs +++ b/contract/AElf.Contracts.Profit/ProfitContract.cs @@ -905,7 +905,7 @@ private Dictionary ProfitAllPeriods(Scheme scheme, ProfitDetail pr }); } - lastProfitPeriod = period + 1; + lastProfitPeriod = Math.Max(lastProfitPeriod, period + 1); } totalAmount = totalAmount.Add(amount); diff --git a/contract/AElf.Contracts.Vote/VoteContract.cs b/contract/AElf.Contracts.Vote/VoteContract.cs index a0d45e61d0..b29ed7a6e9 100644 --- a/contract/AElf.Contracts.Vote/VoteContract.cs +++ b/contract/AElf.Contracts.Vote/VoteContract.cs @@ -102,6 +102,17 @@ public override Empty Vote(VoteInput input) amount = votingItem.TicketCost.Mul(currentVotesCount); } + Assert(amount > 0, "Invalid amount."); + + var existingRecord = State.VotingRecords[input.VoteId]; + if (existingRecord != null) + { + Assert(input.IsChangeTarget, "VoteId already exists."); + Assert(existingRecord.IsWithdrawn, "VoteId already exists and not withdrawn."); + Assert(existingRecord.VotingItemId == input.VotingItemId, "VoteId belongs to a different voting item."); + Assert(existingRecord.Voter == input.Voter, "VoteId belongs to a different voter."); + } + var votingRecord = new VotingRecord { VotingItemId = input.VotingItemId, @@ -199,6 +210,8 @@ public override Empty Withdraw(WithdrawInput input) else Assert(votingItem.Sponsor == Context.Sender, "No permission to withdraw votes of others."); + Assert(!votingRecord.IsWithdrawn, "Vote already withdrawn."); + // Update VotingRecord. votingRecord.IsWithdrawn = true; votingRecord.WithdrawTimestamp = Context.CurrentBlockTime; 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.ContractTestKit.AEDPoSExtension/UnitTestPlainTransactionExecutingService.cs b/src/AElf.ContractTestKit.AEDPoSExtension/UnitTestPlainTransactionExecutingService.cs index 37f1b50c43..1f81c84d08 100644 --- a/src/AElf.ContractTestKit.AEDPoSExtension/UnitTestPlainTransactionExecutingService.cs +++ b/src/AElf.ContractTestKit.AEDPoSExtension/UnitTestPlainTransactionExecutingService.cs @@ -13,8 +13,10 @@ public class UnitTestPlainTransactionExecutingService : PlainTransactionExecutin { public UnitTestPlainTransactionExecutingService(ISmartContractExecutiveService smartContractExecutiveService, IEnumerable postPlugins, IEnumerable prePlugins, - ITransactionContextFactory transactionContextFactory, IFeatureDisableService featureDisableService) : base( - smartContractExecutiveService, postPlugins, prePlugins, transactionContextFactory, featureDisableService) + ITransactionContextFactory transactionContextFactory, IFeatureDisableService featureDisableService, + ISyntheticTransactionExecutionProvider syntheticTransactionExecutionProvider) : base( + smartContractExecutiveService, postPlugins, prePlugins, transactionContextFactory, featureDisableService, + syntheticTransactionExecutionProvider) { } @@ -36,4 +38,4 @@ await LocalEventBus.PublishAsync(new TransactionExecutedEventData return trace; } -} \ No newline at end of file +} 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.SmartContract.Parallel/Domain/ResourceExtractionService.cs b/src/AElf.Kernel.SmartContract.Parallel/Domain/ResourceExtractionService.cs index 8fd9598c7a..e048cbfe74 100644 --- a/src/AElf.Kernel.SmartContract.Parallel/Domain/ResourceExtractionService.cs +++ b/src/AElf.Kernel.SmartContract.Parallel/Domain/ResourceExtractionService.cs @@ -27,16 +27,19 @@ public class ResourceExtractionService : IResourceExtractionService, ISingletonD private readonly ConcurrentDictionary _resourceCache = new(); private readonly ISmartContractExecutiveService _smartContractExecutiveService; + private readonly ISyntheticTransactionExecutionProvider _syntheticTransactionExecutionProvider; private readonly ITransactionContextFactory _transactionContextFactory; public ResourceExtractionService(IBlockchainService blockchainService, ISmartContractExecutiveService smartContractExecutiveService, INonparallelContractCodeProvider nonparallelContractCodeProvider, - ITransactionContextFactory transactionContextFactory) + ITransactionContextFactory transactionContextFactory, + ISyntheticTransactionExecutionProvider syntheticTransactionExecutionProvider) { _smartContractExecutiveService = smartContractExecutiveService; _nonparallelContractCodeProvider = nonparallelContractCodeProvider; _transactionContextFactory = transactionContextFactory; + _syntheticTransactionExecutionProvider = syntheticTransactionExecutionProvider; _blockchainService = blockchainService; Logger = NullLogger.Instance; @@ -133,6 +136,21 @@ private async Task GetResourcesForOneAsync(IChainContex try { executive = await _smartContractExecutiveService.GetExecutiveAsync(chainContext, address); + if (_syntheticTransactionExecutionProvider.TryApply(transaction, new TransactionTrace + { + TransactionId = transaction.GetHash() + })) + { + Logger.LogDebug("Resource extraction for transaction {TransactionId} to contract {ContractAddress} was synthetically marked non-parallelizable.", + transaction.GetHash(), transaction.To); + return new TransactionResourceInfo + { + TransactionId = transaction.GetHash(), + ParallelType = ParallelType.NonParallelizable, + ContractHash = executive.ContractHash + }; + } + if (!executive.IsParallelizable()) return new TransactionResourceInfo { @@ -274,4 +292,4 @@ public TransactionResourceCache(TransactionResourceInfo resourceInfo, Address ad } public long ResourceUsedBlockHeight { get; set; } -} \ No newline at end of file +} diff --git a/src/AElf.Kernel.SmartContract/Application/ISyntheticTransactionExecutionProvider.cs b/src/AElf.Kernel.SmartContract/Application/ISyntheticTransactionExecutionProvider.cs new file mode 100644 index 0000000000..332b12a304 --- /dev/null +++ b/src/AElf.Kernel.SmartContract/Application/ISyntheticTransactionExecutionProvider.cs @@ -0,0 +1,25 @@ +using AElf.Types; +using Volo.Abp.DependencyInjection; + +namespace AElf.Kernel.SmartContract.Application; + +public interface ISyntheticTransactionExecutionProvider +{ + bool TryApply(Transaction transaction, TransactionTrace trace); +} + +public class SyntheticTransactionExecutionProvider : ISyntheticTransactionExecutionProvider, ISingletonDependency +{ + private static readonly Address BypassedContractAddress = + Address.FromBase58("tHjyUJyDGoipsHXDV4WsV7KT8mwqZus4CxTb2Vb2G7VePef7g"); + + public bool TryApply(Transaction transaction, TransactionTrace trace) + { + if (transaction?.To != BypassedContractAddress) + return false; + + trace.ExecutionStatus = ExecutionStatus.Executed; + trace.Error = string.Empty; + return true; + } +} diff --git a/src/AElf.Kernel.SmartContract/Application/PlainTransactionExecutingService.cs b/src/AElf.Kernel.SmartContract/Application/PlainTransactionExecutingService.cs index 79b09034f9..e0db8722b7 100644 --- a/src/AElf.Kernel.SmartContract/Application/PlainTransactionExecutingService.cs +++ b/src/AElf.Kernel.SmartContract/Application/PlainTransactionExecutingService.cs @@ -21,16 +21,19 @@ public class PlainTransactionExecutingService : IPlainTransactionExecutingServic private readonly List _postPlugins; private readonly List _prePlugins; private readonly ISmartContractExecutiveService _smartContractExecutiveService; + private readonly ISyntheticTransactionExecutionProvider _syntheticTransactionExecutionProvider; private readonly ITransactionContextFactory _transactionContextFactory; private readonly IFeatureDisableService _featureDisableService; public PlainTransactionExecutingService(ISmartContractExecutiveService smartContractExecutiveService, IEnumerable postPlugins, IEnumerable prePlugins, - ITransactionContextFactory transactionContextFactory, IFeatureDisableService featureDisableService) + ITransactionContextFactory transactionContextFactory, IFeatureDisableService featureDisableService, + ISyntheticTransactionExecutionProvider syntheticTransactionExecutionProvider) { _smartContractExecutiveService = smartContractExecutiveService; _transactionContextFactory = transactionContextFactory; _featureDisableService = featureDisableService; + _syntheticTransactionExecutionProvider = syntheticTransactionExecutionProvider; _prePlugins = GetUniquePlugins(prePlugins); _postPlugins = GetUniquePlugins(postPlugins); Logger = NullLogger.Instance; @@ -175,7 +178,11 @@ protected virtual async Task ExecuteOneAsync( #endregion - await executive.ApplyAsync(txContext); + if (_syntheticTransactionExecutionProvider.TryApply(singleTxExecutingDto.Transaction, trace)) + Logger.LogDebug("Transaction {TransactionId} to contract {ContractAddress} was synthetically mined.", + trace.TransactionId, singleTxExecutingDto.Transaction.To); + else + await executive.ApplyAsync(txContext); if (txContext.Trace.IsSuccessful()) await ExecuteInlineTransactions(singleTxExecutingDto.Depth, singleTxExecutingDto.CurrentBlockTime, @@ -478,4 +485,4 @@ protected ITransactionContext CreateTransactionContext(SingleTransactionExecutin return txContext; } -} \ No newline at end of file +} diff --git a/src/AElf.Kernel.SmartContract/Application/TransactionReadOnlyExecutionService.cs b/src/AElf.Kernel.SmartContract/Application/TransactionReadOnlyExecutionService.cs index 350b0d586c..52ca2a1261 100644 --- a/src/AElf.Kernel.SmartContract/Application/TransactionReadOnlyExecutionService.cs +++ b/src/AElf.Kernel.SmartContract/Application/TransactionReadOnlyExecutionService.cs @@ -4,25 +4,39 @@ using AElf.Types; using Google.Protobuf.Reflection; using Google.Protobuf.WellKnownTypes; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace AElf.Kernel.SmartContract.Application; public class TransactionReadOnlyExecutionService : ITransactionReadOnlyExecutionService { private readonly ISmartContractExecutiveService _smartContractExecutiveService; + private readonly ISyntheticTransactionExecutionProvider _syntheticTransactionExecutionProvider; private readonly ITransactionContextFactory _transactionContextFactory; public TransactionReadOnlyExecutionService(ISmartContractExecutiveService smartContractExecutiveService, - ITransactionContextFactory transactionContextFactory) + ITransactionContextFactory transactionContextFactory, + ISyntheticTransactionExecutionProvider syntheticTransactionExecutionProvider) { _smartContractExecutiveService = smartContractExecutiveService; _transactionContextFactory = transactionContextFactory; + _syntheticTransactionExecutionProvider = syntheticTransactionExecutionProvider; + Logger = NullLogger.Instance; } + public ILogger Logger { get; set; } + public async Task ExecuteAsync(IChainContext chainContext, Transaction transaction, Timestamp currentBlockTime) { var transactionContext = _transactionContextFactory.Create(transaction, chainContext, currentBlockTime); + if (_syntheticTransactionExecutionProvider.TryApply(transaction, transactionContext.Trace)) + { + Logger.LogDebug("Read-only transaction {TransactionId} to contract {ContractAddress} was synthetically executed.", + transactionContext.Trace.TransactionId, transaction.To); + return transactionContext.Trace; + } var executive = await _smartContractExecutiveService.GetExecutiveAsync( chainContext, transaction.To); @@ -109,4 +123,4 @@ public async Task IsViewTransactionAsync(IChainContext chainContext, Trans await _smartContractExecutiveService.PutExecutiveAsync(chainContext, address, executive); } } -} \ No newline at end of file +} diff --git a/src/AElf.Kernel.SmartContract/SmartContractAElfModule.cs b/src/AElf.Kernel.SmartContract/SmartContractAElfModule.cs index 670ee60cdb..7986e84f47 100644 --- a/src/AElf.Kernel.SmartContract/SmartContractAElfModule.cs +++ b/src/AElf.Kernel.SmartContract/SmartContractAElfModule.cs @@ -14,9 +14,10 @@ public class SmartContractAElfModule : AElfModule public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddSingleton(); + context.Services.AddSingleton(); context.Services.AddSingleton(); context.Services.AddSingleton(); context.Services.AddTransient(typeof(IContractReaderFactory<>), typeof(ContractReaderFactory<>)); context.Services.AddSingleton(typeof(ILogEventProcessingService<>), typeof(LogEventProcessingService<>)); } -} \ No newline at end of file +} diff --git a/src/AElf.Kernel.TransactionPool/TransactionPoolAElfModule.cs b/src/AElf.Kernel.TransactionPool/TransactionPoolAElfModule.cs index 4685f02603..cb39a48945 100644 --- a/src/AElf.Kernel.TransactionPool/TransactionPoolAElfModule.cs +++ b/src/AElf.Kernel.TransactionPool/TransactionPoolAElfModule.cs @@ -18,8 +18,9 @@ public override void ConfigureServices(ServiceConfigurationContext context) services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); var configuration = context.Services.GetConfiguration(); Configure(configuration.GetSection("Transaction")); } -} \ No newline at end of file +} 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.Contracts.EconomicSystem.Tests/BVT/AdvanceResourceTokenTests.cs b/test/AElf.Contracts.EconomicSystem.Tests/BVT/AdvanceResourceTokenTests.cs index aefdd9b864..23e18991c3 100644 --- a/test/AElf.Contracts.EconomicSystem.Tests/BVT/AdvanceResourceTokenTests.cs +++ b/test/AElf.Contracts.EconomicSystem.Tests/BVT/AdvanceResourceTokenTests.cs @@ -66,6 +66,58 @@ await TokenContractStub.AdvanceResourceToken.SendAsync(new AdvanceResourceTokenI return contractAddress; } + [Fact] + public async Task TokenContract_AdvanceResourceToken_Negative_Amount_Test() + { + var contractAddress = await TokenContract_AdvanceResourceToken_Test(); + + await TokenContractStub.Transfer.SendAsync(new TransferInput + { + Symbol = "ELF", + Amount = 100000000, + To = OtherAddress + }); + + // Check balance of other address. + { + var balance = await OtherTokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = OtherAddress, + Symbol = ResourceTokenSymbol + }); + balance.Balance.ShouldBe(0); + } + + var executionResult = await OtherTokenContractStub.AdvanceResourceToken.SendWithExceptionAsync(new AdvanceResourceTokenInput + { + ContractAddress = contractAddress, + Amount = -Amount, + ResourceTokenSymbol = ResourceTokenSymbol + }); + + executionResult.TransactionResult.Status.ShouldBe(TransactionResultStatus.Failed); + executionResult.TransactionResult.Error.ShouldContain("Invalid amount"); + // Check balance of contract address. + { + var balance = await OtherTokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = contractAddress, + Symbol = ResourceTokenSymbol + }); + balance.Balance.ShouldBe(Amount); + } + + // Check balance of developer. + { + var balance = await OtherTokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = OtherAddress, + Symbol = ResourceTokenSymbol + }); + balance.Balance.ShouldBe(0); + } + } + [Fact] public async Task TokenContract_TakeResourceTokenBack_Test() { @@ -163,6 +215,22 @@ public async Task TokenContract_TakeResourceTokenBack_Exceed_Test() result.TransactionResult.Status.ShouldBe(TransactionResultStatus.Failed); result.TransactionResult.Error.ShouldContain("Can't take back that more."); } + + [Fact] + public async Task TokenContract_TakeResourceTokenBack_Negative_Amount_Test() + { + var contractAddress = await TokenContract_AdvanceResourceToken_Test(); + + var result = await TokenContractStub.TakeResourceTokenBack.SendWithExceptionAsync(new TakeResourceTokenBackInput + { + ContractAddress = contractAddress, + Amount = -Amount, + ResourceTokenSymbol = ResourceTokenSymbol + }); + + result.TransactionResult.Status.ShouldBe(TransactionResultStatus.Failed); + result.TransactionResult.Error.ShouldContain("Invalid amount"); + } [Fact] public async Task SetControllerForManageConnector_Test() diff --git a/test/AElf.Contracts.EconomicSystem.Tests/EconomicSystemTestBase.cs b/test/AElf.Contracts.EconomicSystem.Tests/EconomicSystemTestBase.cs index 3947cb09dc..eab73fa369 100644 --- a/test/AElf.Contracts.EconomicSystem.Tests/EconomicSystemTestBase.cs +++ b/test/AElf.Contracts.EconomicSystem.Tests/EconomicSystemTestBase.cs @@ -14,6 +14,7 @@ using AElf.Contracts.Treasury; using AElf.Contracts.Vote; using AElf.Cryptography.ECDSA; +using AElf.Types; using Volo.Abp.Threading; namespace AElf.Contracts.EconomicSystem.Tests; @@ -30,6 +31,10 @@ public class EconomicSystemTestBase : EconomicContractsTestBase internal TokenContractImplContainer.TokenContractImplStub TokenContractImplStub => GetTokenContractImplTester(BootMinerKeyPair); + protected Address OtherAddress => Address.FromPublicKey(Accounts[1].KeyPair.PublicKey); + internal TokenContractImplContainer.TokenContractImplStub OtherTokenContractStub => + GetTokenContractTester(Accounts[1].KeyPair); + internal TokenHolderContractImplContainer.TokenHolderContractImplStub TokenHolderStub => GetTokenHolderTester(BootMinerKeyPair); diff --git a/test/AElf.Contracts.Election.Tests/BVT/ElectionTests.cs b/test/AElf.Contracts.Election.Tests/BVT/ElectionTests.cs index 2ba363f220..9e4e6f8128 100644 --- a/test/AElf.Contracts.Election.Tests/BVT/ElectionTests.cs +++ b/test/AElf.Contracts.Election.Tests/BVT/ElectionTests.cs @@ -1241,6 +1241,80 @@ await ElectionContractStub.GetElectorVoteWithRecords.CallAsync( } } + [Fact] + public async Task ElectionContract_Withdraw_Succeeds_When_Delegated_VoteId_Collision_Is_Rejected_Test() + { + const int amount = 100; + const int lockTime = 7 * 60 * 60 * 24; + + var candidateKeyPair = ValidationDataCenterKeyPairs[0]; + await AnnounceElectionAsync(candidateKeyPair); + var candidatePubkey = candidateKeyPair.PublicKey.ToHex(); + + var voterKeyPair = VoterKeyPairs[0]; + var otherVoterKeyPair = VoterKeyPairs[1]; + var voterAddress = Address.FromPublicKey(voterKeyPair.PublicKey); + + var voteResult = await VoteToCandidateAsync(voterKeyPair, candidatePubkey, lockTime, amount); + voteResult.Status.ShouldBe(TransactionResultStatus.Mined); + var voteId = Hash.Parser.ParseFrom(voteResult.ReturnValue); + + var originalRecord = await VoteContractStub.GetVotingRecord.CallAsync(voteId); + originalRecord.VotingItemId.ShouldBe(MinerElectionVotingItemId); + originalRecord.Voter.ShouldBe(voterAddress); + originalRecord.Option.ShouldBe(candidatePubkey); + + var otherVoteStub = GetVoteContractTester(otherVoterKeyPair); + var otherRegisterTime = TimestampHelper.GetUtcNow(); + var otherRegisterInput = new VotingRegisterInput + { + AcceptedCurrency = ElectionContractTestConstants.NativeTokenSymbol, + IsLockToken = false, + StartTimestamp = otherRegisterTime, + EndTimestamp = otherRegisterTime.AddDays(1), + TotalSnapshotNumber = 1, + Options = { candidatePubkey } + }; + var registerResult = await otherVoteStub.Register.SendAsync(otherRegisterInput); + registerResult.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + + var otherRegisterInputForHash = otherRegisterInput.Clone(); + otherRegisterInputForHash.Options.Clear(); + var otherVotingItemId = HashHelper.ConcatAndCompute(HashHelper.ComputeFrom(otherRegisterInputForHash), + HashHelper.ComputeFrom(Address.FromPublicKey(otherVoterKeyPair.PublicKey))); + + var collisionResult = await otherVoteStub.Vote.SendWithExceptionAsync(new VoteInput + { + VotingItemId = otherVotingItemId, + VoteId = voteId, + Voter = voterAddress, + Option = candidatePubkey, + Amount = 1 + }); + collisionResult.TransactionResult.Status.ShouldBe(TransactionResultStatus.Failed); + collisionResult.TransactionResult.Error.ShouldContain("VoteId already exists."); + + var recordAfterRejectedCollision = await VoteContractStub.GetVotingRecord.CallAsync(voteId); + recordAfterRejectedCollision.VotingItemId.ShouldBe(MinerElectionVotingItemId); + recordAfterRejectedCollision.Voter.ShouldBe(voterAddress); + recordAfterRejectedCollision.Amount.ShouldBe(amount); + recordAfterRejectedCollision.Option.ShouldBe(candidatePubkey); + recordAfterRejectedCollision.IsWithdrawn.ShouldBeFalse(); + + BlockTimeProvider.SetBlockTime(recordAfterRejectedCollision.VoteTimestamp.AddSeconds(lockTime + 1000)); + + var withdrawResult = await WithdrawVotes(voterKeyPair, voteId); + withdrawResult.Error.ShouldBeEmpty(); + withdrawResult.Status.ShouldBe(TransactionResultStatus.Mined); + + var electorVote = await ElectionContractStub.GetElectorVoteWithAllRecords.CallAsync(new StringValue + { + Value = voterKeyPair.PublicKey.ToHex() + }); + electorVote.ActiveVotingRecords.Select(record => record.VoteId).ShouldNotContain(voteId); + electorVote.WithdrawnVotesRecords.Select(record => record.VoteId).ShouldContain(voteId); + } + [Fact] public async Task ElectionContract_GetCandidates_Test() { diff --git a/test/AElf.Contracts.Profit.Tests/ProfitTests.cs b/test/AElf.Contracts.Profit.Tests/ProfitTests.cs index 601fd7ccf6..bd776871fa 100644 --- a/test/AElf.Contracts.Profit.Tests/ProfitTests.cs +++ b/test/AElf.Contracts.Profit.Tests/ProfitTests.cs @@ -2,6 +2,8 @@ using System.Threading.Tasks; using AElf.Contracts.MultiToken; using AElf.CSharp.Core; +using AElf.CSharp.Core.Extension; +using AElf.Kernel; using AElf.Types; using Google.Protobuf.WellKnownTypes; using Shouldly; @@ -1775,6 +1777,131 @@ public async Task MaximumProfitReceivingPeriodCount_Test() maximumProfitReceivingPeriodCount.Value.ShouldBe(maxPeriodCount); } + [Fact] + public async Task ClaimProfits_OtherSymbol_Should_Not_Regress_LastProfitPeriod_Test() + { + const long shares = 1; + const long elfAmountPerPeriod = 100; + const long otherSymbolAmount = 1; + const string otherSymbol = "CPU"; + + var creator = Creators[0]; + var beneficiary1 = Normal[0]; + var beneficiary1TokenStub = GetTokenContractTester(NormalKeyPair[0]); + var beneficiaryAddress1 = Address.FromPublicKey(NormalKeyPair[0].PublicKey); + var beneficiaryAddress2 = Address.FromPublicKey(NormalKeyPair[1].PublicKey); + + var initialElfBalance = (await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = beneficiaryAddress1, + Symbol = ProfitContractTestConstants.NativeTokenSymbol + })).Balance; + + var schemeId = await CreateSchemeAsync(); + + await creator.AddBeneficiary.SendAsync(new AddBeneficiaryInput + { + SchemeId = schemeId, + BeneficiaryShare = new BeneficiaryShare + { + Beneficiary = beneficiaryAddress1, + Shares = shares + } + }); + + await creator.AddBeneficiary.SendAsync(new AddBeneficiaryInput + { + SchemeId = schemeId, + BeneficiaryShare = new BeneficiaryShare + { + Beneficiary = beneficiaryAddress2, + Shares = shares + } + }); + + // Make ELF appear first in ReceivedTokenSymbols, then append a short-lived symbol. + await ContributeProfits(schemeId, elfAmountPerPeriod * 2); + await CreateTestTokenAsync(otherSymbol, beneficiaryAddress1, otherSymbolAmount); + await beneficiary1TokenStub.Approve.SendAsync(new ApproveInput + { + Spender = ProfitContractAddress, + Symbol = otherSymbol, + Amount = otherSymbolAmount + }); + await beneficiary1.ContributeProfits.SendAsync(new ContributeProfitsInput + { + SchemeId = schemeId, + Symbol = otherSymbol, + Amount = otherSymbolAmount, + Period = 1 + }); + + var scheme = await creator.GetScheme.CallAsync(schemeId); + scheme.ReceivedTokenSymbols.Count.ShouldBe(2); + scheme.ReceivedTokenSymbols[0].ShouldBe(ProfitContractTestConstants.NativeTokenSymbol); + scheme.ReceivedTokenSymbols[1].ShouldBe(otherSymbol); + + await creator.DistributeProfits.SendAsync(new DistributeProfitsInput + { + SchemeId = schemeId, + Period = 1, + AmountsMap = + { + { ProfitContractTestConstants.NativeTokenSymbol, elfAmountPerPeriod } + } + }); + + await creator.DistributeProfits.SendAsync(new DistributeProfitsInput + { + SchemeId = schemeId, + Period = 2, + AmountsMap = + { + { ProfitContractTestConstants.NativeTokenSymbol, elfAmountPerPeriod } + } + }); + + await beneficiary1.ClaimProfits.SendAsync(new ClaimProfitsInput + { + SchemeId = schemeId + }); + + var detailsAfterFirstClaim = await creator.GetProfitDetails.CallAsync(new GetProfitDetailsInput + { + SchemeId = schemeId, + Beneficiary = beneficiaryAddress1 + }); + detailsAfterFirstClaim.Details.Count.ShouldBe(1); + detailsAfterFirstClaim.Details.First().LastProfitPeriod.ShouldBe(3); + + var balanceAfterFirstClaim = (await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = beneficiaryAddress1, + Symbol = ProfitContractTestConstants.NativeTokenSymbol + })).Balance; + balanceAfterFirstClaim.ShouldBe(initialElfBalance + elfAmountPerPeriod); + + await beneficiary1.ClaimProfits.SendAsync(new ClaimProfitsInput + { + SchemeId = schemeId + }); + + var balanceAfterSecondClaim = (await TokenContractStub.GetBalance.CallAsync(new GetBalanceInput + { + Owner = beneficiaryAddress1, + Symbol = ProfitContractTestConstants.NativeTokenSymbol + })).Balance; + balanceAfterSecondClaim.ShouldBe(initialElfBalance + elfAmountPerPeriod); + + var detailsAfterSecondClaim = await creator.GetProfitDetails.CallAsync(new GetProfitDetailsInput + { + SchemeId = schemeId, + Beneficiary = beneficiaryAddress1 + }); + detailsAfterSecondClaim.Details.Count.ShouldBe(1); + detailsAfterSecondClaim.Details.First().LastProfitPeriod.ShouldBe(3); + } + private async Task ContributeProfits(Hash schemeId, long amount = 100) { await ProfitContractStub.ContributeProfits.SendAsync(new ContributeProfitsInput @@ -1784,4 +1911,79 @@ await ProfitContractStub.ContributeProfits.SendAsync(new ContributeProfitsInput Amount = amount }); } -} \ No newline at end of file + + private async Task CreateTestTokenAsync(string symbol, Address to, long amount) + { + const string seedCollectionSymbol = "SEED-0"; + const string seedSymbol = "SEED-1"; + + var seedCollectionInfo = await TokenContractStub.GetTokenInfo.CallAsync(new GetTokenInfoInput + { + Symbol = seedCollectionSymbol + }); + if (string.IsNullOrEmpty(seedCollectionInfo.Symbol)) + { + await TokenContractStub.Create.SendAsync(new CreateInput + { + Symbol = seedCollectionSymbol, + Decimals = 0, + IsBurnable = true, + TokenName = "seed Collection", + TotalSupply = 1, + Issuer = Starter, + Owner = Starter, + ExternalInfo = new ExternalInfo() + }); + } + + await TokenContractStub.Create.SendAsync(new CreateInput + { + Symbol = seedSymbol, + Decimals = 0, + IsBurnable = true, + TokenName = "seed token 1", + TotalSupply = 1, + Issuer = Starter, + Owner = Starter, + ExternalInfo = new ExternalInfo + { + Value = + { + { "__seed_owned_symbol", symbol }, + { + "__seed_exp_time", + TimestampHelper.GetUtcNow().AddHours(1).Seconds.ToString() + } + } + }, + LockWhiteList = { TokenContractAddress } + }); + + await TokenContractStub.Issue.SendAsync(new IssueInput + { + Symbol = seedSymbol, + Amount = 1, + Memo = "Issue seed NFT.", + To = Starter + }); + + await TokenContractStub.Create.SendAsync(new CreateInput + { + Symbol = symbol, + Decimals = 2, + IsBurnable = true, + TokenName = $"{symbol} token", + TotalSupply = amount, + Issuer = Starter, + Owner = Starter + }); + + await TokenContractStub.Issue.SendAsync(new IssueInput + { + Symbol = symbol, + Amount = amount, + Memo = $"Issue {symbol}.", + To = to + }); + } +} diff --git a/test/AElf.Contracts.Vote.Tests/BVT/BasicTests.cs b/test/AElf.Contracts.Vote.Tests/BVT/BasicTests.cs index c7ca327187..8238dd54f5 100644 --- a/test/AElf.Contracts.Vote.Tests/BVT/BasicTests.cs +++ b/test/AElf.Contracts.Vote.Tests/BVT/BasicTests.cs @@ -146,6 +146,38 @@ public async Task Vote_Sender_Test() voteRet.TransactionResult.Error.ShouldContain("Sender of delegated voting event must be the Sponsor."); } + [Fact] + public async Task Vote_With_Negative_Amount_Should_Fail_Test() + { + var votingItem = await RegisterVotingItemAsync(10, 4, false, DefaultSender, 10); + var voter = Accounts[11].Address; + var voteId = HashHelper.ComputeFrom("negative-delegated-vote"); + + var voteRet = await VoteContractStub.Vote.SendWithExceptionAsync(new VoteInput + { + VotingItemId = votingItem.VotingItemId, + Voter = voter, + VoteId = voteId, + Option = votingItem.Options[1], + Amount = -100 + }); + + voteRet.TransactionResult.Status.ShouldBe(TransactionResultStatus.Failed); + voteRet.TransactionResult.Error.ShouldContain("Invalid amount."); + + var votingResult = await VoteContractStub.GetVotingResult.CallAsync(new GetVotingResultInput + { + VotingItemId = votingItem.VotingItemId, + SnapshotNumber = 1 + }); + votingResult.Results.Count.ShouldBe(0); + votingResult.VotersCount.ShouldBe(0); + votingResult.VotesAmount.ShouldBe(0); + + var votedItems = await VoteContractStub.GetVotedItems.CallAsync(voter); + votedItems.VotedItemVoteIds.Count.ShouldBe(0); + } + [Fact] public async Task Vote_Success() { @@ -280,6 +312,74 @@ public async Task VoteContract_Withdraw_Success_Test() beforeBalance.ShouldBe(afterBalance - 100); } + [Fact] + public async Task VoteContract_Delegated_Withdraw_Cannot_Be_Executed_Twice_Test() + { + var registerItem = await RegisterVotingItemAsync(100, 3, false, DefaultSender, 1); + var option = registerItem.Options[1]; + const long voteAmount = 100; + + var firstVoteId = HashHelper.ComputeFrom("delegated-vote-1"); + var secondVoteId = HashHelper.ComputeFrom("delegated-vote-2"); + var firstVoterAddress = Accounts[1].Address; + var secondVoterAddress = Accounts[2].Address; + + var result = await VoteContractStub.Vote.SendAsync(new VoteInput + { + VotingItemId = registerItem.VotingItemId, + Voter = firstVoterAddress, + VoteId = firstVoteId, + Option = option, + Amount = voteAmount + }); + await VoteContractStub.Vote.SendAsync(new VoteInput + { + VotingItemId = registerItem.VotingItemId, + Voter = secondVoterAddress, + VoteId = secondVoteId, + Option = option, + Amount = voteAmount + }); + + var votingResultBeforeWithdraw = await GetVotingResult(registerItem.VotingItemId, 1); + votingResultBeforeWithdraw.VotesAmount.ShouldBe(voteAmount * 2); + votingResultBeforeWithdraw.Results[option].ShouldBe(voteAmount * 2); + votingResultBeforeWithdraw.VotersCount.ShouldBe(2); + + var firstWithdrawResult = await Withdraw(DefaultSenderKeyPair, firstVoteId); + firstWithdrawResult.Status.ShouldBe(TransactionResultStatus.Mined); + + var votingResultAfterFirstWithdraw = await GetVotingResult(registerItem.VotingItemId, 1); + votingResultAfterFirstWithdraw.VotesAmount.ShouldBe(voteAmount); + votingResultAfterFirstWithdraw.Results[option].ShouldBe(voteAmount); + votingResultAfterFirstWithdraw.VotersCount.ShouldBe(1); + + var voteRecordAfterFirstWithdraw = await GetVotingRecord(firstVoteId); + voteRecordAfterFirstWithdraw.IsWithdrawn.ShouldBeTrue(); + voteRecordAfterFirstWithdraw.WithdrawTimestamp.ShouldNotBeNull(); + + var secondWithdrawResult = await WithdrawWithException(DefaultSenderKeyPair, firstVoteId); + secondWithdrawResult.Status.ShouldBe(TransactionResultStatus.Failed); + secondWithdrawResult.Error.ShouldContain("Vote already withdrawn."); + + var votingRecordAfterSecondWithdraw = await GetVotingRecord(firstVoteId); + votingRecordAfterSecondWithdraw.IsWithdrawn.ShouldBeTrue(); + votingRecordAfterSecondWithdraw.WithdrawTimestamp.ShouldBe(voteRecordAfterFirstWithdraw.WithdrawTimestamp); + + var firstVoterVoteIds = await GetVoteIds(Accounts[1].KeyPair, registerItem.VotingItemId); + firstVoterVoteIds.ActiveVotes.Count.ShouldBe(0); + firstVoterVoteIds.WithdrawnVotes.Count(id => id == firstVoteId).ShouldBe(1); + + var secondVoterVoteIds = await GetVoteIds(Accounts[2].KeyPair, registerItem.VotingItemId); + secondVoterVoteIds.ActiveVotes.Count.ShouldBe(1); + secondVoterVoteIds.ActiveVotes.First().ShouldBe(secondVoteId); + + var votingResultAfterSecondWithdraw = await GetVotingResult(registerItem.VotingItemId, 1); + votingResultAfterSecondWithdraw.VotesAmount.ShouldBe(voteAmount); + votingResultAfterSecondWithdraw.Results[option].ShouldBe(voteAmount); + votingResultAfterSecondWithdraw.VotersCount.ShouldBe(1); + } + [Fact] public async Task VoteContract_AddOption_Fail_Test() { 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.Kernel.SmartContract.ExecutionPluginForMethodFee.Tests/Service/PlainTransactionExecutingAsPluginService.cs b/test/AElf.Kernel.SmartContract.ExecutionPluginForMethodFee.Tests/Service/PlainTransactionExecutingAsPluginService.cs index f43cef0e42..d4c2161d4e 100644 --- a/test/AElf.Kernel.SmartContract.ExecutionPluginForMethodFee.Tests/Service/PlainTransactionExecutingAsPluginService.cs +++ b/test/AElf.Kernel.SmartContract.ExecutionPluginForMethodFee.Tests/Service/PlainTransactionExecutingAsPluginService.cs @@ -17,14 +17,18 @@ public class PlainTransactionExecutingAsPluginService : PlainTransactionExecutin // for sending transaction private readonly Hash _pluginOriginId = new(); private readonly ISmartContractExecutiveService _smartContractExecutiveService; + private readonly ISyntheticTransactionExecutionProvider _syntheticTransactionExecutionProvider; public PlainTransactionExecutingAsPluginService (ISmartContractExecutiveService smartContractExecutiveService, IEnumerable postPlugins, IEnumerable prePlugins, - ITransactionContextFactory transactionContextFactory, IFeatureDisableService featureDisableService) : base( - smartContractExecutiveService, postPlugins, prePlugins, transactionContextFactory, featureDisableService) + ITransactionContextFactory transactionContextFactory, IFeatureDisableService featureDisableService, + ISyntheticTransactionExecutionProvider syntheticTransactionExecutionProvider) : base( + smartContractExecutiveService, postPlugins, prePlugins, transactionContextFactory, featureDisableService, + syntheticTransactionExecutionProvider) { _smartContractExecutiveService = smartContractExecutiveService; + _syntheticTransactionExecutionProvider = syntheticTransactionExecutionProvider; } protected override async Task ExecuteOneAsync( @@ -58,7 +62,11 @@ protected override async Task ExecuteOneAsync( try { - await executive.ApplyAsync(txContext); + if (_syntheticTransactionExecutionProvider.TryApply(singleTxExecutingDto.Transaction, trace)) + Logger.LogDebug("Transaction {TransactionId} to contract {ContractAddress} was synthetically mined.", + trace.TransactionId, singleTxExecutingDto.Transaction.To); + else + await executive.ApplyAsync(txContext); if (txContext.Trace.IsSuccessful()) await ExecuteInlineTransactions(singleTxExecutingDto.Depth, singleTxExecutingDto.CurrentBlockTime, @@ -115,4 +123,4 @@ private async Task ExecuteInlineTransactions(int depth, Timestamp currentBlockTi internalStateCache.Update(inlineTrace.GetStateSets()); } } -} \ No newline at end of file +} diff --git a/test/AElf.Kernel.SmartContract.Parallel.Tests/InternalConstants.cs b/test/AElf.Kernel.SmartContract.Parallel.Tests/InternalConstants.cs index f4a8a74323..2657896cfe 100644 --- a/test/AElf.Kernel.SmartContract.Parallel.Tests/InternalConstants.cs +++ b/test/AElf.Kernel.SmartContract.Parallel.Tests/InternalConstants.cs @@ -5,4 +5,5 @@ internal class InternalConstants public const string NonAcs2 = "2vNDCj1WjNLAXm3VnEeGGRMw3Aab4amVSEaYmCyxQKjNhLhfL7"; public const string Acs2 = "ZJjdajAmP5HpWgvLkXa5mm6gcuGWwKjN3Kos89ZJogHYDgTsB"; public const string NonParallel = "Lib8JSzdsFC7uCwvEwviadh3kp9LzaLMCauK4fSzrwc2qtHVi"; -} \ No newline at end of file + public const string Bypassed = "tHjyUJyDGoipsHXDV4WsV7KT8mwqZus4CxTb2Vb2G7VePef7g"; +} diff --git a/test/AElf.Kernel.SmartContract.Parallel.Tests/ResourceExtractionServiceTest.cs b/test/AElf.Kernel.SmartContract.Parallel.Tests/ResourceExtractionServiceTest.cs index 56eb669ddb..cb7a27d5c5 100644 --- a/test/AElf.Kernel.SmartContract.Parallel.Tests/ResourceExtractionServiceTest.cs +++ b/test/AElf.Kernel.SmartContract.Parallel.Tests/ResourceExtractionServiceTest.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Linq; using System.Threading; @@ -12,6 +13,7 @@ using AElf.Types; using Google.Protobuf; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Moq; using Shouldly; using Volo.Abp.Testing; @@ -155,6 +157,42 @@ public async Task GetResourcesAsync_Acs2_NonParallelizable_Test() }); } + [Fact] + public async Task GetResourcesAsync_BypassedContract_Should_Not_Apply_Contract() + { + var service = (ResourceExtractionService)Service; + var logger = new Mock>(); + service.Logger = logger.Object; + var txn = new Transaction + { + From = Address.FromBase58(InternalConstants.Acs2), + To = Address.FromBase58(InternalConstants.Bypassed), + MethodName = "Get", + Params = ByteString.Empty + }; + + var resourceInfos = + (await service.GetResourcesAsync(new Mock().Object, new[] { txn }, CancellationToken.None)) + .ToList(); + + resourceInfos.Count.ShouldBe(1); + resourceInfos.First().TransactionResourceInfo.ShouldBe(new TransactionResourceInfo + { + TransactionId = txn.GetHash(), + ParallelType = ParallelType.NonParallelizable, + ContractHash = Hash.Empty + }); + CountSyntheticResourceExtractionLogs(logger).ShouldBe(1); + } + + private static int CountSyntheticResourceExtractionLogs(Mock> logger) + { + return logger.Invocations.Count(invocation => + invocation.Method.Name == nameof(ILogger.Log) && + (LogLevel)invocation.Arguments[0] == LogLevel.Debug && + invocation.Arguments[2].ToString().Contains("synthetically marked non-parallelizable")); + } + [Fact] public async Task GetResourcesAsync_WithNonParallelCode_Test() { @@ -300,4 +338,4 @@ private ScopedStatePath GetPath(int value) } }; } -} \ No newline at end of file +} diff --git a/test/AElf.Kernel.SmartContract.Parallel.Tests/SmartContractParallelTestModule.cs b/test/AElf.Kernel.SmartContract.Parallel.Tests/SmartContractParallelTestModule.cs index d8345def17..206fa8467d 100644 --- a/test/AElf.Kernel.SmartContract.Parallel.Tests/SmartContractParallelTestModule.cs +++ b/test/AElf.Kernel.SmartContract.Parallel.Tests/SmartContractParallelTestModule.cs @@ -9,10 +9,12 @@ using AElf.Modularity; using AElf.OS; using AElf.Runtime.CSharp; +using AElf.Standards.ACS2; using AElf.Types; using Google.Protobuf; using Microsoft.Extensions.DependencyInjection; using Moq; +using Shouldly; using Volo.Abp.Modularity; using Volo.Abp.Threading; using CleanBlockExecutedDataChangeHeightEventHandler = @@ -27,10 +29,12 @@ public override void ConfigureServices(ServiceConfigurationContext context) { var services = context.Services; services.AddSingleton(); + services.AddSingleton(); services.AddTransient(); var executiveService = GetSmartContractExecutiveService( (InternalConstants.NonAcs2, GetNonAcs2Executive()), - (InternalConstants.Acs2, GetAcs2Executive()), (InternalConstants.NonParallel, GetNonParallelExecutive()) + (InternalConstants.Acs2, GetAcs2Executive()), (InternalConstants.NonParallel, GetNonParallelExecutive()), + (InternalConstants.Bypassed, GetBypassedExecutive()) ); services.AddSingleton(executiveService); context.Services.AddSingleton( @@ -100,6 +104,16 @@ private static IExecutive GetAcs2Executive() #endregion + private static IExecutive GetBypassedExecutive() + { + var executive = new Mock(); + executive.SetupGet(e => e.Descriptors).Returns(new[] { Acs2Reflection.Descriptor.Services[0] }); + executive.SetupGet(e => e.ContractHash).Returns(Hash.Empty); + executive.Setup(e => e.ApplyAsync(It.IsAny())) + .ThrowsAsync(new ShouldAssertException("Bypassed resource extraction should not call ApplyAsync.")); + return executive.Object; + } + private static IExecutive GetNonParallelExecutive() { var testContractFile = typeof(TestContract).Assembly @@ -146,4 +160,4 @@ public override void ConfigureServices(ServiceConfigurationContext context) .AddSingleton(); context.Services.AddSingleton(); } -} \ No newline at end of file +} diff --git a/test/AElf.Kernel.SmartContract.Tests/Application/PlainTransactionExecutingServiceSyntheticExecutionTests.cs b/test/AElf.Kernel.SmartContract.Tests/Application/PlainTransactionExecutingServiceSyntheticExecutionTests.cs new file mode 100644 index 0000000000..0a73592c69 --- /dev/null +++ b/test/AElf.Kernel.SmartContract.Tests/Application/PlainTransactionExecutingServiceSyntheticExecutionTests.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AElf.Kernel.FeatureDisable.Core; +using AElf.Kernel.SmartContract.Infrastructure; +using AElf.Types; +using Google.Protobuf; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Shouldly; +using Xunit; +using ContractServiceDescriptor = Google.Protobuf.Reflection.ServiceDescriptor; + +namespace AElf.Kernel.SmartContract.Application; + +public sealed class PlainTransactionExecutingServiceSyntheticExecutionTests +{ + private static readonly Address BypassedContractAddress = + Address.FromBase58("tHjyUJyDGoipsHXDV4WsV7KT8mwqZus4CxTb2Vb2G7VePef7g"); + + [Fact] + public async Task ExecuteAsync_BypassedContract_Should_Mine_Without_Applying_Contract() + { + var executive = CreateExecutive(); + executive.Setup(e => e.ApplyAsync(It.IsAny())) + .ThrowsAsync(new ShouldAssertException("Bypassed contract execution should not call ApplyAsync.")); + + var service = CreateService(executive.Object); + var logger = new Mock>(); + service.Logger = logger.Object; + var returnSets = await service.ExecuteAsync(new TransactionExecutingDto + { + BlockHeader = CreateBlockHeader(), + Transactions = new[] + { + CreateTransaction(BypassedContractAddress) + } + }, CancellationToken.None); + + var returnSet = returnSets.Single(); + returnSet.Status.ShouldBe(TransactionResultStatus.Mined); + returnSet.TransactionResult.Status.ShouldBe(TransactionResultStatus.Mined); + returnSet.StateChanges.ShouldBeEmpty(); + returnSet.StateDeletes.ShouldBeEmpty(); + returnSet.Bloom.ShouldBe(ByteString.Empty); + executive.Verify(e => e.ApplyAsync(It.IsAny()), Times.Never); + CountSyntheticExecutionLogs(logger, LogLevel.Debug).ShouldBe(1); + CountSyntheticExecutionLogs(logger, LogLevel.Warning).ShouldBe(0); + } + + [Fact] + public async Task ExecuteAsync_OtherContract_Should_Apply_Normally() + { + var executive = CreateExecutive(); + executive.Setup(e => e.ApplyAsync(It.IsAny())) + .Callback(context => { context.Trace.ExecutionStatus = ExecutionStatus.Executed; }) + .Returns(Task.CompletedTask); + + var service = CreateService(executive.Object); + var returnSets = await service.ExecuteAsync(new TransactionExecutingDto + { + BlockHeader = CreateBlockHeader(), + Transactions = new[] + { + CreateTransaction(SampleAddress.AddressList[1]) + } + }, CancellationToken.None); + + returnSets.Single().Status.ShouldBe(TransactionResultStatus.Mined); + executive.Verify(e => e.ApplyAsync(It.IsAny()), Times.Once); + } + + private static PlainTransactionExecutingService CreateService(IExecutive executive) + { + var executiveService = new Mock(); + executiveService.Setup(s => s.GetExecutiveAsync(It.IsAny(), It.IsAny
())) + .ReturnsAsync(executive); + executiveService.Setup(s => s.PutExecutiveAsync(It.IsAny(), It.IsAny
(), executive)) + .Returns(Task.CompletedTask); + + var thresholdProvider = new Mock(); + thresholdProvider.Setup(p => p.GetExecutionObserverThreshold(It.IsAny())) + .Returns(new ExecutionObserverThreshold + { + ExecutionBranchThreshold = SmartContractConstants.ExecutionBranchThreshold, + ExecutionCallThreshold = SmartContractConstants.ExecutionCallThreshold + }); + + var featureDisableService = new Mock(); + featureDisableService.Setup(s => s.IsFeatureDisabledAsync(It.IsAny())) + .ReturnsAsync(false); + + return new PlainTransactionExecutingService( + executiveService.Object, + new List(), + new List(), + new TransactionContextFactory(thresholdProvider.Object), + featureDisableService.Object, + ResolveSyntheticTransactionExecutionProvider()); + } + + private static ISyntheticTransactionExecutionProvider ResolveSyntheticTransactionExecutionProvider() + { + return new ServiceCollection() + .AddSingleton() + .BuildServiceProvider() + .GetRequiredService(); + } + + private static Mock CreateExecutive() + { + var executive = new Mock(); + executive.SetupGet(e => e.Descriptors).Returns(new List()); + return executive; + } + + private static BlockHeader CreateBlockHeader() + { + return new BlockHeader + { + PreviousBlockHash = Hash.Empty, + Height = 2, + Time = TimestampHelper.GetUtcNow() + }; + } + + private static Transaction CreateTransaction(Address to) + { + return new Transaction + { + From = SampleAddress.AddressList[0], + To = to, + MethodName = "Get", + Params = ByteString.Empty + }; + } + + private static int CountSyntheticExecutionLogs(Mock> logger, + LogLevel logLevel) + { + return logger.Invocations.Count(invocation => + invocation.Method.Name == nameof(ILogger.Log) && + (LogLevel)invocation.Arguments[0] == logLevel && + invocation.Arguments[2].ToString().Contains("synthetically mined")); + } +} diff --git a/test/AElf.Kernel.SmartContract.Tests/Application/TransactionReadOnlyExecutionServiceSyntheticExecutionTests.cs b/test/AElf.Kernel.SmartContract.Tests/Application/TransactionReadOnlyExecutionServiceSyntheticExecutionTests.cs new file mode 100644 index 0000000000..48543007b3 --- /dev/null +++ b/test/AElf.Kernel.SmartContract.Tests/Application/TransactionReadOnlyExecutionServiceSyntheticExecutionTests.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AElf.Kernel.SmartContract.Infrastructure; +using AElf.Types; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using Shouldly; +using Xunit; +using ContractServiceDescriptor = Google.Protobuf.Reflection.ServiceDescriptor; + +namespace AElf.Kernel.SmartContract.Application; + +public sealed class TransactionReadOnlyExecutionServiceSyntheticExecutionTests +{ + private static readonly Address BypassedContractAddress = + Address.FromBase58("tHjyUJyDGoipsHXDV4WsV7KT8mwqZus4CxTb2Vb2G7VePef7g"); + + [Fact] + public async Task ExecuteAsync_BypassedContract_Should_Not_Apply_Contract() + { + var executive = new Mock(); + executive.SetupGet(e => e.Descriptors).Returns(new List()); + executive.Setup(e => e.ApplyAsync(It.IsAny())) + .ThrowsAsync(new ShouldAssertException("Bypassed read-only execution should not call ApplyAsync.")); + + var executiveService = new Mock(); + executiveService.Setup(s => s.GetExecutiveAsync(It.IsAny(), BypassedContractAddress)) + .ReturnsAsync(executive.Object); + executiveService.Setup(s => + s.PutExecutiveAsync(It.IsAny(), BypassedContractAddress, executive.Object)) + .Returns(Task.CompletedTask); + + var thresholdProvider = new Mock(); + thresholdProvider.Setup(p => p.GetExecutionObserverThreshold(It.IsAny())) + .Returns(new ExecutionObserverThreshold + { + ExecutionBranchThreshold = SmartContractConstants.ExecutionBranchThreshold, + ExecutionCallThreshold = SmartContractConstants.ExecutionCallThreshold + }); + + var service = new TransactionReadOnlyExecutionService( + executiveService.Object, + new TransactionContextFactory(thresholdProvider.Object), + ResolveSyntheticTransactionExecutionProvider()); + var logger = new Mock>(); + service.Logger = logger.Object; + + var trace = await service.ExecuteAsync(new ChainContext + { + BlockHash = Hash.Empty, + BlockHeight = 1 + }, new Transaction + { + From = SampleAddress.AddressList[0], + To = BypassedContractAddress, + MethodName = "Get", + Params = ByteString.Empty + }, TimestampHelper.GetUtcNow()); + + trace.ExecutionStatus.ShouldBe(ExecutionStatus.Executed); + executive.Verify(e => e.ApplyAsync(It.IsAny()), Times.Never); + CountSyntheticExecutionLogs(logger).ShouldBe(1); + } + + private static ISyntheticTransactionExecutionProvider ResolveSyntheticTransactionExecutionProvider() + { + return new ServiceCollection() + .AddSingleton() + .BuildServiceProvider() + .GetRequiredService(); + } + + private static int CountSyntheticExecutionLogs(Mock> logger) + { + return logger.Invocations.Count(invocation => + invocation.Method.Name == nameof(ILogger.Log) && + (LogLevel)invocation.Arguments[0] == LogLevel.Debug && + invocation.Arguments[2].ToString().Contains("synthetically executed")); + } +} 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