From da0de9ad38295263155243c151a2c2dc29a999de Mon Sep 17 00:00:00 2001 From: s2quake Date: Fri, 25 Oct 2024 15:54:11 +0900 Subject: [PATCH] test: Add test code --- .Lib9c.Tests/Action/ActionContext.cs | 2 + .../Action/ValidatorDelegation/GasTest.cs | 149 ++++++++++ .../GasWithTransferAssetTest.cs | 172 +++++++++++ .../ValidatorDelegation/TxAcitonTestBase.cs | 275 ++++++++++++++++++ .Lib9c.Tests/Policy/BlockPolicyTest.cs | 8 +- 5 files changed, 602 insertions(+), 4 deletions(-) create mode 100644 .Lib9c.Tests/Action/ValidatorDelegation/GasTest.cs create mode 100644 .Lib9c.Tests/Action/ValidatorDelegation/GasWithTransferAssetTest.cs create mode 100644 .Lib9c.Tests/Action/ValidatorDelegation/TxAcitonTestBase.cs diff --git a/.Lib9c.Tests/Action/ActionContext.cs b/.Lib9c.Tests/Action/ActionContext.cs index 0323a0c918..2ced533f92 100644 --- a/.Lib9c.Tests/Action/ActionContext.cs +++ b/.Lib9c.Tests/Action/ActionContext.cs @@ -46,6 +46,8 @@ public class ActionContext : IActionContext public FungibleAssetValue? MaxGasPrice { get; set; } + public long? GasLimit { get; set; } + public IReadOnlyList Txs { get => _txs ?? ImmutableList.Empty; diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/GasTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/GasTest.cs new file mode 100644 index 0000000000..800cadd53e --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/GasTest.cs @@ -0,0 +1,149 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation; + +using System; +using System.Linq; +using Libplanet.Action; +using Libplanet.Crypto; +using Nekoyume.Action; +using Xunit; + +public class GasTest : TxAcitonTestBase +{ + [Theory] + [InlineData(0, 0, 4)] + [InlineData(1, 1, 4)] + [InlineData(4, 4, 4)] + [InlineData(long.MaxValue, 4, 4)] + [InlineData(long.MaxValue, 0, 4)] + [InlineData(long.MaxValue, 1, 4)] + public void Execute(long gasLimit, long gasConsumption, long gasOwned) + { + if (gasLimit < gasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasLimit), + $"{nameof(gasLimit)} must be greater than or equal to {nameof(gasConsumption)}."); + } + + if (gasOwned < gasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasOwned), + $"{nameof(gasOwned)} must be greater than or equal to {nameof(gasConsumption)}."); + } + + // Given + var signerKey = new PrivateKey(); + var signerMead = Mead * gasOwned; + EnsureToMintAsset(signerKey, signerMead); + + // When + var expectedMead = signerMead - Mead * gasConsumption; + var gasAction = new GasAction { Consumption = gasConsumption }; + MakeTransaction( + signerKey, + new ActionBase[] { gasAction, }, + maxGasPrice: Mead * 1, + gasLimit: gasLimit); + MoveToNextBlock(throwOnError: true); + + // Then + var actualMead = GetBalance(signerKey.Address, Mead); + Assert.Equal(expectedMead, actualMead); + } + + [Theory] + [InlineData(1, 1, 0)] + [InlineData(4, 4, 0)] + [InlineData(4, 4, 1)] + [InlineData(long.MaxValue, 4, 0)] + [InlineData(long.MaxValue, 4, 1)] + [InlineData(long.MaxValue, 1, 0)] + public void Execute_InsufficientMead_Throw( + long gasLimit, long gasConsumption, long gasOwned) + { + if (gasLimit < gasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasLimit), + $"{nameof(gasLimit)} must be greater than or equal to {nameof(gasConsumption)}."); + } + + if (gasOwned >= gasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasOwned), + $"{nameof(gasOwned)} must be less than {nameof(gasConsumption)}."); + } + + // Given + var signerKey = new PrivateKey(); + var signerMead = Mead * gasOwned; + EnsureToMintAsset(signerKey, signerMead); + + // When + var expectedMead = Mead * 0; + var gasAction = new GasAction { Consumption = gasConsumption }; + MakeTransaction( + signerKey, + new ActionBase[] { gasAction, }, + maxGasPrice: Mead * 1, + gasLimit: gasLimit); + + // Then + var e = Assert.Throws(() => MoveToNextBlock(throwOnError: true)); + var innerExceptions = e.InnerExceptions + .Cast() + .ToArray(); + var actualMead = GetBalance(signerKey.Address, Mead); + Assert.Single(innerExceptions); + Assert.Contains(innerExceptions, i => i.Action is GasAction); + Assert.Equal(expectedMead, actualMead); + } + + [Theory] + [InlineData(4, 5, 5)] + [InlineData(1, 5, 10)] + public void Execute_ExcceedGasLimit_Throw( + long gasLimit, long gasConsumption, long gasOwned) + { + if (gasLimit >= gasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasLimit), + $"{nameof(gasLimit)} must be less than {nameof(gasConsumption)}."); + } + + if (gasOwned < gasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasOwned), + $"{nameof(gasOwned)} must be greater than or equal to {nameof(gasConsumption)}."); + } + + // Given + var signerKey = new PrivateKey(); + var signerMead = Mead * gasOwned; + EnsureToMintAsset(signerKey, signerMead); + + // When + var expectedMead = signerMead - (Mead * gasLimit); + var gasAction = new GasAction { Consumption = gasConsumption }; + MakeTransaction( + signerKey, + new ActionBase[] { gasAction, }, + maxGasPrice: Mead * 1, + gasLimit: gasLimit); + + // Then + var e = Assert.Throws(() => MoveToNextBlock(throwOnError: true)); + var innerExceptions = e.InnerExceptions + .Cast() + .ToArray(); + var actualMead = GetBalance(signerKey.Address, Mead); + Assert.Single(innerExceptions); + Assert.Contains(innerExceptions, i => i.Action is GasAction); + Assert.Equal(expectedMead, actualMead); + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/GasWithTransferAssetTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/GasWithTransferAssetTest.cs new file mode 100644 index 0000000000..8915d11772 --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/GasWithTransferAssetTest.cs @@ -0,0 +1,172 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation; + +using System; +using System.Linq; +using Libplanet.Action; +using Libplanet.Crypto; +using Nekoyume.Action; +using Xunit; + +public class GasWithTransferAssetTest : TxAcitonTestBase +{ + public const long GasConsumption = 4; + + [Theory] + [InlineData(4, 4)] + [InlineData(5, 4)] + [InlineData(6, 4)] + [InlineData(long.MaxValue, 4)] + [InlineData(long.MaxValue, 5)] + [InlineData(long.MaxValue, 6)] + public void Execute(long gasLimit, long gasOwned) + { + if (gasLimit < GasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasLimit), + $"{nameof(gasLimit)} must be greater than or equal to {GasConsumption}."); + } + + if (gasOwned < GasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasOwned), + $"{nameof(gasOwned)} must be greater than or equal to {GasConsumption}."); + } + + // Given + var signerKey = new PrivateKey(); + var signerMead = Mead * gasOwned; + var recipientKey = new PrivateKey(); + EnsureToMintAsset(signerKey, signerMead); + EnsureToMintAsset(signerKey, NCG * 100); + + // When + var amount = NCG * 1; + var expectedNCG = NCG * 1; + var expectedMead = signerMead - Mead * GasConsumption; + var transferAsset = new TransferAsset( + signerKey.Address, recipientKey.Address, amount, memo: "test"); + MakeTransaction( + signerKey, + new ActionBase[] { transferAsset, }, + maxGasPrice: Mead * 1, + gasLimit: gasLimit); + + // ` + MoveToNextBlock(throwOnError: true); + var actualNCG = GetBalance(recipientKey.Address, NCG); + var actualMead = GetBalance(signerKey.Address, Mead); + Assert.Equal(expectedNCG, actualNCG); + Assert.Equal(expectedMead, actualMead); + } + + [Theory] + [InlineData(4, 0)] + [InlineData(5, 0)] + [InlineData(6, 1)] + [InlineData(long.MaxValue, 0)] + [InlineData(long.MaxValue, 1)] + [InlineData(long.MaxValue, 2)] + public void Execute_InsufficientMead_Throw( + long gasLimit, long gasOwned) + { + if (gasLimit < GasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasLimit), + $"{nameof(gasLimit)} must be greater than or equal to {GasConsumption}."); + } + + if (gasOwned >= GasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasOwned), + $"{nameof(gasOwned)} must be less than {GasConsumption}."); + } + + // Given + var signerKey = new PrivateKey(); + var signerMead = Mead * gasOwned; + var recipientKey = new PrivateKey(); + EnsureToMintAsset(signerKey, signerMead); + EnsureToMintAsset(signerKey, NCG * 100); + + // When + var amount = NCG * 1; + var expectedMead = Mead * 0; + var expectedNCG = NCG * 0; + var transferAsset = new TransferAsset( + signerKey.Address, recipientKey.Address, amount, memo: "test"); + MakeTransaction( + signerKey, + new ActionBase[] { transferAsset, }, + maxGasPrice: Mead * 1, + gasLimit: gasLimit); + + // Then + var e = Assert.Throws(() => MoveToNextBlock(throwOnError: true)); + var innerExceptions = e.InnerExceptions + .Cast() + .ToArray(); + var actualNCG = GetBalance(recipientKey.Address, NCG); + var actualMead = GetBalance(signerKey.Address, Mead); + Assert.Single(innerExceptions); + Assert.Contains(innerExceptions, i => i.Action is TransferAsset); + Assert.Equal(expectedNCG, actualNCG); + Assert.Equal(expectedMead, actualMead); + } + + [Theory] + [InlineData(3, 5)] + [InlineData(1, 10)] + public void Execute_ExcceedGasLimit_Throw( + long gasLimit, long gasOwned) + { + if (gasLimit >= GasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasLimit), + $"{nameof(gasLimit)} must be less than {GasConsumption}."); + } + + if (gasOwned < GasConsumption) + { + throw new ArgumentOutOfRangeException( + nameof(gasOwned), + $"{nameof(gasOwned)} must be greater than or equal to {GasConsumption}."); + } + + // Given + var amount = NCG * 1; + var signerKey = new PrivateKey(); + var signerMead = Mead * gasOwned; + var recipientKey = new PrivateKey(); + EnsureToMintAsset(signerKey, signerMead); + EnsureToMintAsset(signerKey, NCG * 100); + + // When + var expectedMead = signerMead - (Mead * gasLimit); + var expectedNCG = NCG * 0; + var transferAsset = new TransferAsset( + signerKey.Address, recipientKey.Address, amount, memo: "test"); + MakeTransaction( + signerKey, + new ActionBase[] { transferAsset, }, + maxGasPrice: Mead * 1, + gasLimit: gasLimit); + + // Then + var e = Assert.Throws(() => MoveToNextBlock(throwOnError: true)); + var innerExceptions = e.InnerExceptions + .Cast() + .ToArray(); + var actualNCG = GetBalance(recipientKey.Address, NCG); + var actualMead = GetBalance(signerKey.Address, Mead); + Assert.Single(innerExceptions); + Assert.Contains(innerExceptions, i => i.Action is TransferAsset); + Assert.Equal(expectedNCG, actualNCG); + Assert.Equal(expectedMead, actualMead); + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/TxAcitonTestBase.cs b/.Lib9c.Tests/Action/ValidatorDelegation/TxAcitonTestBase.cs new file mode 100644 index 0000000000..04e63903b5 --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/TxAcitonTestBase.cs @@ -0,0 +1,275 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation; + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Security.Cryptography; +using System.Threading; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.Loader; +using Libplanet.Action.State; +using Libplanet.Blockchain; +using Libplanet.Blockchain.Policies; +using Libplanet.Blockchain.Renderers; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Store; +using Libplanet.Store.Trie; +using Libplanet.Types.Assets; +using Libplanet.Types.Blocks; +using Libplanet.Types.Consensus; +using Nekoyume; +using Nekoyume.Action; +using Nekoyume.Action.Loader; +using Nekoyume.Blockchain.Policy; +using Nekoyume.Model; +using Nekoyume.Model.State; + +public abstract class TxAcitonTestBase +{ + protected static readonly Currency Mead = Currencies.Mead; + protected static readonly Currency NCG = Currency.Legacy("NCG", 2, null); + private readonly PrivateKey _privateKey = new PrivateKey(); + private BlockCommit? _lastCommit; + + protected TxAcitonTestBase() + { + var validatorKey = new PrivateKey(); + + var blockPolicySource = new BlockPolicySource( + actionLoader: new GasActionLoader()); + var policy = blockPolicySource.GetPolicy( + maxTransactionsBytesPolicy: null!, + minTransactionsPerBlockPolicy: null!, + maxTransactionsPerBlockPolicy: null!, + maxTransactionsPerSignerPerBlockPolicy: null!); + var stagePolicy = new VolatileStagePolicy(); + var validator = new Validator(validatorKey.PublicKey, 10_000_000_000_000_000_000); + var genesis = MakeGenesisBlock( + new ValidatorSet(new List { validator })); + using var store = new MemoryStore(); + using var keyValueStore = new MemoryKeyValueStore(); + using var stateStore = new TrieStateStore(keyValueStore); + var actionEvaluator = new ActionEvaluator( + policy.PolicyActionsRegistry, + stateStore: stateStore, + actionTypeLoader: new GasActionLoader()); + var actionRenderer = new ActionRenderer(); + + var blockChain = BlockChain.Create( + policy, + stagePolicy, + store, + stateStore, + genesis, + actionEvaluator, + renderers: new[] { actionRenderer }); + + BlockChain = blockChain; + Renderer = actionRenderer; + ValidatorKey = validatorKey; + } + + protected BlockChain BlockChain { get; } + + protected ActionRenderer Renderer { get; } + + protected PrivateKey ValidatorKey { get; } + + protected void EnsureToMintAsset(PrivateKey privateKey, FungibleAssetValue fav) + { + var prepareRewardAssets = new PrepareRewardAssets + { + RewardPoolAddress = privateKey.Address, + Assets = new List + { + fav, + }, + }; + var actions = new ActionBase[] { prepareRewardAssets, }; + + Renderer.Reset(); + MakeTransaction(privateKey, actions); + MoveToNextBlock(); + Renderer.Wait(); + } + + protected void MoveToNextBlock(bool throwOnError = false) + { + var blockChain = BlockChain; + var lastCommit = _lastCommit; + var validatorKey = ValidatorKey; + var block = blockChain.ProposeBlock(validatorKey, lastCommit); + var worldState = blockChain.GetNextWorldState() + ?? throw new InvalidOperationException("Failed to get next world state"); + var validatorSet = worldState.GetValidatorSet(); + var blockCommit = GenerateBlockCommit( + block, validatorSet, new PrivateKey[] { validatorKey }); + + Renderer.Reset(); + blockChain.Append(block, blockCommit); + Renderer.Wait(); + if (throwOnError && Renderer.Exceptions.Any()) + { + throw new AggregateException(Renderer.Exceptions); + } + + _lastCommit = blockCommit; + } + + protected IWorldState GetNextWorldState() + { + var blockChain = BlockChain; + return blockChain.GetNextWorldState() + ?? throw new InvalidOperationException("Failed to get next world state"); + } + + protected void MakeTransaction( + PrivateKey privateKey, + IEnumerable actions, + FungibleAssetValue? maxGasPrice = null, + long? gasLimit = null, + DateTimeOffset? timestamp = null) + { + var blockChain = BlockChain; + blockChain.MakeTransaction( + privateKey, actions, maxGasPrice, gasLimit, timestamp); + } + + protected FungibleAssetValue GetBalance(Address address, Currency currency) + => GetNextWorldState().GetBalance(address, currency); + + private BlockCommit GenerateBlockCommit( + Block block, ValidatorSet validatorSet, IEnumerable validatorPrivateKeys) + { + return block.Index != 0 + ? new BlockCommit( + block.Index, + 0, + block.Hash, + validatorPrivateKeys.Select(k => new VoteMetadata( + block.Index, + 0, + block.Hash, + DateTimeOffset.UtcNow, + k.PublicKey, + validatorSet.GetValidator(k.PublicKey).Power, + VoteFlag.PreCommit).Sign(k)).ToImmutableArray()) + : throw new InvalidOperationException("Block index must be greater than 0"); + } + + private Block MakeGenesisBlock(ValidatorSet validators) + { + var nonce = new byte[] { 0x00, 0x01, 0x02, 0x03 }; + (ActivationKey _, PendingActivationState pendingActivation) = + ActivationKey.Create(_privateKey, nonce); + var pendingActivations = new PendingActivationState[] { pendingActivation }; + + var sheets = TableSheetsImporter.ImportSheets(); + return BlockHelper.ProposeGenesisBlock( + validators, + sheets, + new GoldDistribution[0], + pendingActivations); + } + + protected sealed class ActionRenderer : IActionRenderer + { + private readonly ManualResetEvent _resetEvent = new ManualResetEvent(false); + private List _exceptionList = new List(); + + public Exception[] Exceptions => _exceptionList.ToArray(); + + public void RenderAction(IValue action, ICommittedActionContext context, HashDigest nextState) + { + } + + public void RenderActionError(IValue action, ICommittedActionContext context, Exception exception) + { + _exceptionList.Add(exception); + } + + public void RenderBlock(Block oldTip, Block newTip) + { + _exceptionList.Clear(); + } + + public void RenderBlockEnd(Block oldTip, Block newTip) + { + _resetEvent.Set(); + } + + public void Reset() => _resetEvent.Reset(); + + public void Wait(int timeout) + { + if (!_resetEvent.WaitOne(timeout)) + { + throw new TimeoutException("Timeout"); + } + } + + public void Wait() => Wait(10000); + } + + [ActionType(TypeIdentifier)] + protected class GasAction : ActionBase + { + public const string TypeIdentifier = "gas_action"; + + public GasAction() + { + } + + public long Consumption { get; set; } + + public override IValue PlainValue => Dictionary.Empty + .Add("type_id", TypeIdentifier) + .Add("consumption", new Integer(Consumption)); + + public override void LoadPlainValue(IValue plainValue) + { + if (plainValue is not Dictionary root || + !root.TryGetValue((Text)"consumption", out var rawValues) || + rawValues is not Integer value) + { + throw new InvalidCastException(); + } + + Consumption = (long)value.Value; + } + + public override IWorld Execute(IActionContext context) + { + GasTracer.UseGas(Consumption); + return context.PreviousState; + } + } + + protected class GasActionLoader : IActionLoader + { + private readonly NCActionLoader _actionLoader; + + public GasActionLoader() + { + _actionLoader = new NCActionLoader(); + } + + public IAction LoadAction(long index, IValue value) + { + if (value is Dictionary pv && + pv.TryGetValue((Text)"type_id", out IValue rawTypeId) && + rawTypeId is Text typeId && typeId == GasAction.TypeIdentifier) + { + var action = new GasAction(); + action.LoadPlainValue(pv); + return action; + } + + return _actionLoader.LoadAction(index, value); + } + } +} diff --git a/.Lib9c.Tests/Policy/BlockPolicyTest.cs b/.Lib9c.Tests/Policy/BlockPolicyTest.cs index 76b165d511..1fe4fefd0d 100644 --- a/.Lib9c.Tests/Policy/BlockPolicyTest.cs +++ b/.Lib9c.Tests/Policy/BlockPolicyTest.cs @@ -98,7 +98,7 @@ public void ValidateNextBlockTx_Mead() new PrivateKey[] { adminPrivateKey })); Assert.Equal( - 1 * Currencies.Mead, + (1 + 5) * Currencies.Mead, blockChain .GetWorldState() .GetBalance(adminAddress, Currencies.Mead)); @@ -266,7 +266,7 @@ public void MustNotIncludeBlockActionAtTransaction() } [Fact] - public void EarnMiningGoldWhenSuccessMining() + public void EarnMiningMeadWhenSuccessMining() { var adminPrivateKey = new PrivateKey(); var adminAddress = adminPrivateKey.Address; @@ -355,7 +355,7 @@ public void EarnMiningGoldWhenSuccessMining() var actualBalance = blockChain .GetNextWorldState() .GetBalance(adminAddress, rewardCurrency); - var expectedBalance = mintAmount + new FungibleAssetValue(rewardCurrency, 1, 450000000000000000); + var expectedBalance = mintAmount + rewardCurrency * (5 + 5); Assert.Equal(expectedBalance, actualBalance); // After claimed, mead have to be used? @@ -376,7 +376,7 @@ public void EarnMiningGoldWhenSuccessMining() actualBalance = blockChain .GetNextWorldState() .GetBalance(adminAddress, rewardCurrency); - expectedBalance = mintAmount + new FungibleAssetValue(rewardCurrency, 20, 0); + expectedBalance = mintAmount + rewardCurrency * (5 + 5 + 5) + (rewardCurrency * 1).DivRem(2).Quotient - rewardCurrency * 1; Assert.Equal(expectedBalance, actualBalance); }