From 2da36ed70e518842e50862fa8cf001e0f76cd64f Mon Sep 17 00:00:00 2001 From: s2quake Date: Fri, 25 Oct 2024 15:52:41 +0900 Subject: [PATCH 1/6] refactor: Refactor RewardGold for mining --- Lib9c/Action/RewardGold.cs | 33 ++++++++++++--------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/Lib9c/Action/RewardGold.cs b/Lib9c/Action/RewardGold.cs index 97289da705..f657a157a6 100644 --- a/Lib9c/Action/RewardGold.cs +++ b/Lib9c/Action/RewardGold.cs @@ -37,7 +37,6 @@ public override void LoadPlainValue(IValue plainValue) public override IWorld Execute(IActionContext context) { - GasTracer.UseGas(1); var states = context.PreviousState; states = TransferMead(context, states); states = GenesisGoldDistribution(context, states); @@ -283,27 +282,19 @@ public IWorld ResetChallengeCount(IActionContext ctx, IWorld states) public IWorld MinerReward(IActionContext ctx, IWorld states) { - // 마이닝 보상 - // https://www.notion.so/planetarium/Mining-Reward-b7024ef463c24ebca40a2623027d497d - // Currency currency = states.GetGoldCurrency(); Currency currency = Currencies.Mead; - FungibleAssetValue defaultMiningReward = currency * 10; - var countOfHalfLife = (int)Math.Pow(2, Convert.ToInt64((ctx.BlockIndex - 1) / 12614400)); - FungibleAssetValue miningReward = - defaultMiningReward.DivRem(countOfHalfLife, out FungibleAssetValue _); - - // var balance = states.GetBalance(GoldCurrencyState.Address, currency); - // if (miningReward >= FungibleAssetValue.Parse(currency, "1.25") && balance >= miningReward) - // { - // states = states.TransferAsset( - // ctx, - // GoldCurrencyState.Address, - // Addresses.RewardPool, - // miningReward - // ); - // } - - states = states.MintAsset(ctx, Addresses.RewardPool, miningReward); + var usedGas = states.GetBalance(Addresses.GasPool, currency); + var defaultReward = currency * 5; + var halfOfUsedGas = usedGas.DivRem(2).Quotient; + var gasToBurn = usedGas - halfOfUsedGas; + var miningReward = halfOfUsedGas + defaultReward; + states = states.MintAsset(ctx, Addresses.GasPool, defaultReward); + if (gasToBurn.Sign > 0) + { + states = states.BurnAsset(ctx, Addresses.GasPool, gasToBurn); + } + states = states.TransferAsset( + ctx, Addresses.GasPool, ctx.Miner, miningReward); return states; } From fbd6165213ede73edf575a07a15b6834eebf3c30 Mon Sep 17 00:00:00 2001 From: s2quake Date: Fri, 25 Oct 2024 15:53:28 +0900 Subject: [PATCH 2/6] bump: Bump libplanet for GasTracer --- .Libplanet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.Libplanet b/.Libplanet index 2b8189db79..7b8b6ad6d7 160000 --- a/.Libplanet +++ b/.Libplanet @@ -1 +1 @@ -Subproject commit 2b8189db79f53dd2541a45692c56bed31a593838 +Subproject commit 7b8b6ad6d76eb3652d03a0d895253c358a074612 From f437f33e84f61ef667eac2fe1488fe7c6bc83f93 Mon Sep 17 00:00:00 2001 From: s2quake Date: Fri, 25 Oct 2024 21:35:47 +0900 Subject: [PATCH 3/6] bump: Bump libplanet for GasTracer --- .Libplanet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.Libplanet b/.Libplanet index 7b8b6ad6d7..7d041d1c72 160000 --- a/.Libplanet +++ b/.Libplanet @@ -1 +1 @@ -Subproject commit 7b8b6ad6d76eb3652d03a0d895253c358a074612 +Subproject commit 7d041d1c7292aeae445b6efa205ac6c0a2d5641d From 9d67ee38e2ee62ec27c1362df4becfbd66eeadc0 Mon Sep 17 00:00:00 2001 From: s2quake Date: Fri, 25 Oct 2024 21:36:45 +0900 Subject: [PATCH 4/6] fix: Remove UseGas code from tx actions --- .../Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs | 2 -- Lib9c/Action/ValidatorDelegation/SlashValidator.cs | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Lib9c/Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs b/Lib9c/Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs index 1ac8e7f721..9ac68cd66c 100644 --- a/Lib9c/Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs +++ b/Lib9c/Action/ValidatorDelegation/ReleaseValidatorUnbondings.cs @@ -26,8 +26,6 @@ public override void LoadPlainValue(IValue plainValue) public override IWorld Execute(IActionContext context) { - GasTracer.UseGas(0L); - var world = context.PreviousState; var repository = new ValidatorRepository(world, context); var unbondingSet = repository.GetUnbondingSet(); diff --git a/Lib9c/Action/ValidatorDelegation/SlashValidator.cs b/Lib9c/Action/ValidatorDelegation/SlashValidator.cs index 74ab18bd9c..9ec0001745 100644 --- a/Lib9c/Action/ValidatorDelegation/SlashValidator.cs +++ b/Lib9c/Action/ValidatorDelegation/SlashValidator.cs @@ -31,8 +31,6 @@ public override void LoadPlainValue(IValue plainValue) public override IWorld Execute(IActionContext context) { - GasTracer.UseGas(0L); - var world = context.PreviousState; var repository = new ValidatorRepository(world, context); @@ -68,7 +66,7 @@ public override IWorld Execute(IActionContext context) break; } } - + return repository.World; } } From c3aeb9255ab74017d8760c5390963993164cea27 Mon Sep 17 00:00:00 2001 From: s2quake Date: Fri, 25 Oct 2024 21:37:22 +0900 Subject: [PATCH 5/6] feat: Add tx actions for collecting used-gas --- Lib9c.Policy/Policy/BlockPolicySource.cs | 10 +++- Lib9c/Action/ValidatorDelegation/Mortgage.cs | 62 ++++++++++++++++++++ Lib9c/Action/ValidatorDelegation/Refund.cs | 50 ++++++++++++++++ Lib9c/Action/ValidatorDelegation/Reward.cs | 56 ++++++++++++++++++ Lib9c/Addresses.cs | 2 + 5 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 Lib9c/Action/ValidatorDelegation/Mortgage.cs create mode 100644 Lib9c/Action/ValidatorDelegation/Refund.cs create mode 100644 Lib9c/Action/ValidatorDelegation/Reward.cs diff --git a/Lib9c.Policy/Policy/BlockPolicySource.cs b/Lib9c.Policy/Policy/BlockPolicySource.cs index e4fa660d62..b7dd28b9bd 100644 --- a/Lib9c.Policy/Policy/BlockPolicySource.cs +++ b/Lib9c.Policy/Policy/BlockPolicySource.cs @@ -150,8 +150,12 @@ internal IBlockPolicy GetPolicy( new RewardGold(), new ReleaseValidatorUnbondings(), }.ToImmutableArray(), - beginTxActions: ImmutableArray.Empty, - endTxActions: ImmutableArray.Empty), + beginTxActions: new IAction[] { + new Mortgage(), + }.ToImmutableArray(), + endTxActions: new IAction[] { + new Reward(), new Refund(), + }.ToImmutableArray()), blockInterval: BlockInterval, validateNextBlockTx: validateNextBlockTx, validateNextBlock: validateNextBlock, @@ -169,7 +173,7 @@ internal IBlockPolicy GetPolicy( Transaction transaction) { // Avoid NRE when genesis block appended - long index = blockChain.Count > 0 ? blockChain.Tip.Index + 1: 0; + long index = blockChain.Count > 0 ? blockChain.Tip.Index + 1 : 0; if (transaction.Actions?.Count > 1) { diff --git a/Lib9c/Action/ValidatorDelegation/Mortgage.cs b/Lib9c/Action/ValidatorDelegation/Mortgage.cs new file mode 100644 index 0000000000..bd65d6e7d7 --- /dev/null +++ b/Lib9c/Action/ValidatorDelegation/Mortgage.cs @@ -0,0 +1,62 @@ +using Bencodex.Types; +using Lib9c; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Types.Assets; + +namespace Nekoyume.Action.ValidatorDelegation +{ + /// + /// An action for mortgage gas fee for a transaction. + /// Should be executed at the beginning of the tx. + /// + public sealed class Mortgage : ActionBase + { + /// + /// Creates a new instance of . + /// + public Mortgage() + { + } + + /// + public override IValue PlainValue => new Bencodex.Types.Boolean(true); + + /// + public override void LoadPlainValue(IValue plainValue) + { + // Method intentionally left empty. + } + + /// + public override IWorld Execute(IActionContext context) + { + var state = context.PreviousState; + if (context.MaxGasPrice is not { Sign: > 0 } realGasPrice) + { + return state; + } + + var gasOwned = state.GetBalance(context.Signer, realGasPrice.Currency); + var gasRequired = realGasPrice * GasTracer.GasAvailable; + var gasToMortgage = gasOwned < gasRequired ? gasOwned : gasRequired; + if (gasOwned < gasRequired) + { + // var msg = + // $"The account {context.Signer}'s balance of {realGasPrice.Currency} is " + + // "insufficient to pay gas fee: " + + // $"{gasOwned} < {realGasPrice * gasLimit}."; + GasTracer.CancelTrace(); + // throw new InsufficientBalanceException(msg, context.Signer, gasOwned); + } + + if (gasToMortgage.Sign > 0) + { + return state.TransferAsset( + context, context.Signer, Addresses.MortgagePool, gasToMortgage); + } + + return state; + } + } +} diff --git a/Lib9c/Action/ValidatorDelegation/Refund.cs b/Lib9c/Action/ValidatorDelegation/Refund.cs new file mode 100644 index 0000000000..6ee1b8731a --- /dev/null +++ b/Lib9c/Action/ValidatorDelegation/Refund.cs @@ -0,0 +1,50 @@ +using Bencodex.Types; +using Lib9c; +using Libplanet.Action; +using Libplanet.Action.State; + +namespace Nekoyume.Action.ValidatorDelegation +{ + /// + /// An action for refund gas fee for a transaction. + /// Should be executed at the beginning of the tx. + /// + public sealed class Refund : ActionBase + { + /// + /// Creates a new instance of . + /// + public Refund() + { + } + + /// + public override IValue PlainValue => new Bencodex.Types.Boolean(true); + + /// + public override void LoadPlainValue(IValue plainValue) + { + // Method intentionally left empty. + } + + /// + public override IWorld Execute(IActionContext context) + { + var world = context.PreviousState; + if (context.MaxGasPrice is not { Sign: > 0 } realGasPrice) + { + return world; + } + + // Need to check if this matches GasTracer.GasAvailable? + var remaining = world.GetBalance(Addresses.MortgagePool, realGasPrice.Currency); + if (remaining.Sign <= 0) + { + return world; + } + + return world.TransferAsset( + context, Addresses.MortgagePool, context.Signer, remaining); + } + } +} diff --git a/Lib9c/Action/ValidatorDelegation/Reward.cs b/Lib9c/Action/ValidatorDelegation/Reward.cs new file mode 100644 index 0000000000..4d45e425a6 --- /dev/null +++ b/Lib9c/Action/ValidatorDelegation/Reward.cs @@ -0,0 +1,56 @@ +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; + +namespace Nekoyume.Action.ValidatorDelegation +{ + /// + /// An action for reward for a transaction. + /// Should be executed at the beginning of the tx. + /// + public sealed class Reward : ActionBase + { + /// + /// Creates a new instance of . + /// + public Reward() + { + } + + /// + public override IValue PlainValue => new Bencodex.Types.Boolean(true); + + /// + public override void LoadPlainValue(IValue plainValue) + { + // Method intentionally left empty. + } + + /// + public override IWorld Execute(IActionContext context) + { + var world = context.PreviousState; + if (context.MaxGasPrice is not { Sign: > 0 } realGasPrice) + { + return world; + } + + if (GasTracer.GasUsed <= 0) + { + return world; + } + + var gasMortgaged = world.GetBalance(Addresses.MortgagePool, realGasPrice.Currency); + var gasUsedPrice = realGasPrice * GasTracer.GasUsed; + var gasToTransfer = gasMortgaged < gasUsedPrice ? gasMortgaged : gasUsedPrice; + + if (gasToTransfer.Sign <= 0) + { + return world; + } + + return world.TransferAsset( + context, Addresses.MortgagePool, Addresses.GasPool, gasToTransfer); + } + } +} diff --git a/Lib9c/Addresses.cs b/Lib9c/Addresses.cs index 56adc24940..2da809ef1e 100644 --- a/Lib9c/Addresses.cs +++ b/Lib9c/Addresses.cs @@ -48,6 +48,8 @@ public static class Addresses public static readonly Address DailyReward = new Address("0000000000000000000000000000000000000020"); public static readonly Address ActionPoint = new Address("0000000000000000000000000000000000000021"); public static readonly Address RuneState = new Address("0000000000000000000000000000000000000022"); + public static readonly Address MortgagePool = new Address("0000000000000000000000000000000000100000"); + public static readonly Address GasPool = new Address("0000000000000000000000000000000000100001"); // Adventure Boss public static readonly Address AdventureBoss = new Address("0000000000000000000000000000000000000100"); From 36fdc4d00722f05bd2c6cf70e793b69b91fb1279 Mon Sep 17 00:00:00 2001 From: s2quake Date: Fri, 25 Oct 2024 21:37:53 +0900 Subject: [PATCH 6/6] test: Add test code for collecting gas --- .../Action/ValidatorDelegation/GasTest.cs | 181 ++++++++++++ .../GasWithTransferAssetTest.cs | 205 +++++++++++++ .../ValidatorDelegation/TxAcitonTestBase.cs | 275 ++++++++++++++++++ .Lib9c.Tests/Policy/BlockPolicyTest.cs | 12 +- 4 files changed, 668 insertions(+), 5 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/ValidatorDelegation/GasTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/GasTest.cs new file mode 100644 index 0000000000..306f56640d --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/GasTest.cs @@ -0,0 +1,181 @@ +#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)] + 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 gasActions = new IAction[] + { + new GasAction { Consumption = gasConsumption }, + }; + + MakeTransaction( + signerKey, + gasActions, + maxGasPrice: Mead * 1, + gasLimit: gasLimit); + MoveToNextBlock(throwOnError: true); + + // Then + var actualMead = GetBalance(signerKey.Address, Mead); + Assert.Equal(expectedMead, actualMead); + } + + [Theory] + [InlineData(0, 4)] + [InlineData(1, 4)] + [InlineData(4, 4)] + public void Execute_Without_GasLimit_And_MaxGasPrice( + long gasConsumption, long gasOwned) + { + 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; + var gasActions = new IAction[] + { + new GasAction { Consumption = gasConsumption }, + }; + + MakeTransaction(signerKey, gasActions); + 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)] + 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.IsType(innerExceptions[0].Action); + 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..1815411ccd --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/GasWithTransferAssetTest.cs @@ -0,0 +1,205 @@ +#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(4, 5)] + [InlineData(4, 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(0, 4)] + [InlineData(1, 4)] + [InlineData(4, 4)] + public void Execute_Without_GasLimit_And_MaxGasPrice( + long gasConsumption, long gasOwned) + { + 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; + var recipientKey = new PrivateKey(); + EnsureToMintAsset(signerKey, signerMead); + EnsureToMintAsset(signerKey, NCG * 100); + + // When + var amount = NCG * 1; + var expectedNCG = NCG * 1; + var expectedMead = signerMead; + var transferAsset = new TransferAsset( + signerKey.Address, recipientKey.Address, amount, memo: "test"); + MakeTransaction( + signerKey, + new ActionBase[] { transferAsset, }); + + // Then + 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)] + 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..147da5fb4b 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,13 +355,15 @@ 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? blockChain.MakeTransaction( adminPrivateKey, - new ActionBase[] { new ClaimRewardValidatorSelf(), } + new ActionBase[] { new ClaimRewardValidatorSelf(), }, + gasLimit: 1, + maxGasPrice: Currencies.Mead * 1 ); block = blockChain.ProposeBlock(adminPrivateKey, commit); @@ -376,7 +378,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); }