From 24d1ead5902be1e4b382df1829c7933b86a6aaff Mon Sep 17 00:00:00 2001 From: s2quake Date: Tue, 24 Sep 2024 10:58:02 +0900 Subject: [PATCH 1/5] test: Add test for PromoteValidatorTest --- .../PromoteValidatorTest.cs | 27 ++++++- .../ValidatorDelegationTestBase.cs | 76 +++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 .Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/PromoteValidatorTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/PromoteValidatorTest.cs index 7006fcb4e1..100161608d 100644 --- a/.Lib9c.Tests/Action/ValidatorDelegation/PromoteValidatorTest.cs +++ b/.Lib9c.Tests/Action/ValidatorDelegation/PromoteValidatorTest.cs @@ -12,7 +12,7 @@ namespace Lib9c.Tests.Action.ValidatorDelegation using Nekoyume.ValidatorDelegation; using Xunit; - public class PromoteValidatorTest + public class PromoteValidatorTest : ValidatorDelegationTestBase { [Fact] public void Serialization() @@ -135,5 +135,30 @@ public void CannotPromoteWithInsufficientBalance() Signer = publicKey.Address, })); } + + [Fact] + public void Promote_PromotedValidator_Throw() + { + // Given + var world = World; + var validatorPrivateKey = new PrivateKey(); + var blockHeight = 1L; + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey, NCG * 10, blockHeight++); + + // When + var promoteValidator = new PromoteValidator( + validatorPrivateKey.PublicKey, NCG * 10); + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight++, + Signer = validatorPrivateKey.Address, + }; + + // Then + Assert.Throws( + () => promoteValidator.Execute(actionContext)); + } } } diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs b/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs new file mode 100644 index 0000000000..008daea725 --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs @@ -0,0 +1,76 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using System.Numerics; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Mocks; + using Libplanet.Types.Assets; + using Libplanet.Types.Blocks; + using Libplanet.Types.Consensus; + using Libplanet.Types.Evidence; + using Nekoyume; + using Nekoyume.Action.ValidatorDelegation; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Nekoyume.ValidatorDelegation; + + public class ValidatorDelegationTestBase + { + protected static readonly Currency NCG = Currency.Uncapped("NCG", 2, null); + + public ValidatorDelegationTestBase() + { + var world = new World(MockUtil.MockModernWorldState); + var goldCurrencyState = new GoldCurrencyState(NCG); + World = world + .SetLegacyState(Addresses.GoldCurrency, goldCurrencyState.Serialize()); + } + + protected static BlockHash EmptyBlockHash { get; } + = new BlockHash(GetRandomArray(BlockHash.Size, _ => (byte)0x01)); + + protected PrivateKey AdminKey { get; } = new PrivateKey(); + + protected IWorld World { get; } + + protected static T[] GetRandomArray(int length, Func creator) + => Enumerable.Range(0, length).Select(creator).ToArray(); + + protected static IWorld MintAsset( + IWorld world, + PrivateKey delegatorPrivateKey, + FungibleAssetValue amount, + long blockHeight) + { + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + }; + return world.MintAsset(actionContext, delegatorPrivateKey.Address, amount); + } + + protected static IWorld EnsureValidatorToBePromoted( + IWorld world, + PrivateKey validatorPrivateKey, + FungibleAssetValue amount, + long blockHeight) + { + var validatorPublicKey = validatorPrivateKey.PublicKey; + var promoteValidator = new PromoteValidator(validatorPublicKey, amount); + var actionContext = new ActionContext + { + PreviousState = MintAsset( + world, validatorPrivateKey, amount, blockHeight), + Signer = validatorPublicKey.Address, + BlockIndex = blockHeight, + }; + return promoteValidator.Execute(actionContext); + } + } +} From f33cba7152bbcbd5205eda2d11187459f2f96bfc Mon Sep 17 00:00:00 2001 From: s2quake Date: Tue, 24 Sep 2024 11:03:17 +0900 Subject: [PATCH 2/5] test: Add infraction-related test code --- .../ValidatorDelegation/SlashValidatorTest.cs | 172 +++++++++++++ .../UnjailValidatorTest.cs | 148 +++++++++++ .../ValidatorDelegationTestBase.cs | 229 ++++++++++++++++++ 3 files changed, 549 insertions(+) create mode 100644 .Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs create mode 100644 .Lib9c.Tests/Action/ValidatorDelegation/UnjailValidatorTest.cs diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs new file mode 100644 index 0000000000..5c9b77f849 --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs @@ -0,0 +1,172 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Numerics; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Blocks; + using Libplanet.Types.Consensus; + using Libplanet.Types.Evidence; + using Nekoyume.Action.ValidatorDelegation; + using Nekoyume.ValidatorDelegation; + using Xunit; + + public class SlashValidatorTest : ValidatorDelegationTestBase + { + [Fact] + public void Serialization() + { + var action = new SlashValidator(); + var plainValue = action.PlainValue; + + var deserialized = new SlashValidator(); + deserialized.LoadPlainValue(plainValue); + } + + [Fact] + public void Execute() + { + // Given + const int length = 10; + var world = World; + var validatorPrivateKey = new PrivateKey(); + var validatorGold = NCG * 10; + var deletatorPrivateKeys = GetRandomArray(length, _ => new PrivateKey()); + var delegatorNCGs = GetRandomArray( + length, i => NCG * Random.Shared.Next(10, 100)); + var blockHeight = 1L; + var actionContext = new ActionContext { }; + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey, NCG * 10, blockHeight++); + world = EnsureDelegatorsToBeBond( + world, deletatorPrivateKeys, validatorPrivateKey, delegatorNCGs, blockHeight++); + + // When + var validatorSet = new ValidatorSet(new List + { + new (validatorPrivateKey.PublicKey, new BigInteger(1000)), + }); + var vote1 = new VoteMetadata( + height: blockHeight - 1, + round: 0, + blockHash: new BlockHash(GetRandomArray(BlockHash.Size, _ => (byte)0x01)), + timestamp: DateTimeOffset.UtcNow, + validatorPublicKey: validatorPrivateKey.PublicKey, + validatorPower: BigInteger.One, + flag: VoteFlag.PreCommit).Sign(validatorPrivateKey); + var vote2 = new VoteMetadata( + height: blockHeight - 1, + round: 0, + blockHash: new BlockHash(GetRandomArray(BlockHash.Size, _ => (byte)0x02)), + timestamp: DateTimeOffset.UtcNow, + validatorPublicKey: validatorPrivateKey.PublicKey, + validatorPower: BigInteger.One, + flag: VoteFlag.PreCommit).Sign(validatorPrivateKey); + var evidence = new DuplicateVoteEvidence( + vote1, + vote2, + validatorSet, + vote1.Timestamp); + var lastCommit = new BlockCommit( + height: blockHeight - 1, + round: 0, + blockHash: EmptyBlockHash, + ImmutableArray.Create(vote1)); + var slashValidator = new SlashValidator(); + actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight++, + Evidence = new List { evidence }, + LastCommit = lastCommit, + }; + world = slashValidator.Execute(actionContext); + + // Then + var balance = world.GetBalance(validatorPrivateKey.Address, NCG); + var repository = new ValidatorRepository(world, actionContext); + var delegatee = repository.GetValidatorDelegatee(validatorPrivateKey.Address); + + Assert.True(delegatee.Jailed); + Assert.Equal(long.MaxValue, delegatee.JailedUntil); + Assert.True(delegatee.Tombstoned); + } + + [Fact] + public void Jail_By_Abstain() + { + // Given + var world = World; + var validatorPrivateKey = new PrivateKey(); + var actionContext = new ActionContext { }; + var blockHeight = 1L; + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey, NCG * 10, blockHeight++); + + // When + for (var i = 0L; i <= AbstainHistory.MaxAbstainAllowance; i++) + { + var vote = CreateNullVote(validatorPrivateKey, blockHeight - 1); + var lastCommit = new BlockCommit( + height: blockHeight - 1, + round: 0, + blockHash: vote.BlockHash, + ImmutableArray.Create(vote)); + world = ExecuteSlashValidator( + world, validatorPrivateKey.PublicKey, lastCommit, blockHeight++); + } + + // Then + var repository = new ValidatorRepository(world, actionContext); + var delegatee = repository.GetValidatorDelegatee(validatorPrivateKey.Address); + Assert.True(delegatee.Jailed); + Assert.False(delegatee.Tombstoned); + } + + [Fact] + public void Jail_JailedDelegatee_Nothing_Happens_Test() + { + // Given + var world = World; + var validatorPrivateKey = new PrivateKey(); + var blockHeight = 1L; + var actionContext = new ActionContext(); + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey, NCG * 10, blockHeight++); + world = EnsureValidatorToBeTombstoned(world, validatorPrivateKey, blockHeight++); + + // When + var expectedRepository = new ValidatorRepository(world, actionContext); + var expectedDelegatee = expectedRepository.GetValidatorDelegatee( + validatorPrivateKey.Address); + var expectedJailed = expectedDelegatee.Jailed; + var evidence = CreateDuplicateVoteEvidence(validatorPrivateKey, blockHeight - 1); + var lastCommit = new BlockCommit( + height: blockHeight - 1, + round: 0, + blockHash: EmptyBlockHash, + ImmutableArray.Create(evidence.VoteRef)); + var slashValidator = new SlashValidator(); + actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight + SlashValidator.AbstainJailTime, + Signer = validatorPrivateKey.Address, + Evidence = new List { evidence }, + LastCommit = lastCommit, + }; + world = slashValidator.Execute(actionContext); + + // Then + var actualRepository = new ValidatorRepository(world, actionContext); + var actualDelegatee = actualRepository.GetValidatorDelegatee( + validatorPrivateKey.Address); + var actualJailed = actualDelegatee.Jailed; + + Assert.Equal(expectedJailed, actualJailed); + } + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/UnjailValidatorTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/UnjailValidatorTest.cs new file mode 100644 index 0000000000..2e2111fd63 --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/UnjailValidatorTest.cs @@ -0,0 +1,148 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation +{ + using System; + using Libplanet.Crypto; + using Nekoyume.Action; + using Nekoyume.Action.ValidatorDelegation; + using Nekoyume.ValidatorDelegation; + using Xunit; + + public class UnjailValidatorTest : ValidatorDelegationTestBase + { + [Fact] + public void Serialization() + { + var action = new UnjailValidator(); + var plainValue = action.PlainValue; + + var deserialized = new UnjailValidator(); + deserialized.LoadPlainValue(plainValue); + } + + [Fact] + public void Execute() + { + // Given + var world = World; + var validatorPrivateKey = new PrivateKey(); + var blockHeight = 1L; + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey, NCG * 10, blockHeight++); + world = EnsureValidatorToBeJailed(world, validatorPrivateKey, ref blockHeight); + + // When + var unjailValidator = new UnjailValidator(); + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight + SlashValidator.AbstainJailTime, + Signer = validatorPrivateKey.PublicKey.Address, + }; + world = unjailValidator.Execute(actionContext); + + // Then + var repository = new ValidatorRepository(world, actionContext); + var delegatee = repository.GetValidatorDelegatee(validatorPrivateKey.Address); + Assert.False(delegatee.Jailed); + Assert.Equal(-1, delegatee.JailedUntil); + Assert.False(delegatee.Tombstoned); + } + + [Fact] + public void Unjail_NotExistedDelegatee_Throw() + { + // Given + var world = World; + var validatorPrivateKey = new PrivateKey(); + var blockHeight = 1L; + + // When + var unjailValidator = new UnjailValidator(); + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight + SlashValidator.AbstainJailTime, + Signer = validatorPrivateKey.Address, + }; + + // Then + Assert.Throws( + () => unjailValidator.Execute(actionContext)); + } + + [Fact] + public void Unjail_JaliedValidator_NotJailed_Throw() + { + // Given + var world = World; + var validatorPrivateKey = new PrivateKey(); + var blockHeight = 1L; + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey, NCG * 10, blockHeight++); + + // When + var unjailValidator = new UnjailValidator(); + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight + SlashValidator.AbstainJailTime, + Signer = validatorPrivateKey.PublicKey.Address, + }; + + // Then + Assert.Throws( + () => unjailValidator.Execute(actionContext)); + } + + [Fact] + public void Unjail_JaliedValidator_Early_Throw() + { + // Given + var world = World; + var validatorPrivateKey = new PrivateKey(); + var blockHeight = 1L; + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey, NCG * 10, blockHeight++); + world = EnsureValidatorToBeJailed(world, validatorPrivateKey, ref blockHeight); + + // When + var unjailValidator = new UnjailValidator(); + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight + SlashValidator.AbstainJailTime - 1, + Signer = validatorPrivateKey.PublicKey.Address, + }; + + // Then + Assert.Throws( + () => unjailValidator.Execute(actionContext)); + } + + [Fact] + public void Unjail_JaliedValidator_Tombstoned_Throw() + { + // Given + var world = World; + var validatorPrivateKey = new PrivateKey(); + var blockHeight = 1L; + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey, NCG * 10, blockHeight++); + world = EnsureValidatorToBeTombstoned(world, validatorPrivateKey, blockHeight++); + + // When + var unjailValidator = new UnjailValidator(); + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight + SlashValidator.AbstainJailTime, + Signer = validatorPrivateKey.PublicKey.Address, + }; + + // Then + Assert.Throws( + () => unjailValidator.Execute(actionContext)); + } + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs b/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs index 008daea725..83529d8445 100644 --- a/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs +++ b/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs @@ -72,5 +72,234 @@ protected static IWorld EnsureValidatorToBePromoted( }; return promoteValidator.Execute(actionContext); } + + protected static IWorld ExecuteSlashValidator( + IWorld world, + PublicKey validatorPublicKey, + BlockCommit lastCommit, + long blockHeight) + { + var slashValidator = new SlashValidator(); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorPublicKey.Address, + BlockIndex = blockHeight, + LastCommit = lastCommit, + }; + return slashValidator.Execute(actionContext); + } + + protected static IWorld EnsureDelegatorToBeBond( + IWorld world, + PrivateKey delegatorPrivateKey, + PrivateKey validatorPrivateKey, + FungibleAssetValue amount, + long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var delegatorAddress = delegatorPrivateKey.Address; + var validatorAddress = validatorPrivateKey.Address; + var actionContext = new ActionContext + { + PreviousState = MintAsset( + world, delegatorPrivateKey, amount, blockHeight), + BlockIndex = blockHeight, + Signer = delegatorAddress, + }; + var delegatorValidator = new DelegateValidator( + validatorAddress, amount); + return delegatorValidator.Execute(actionContext); + } + + protected static IWorld EnsureDelegatorsToBeBond( + IWorld world, + PrivateKey[] delegatorPrivateKeys, + PrivateKey validatorPrivateKey, + FungibleAssetValue[] amounts, + long blockHeight) + { + if (delegatorPrivateKeys.Length != amounts.Length) + { + throw new ArgumentException( + "The length of delegatorPrivateKeys and amounts must be the same."); + } + + for (var i = 0; i < delegatorPrivateKeys.Length; i++) + { + world = EnsureDelegatorToBeBond( + world, delegatorPrivateKeys[i], validatorPrivateKey, amounts[i], blockHeight); + } + + return world; + } + + protected static IWorld EnsureValidatorToBeJailed( + IWorld world, PrivateKey validatorPrivateKey, ref long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var repository = new ValidatorRepository(world, new ActionContext()); + var delegatee = repository.GetValidatorDelegatee(validatorPrivateKey.Address); + if (delegatee.Jailed) + { + throw new ArgumentException( + "The validator is already jailed.", nameof(validatorPrivateKey)); + } + + for (var i = 0L; i <= AbstainHistory.MaxAbstainAllowance; i++) + { + var vote = CreateNullVote(validatorPrivateKey, blockHeight - 1); + var lastCommit = new BlockCommit( + height: blockHeight - 1, + round: 0, + blockHash: vote.BlockHash, + ImmutableArray.Create(vote)); + world = ExecuteSlashValidator( + world, validatorPrivateKey.PublicKey, lastCommit, blockHeight); + blockHeight++; + repository = new ValidatorRepository(world, new ActionContext()); + delegatee = repository.GetValidatorDelegatee(validatorPrivateKey.Address); + if (delegatee.Jailed) + { + break; + } + } + + return world; + } + + protected static IWorld EnsureValidatorToBeTombstoned( + IWorld world, PrivateKey validatorPrivateKey, long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var evidence = CreateDuplicateVoteEvidence(validatorPrivateKey, blockHeight - 1); + var lastCommit = new BlockCommit( + height: blockHeight - 1, + round: 0, + blockHash: EmptyBlockHash, + ImmutableArray.Create(evidence.VoteRef)); + + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight, + Evidence = new List { evidence }, + LastCommit = lastCommit, + }; + var slashValidator = new SlashValidator(); + + return slashValidator.Execute(actionContext); + } + + protected static Vote CreateNullVote( + PrivateKey privateKey, long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var power = new BigInteger(100); + var validator = new Validator(privateKey.PublicKey, power); + var blockHash = EmptyBlockHash; + var timestamp = DateTimeOffset.UtcNow; + var voteMetadata = new VoteMetadata( + height: blockHeight, + round: 0, + blockHash: blockHash, + timestamp: timestamp, + validatorPublicKey: validator.PublicKey, + validatorPower: power, + flag: VoteFlag.Null); + return voteMetadata.Sign(null); + } + + protected static Vote CreateVote( + PrivateKey privateKey, long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var power = new BigInteger(100); + var validator = new Validator(privateKey.PublicKey, power); + var blockHash = EmptyBlockHash; + var timestamp = DateTimeOffset.UtcNow; + var voteMetadata = new VoteMetadata( + height: blockHeight, + round: 0, + blockHash: blockHash, + timestamp: timestamp, + validatorPublicKey: validator.PublicKey, + validatorPower: power, + flag: VoteFlag.PreCommit); + return voteMetadata.Sign(privateKey); + } + + protected static BlockCommit CreateLastCommit( + PrivateKey privateKey, long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var vote = CreateVote(privateKey, blockHeight); + return new BlockCommit( + height: blockHeight, + round: 0, + blockHash: vote.BlockHash, + ImmutableArray.Create(vote)); + } + + protected static DuplicateVoteEvidence CreateDuplicateVoteEvidence( + PrivateKey validatorPrivateKey, long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var validatorSet = new ValidatorSet(new List + { + new (validatorPrivateKey.PublicKey, new BigInteger(1000)), + }); + var vote1 = new VoteMetadata( + height: blockHeight, + round: 0, + blockHash: new BlockHash(GetRandomArray(BlockHash.Size, _ => (byte)0x01)), + timestamp: DateTimeOffset.UtcNow, + validatorPublicKey: validatorPrivateKey.PublicKey, + validatorPower: BigInteger.One, + flag: VoteFlag.PreCommit).Sign(validatorPrivateKey); + var vote2 = new VoteMetadata( + height: blockHeight, + round: 0, + blockHash: new BlockHash(GetRandomArray(BlockHash.Size, _ => (byte)0x02)), + timestamp: DateTimeOffset.UtcNow, + validatorPublicKey: validatorPrivateKey.PublicKey, + validatorPower: BigInteger.One, + flag: VoteFlag.PreCommit).Sign(validatorPrivateKey); + var evidence = new DuplicateVoteEvidence( + vote1, + vote2, + validatorSet, + vote1.Timestamp); + + return evidence; + } } } From bb808af78a80379c62c89587df884f114fe5f8c6 Mon Sep 17 00:00:00 2001 From: s2quake Date: Tue, 24 Sep 2024 11:04:31 +0900 Subject: [PATCH 3/5] test: Add delegation-related test code --- .../DelegateValidatorTest.cs | 26 ++- .../RedelegateValidatorTest.cs | 159 +++++++++++++++++- .../UndelegateValidatorTest.cs | 129 +++++++++++++- 3 files changed, 311 insertions(+), 3 deletions(-) diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/DelegateValidatorTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/DelegateValidatorTest.cs index 5ad8613c00..a7b71a632e 100644 --- a/.Lib9c.Tests/Action/ValidatorDelegation/DelegateValidatorTest.cs +++ b/.Lib9c.Tests/Action/ValidatorDelegation/DelegateValidatorTest.cs @@ -6,13 +6,14 @@ namespace Lib9c.Tests.Action.ValidatorDelegation using Libplanet.Mocks; using Libplanet.Types.Assets; using Nekoyume; + using Nekoyume.Action; using Nekoyume.Action.ValidatorDelegation; using Nekoyume.Model.State; using Nekoyume.Module; using Nekoyume.ValidatorDelegation; using Xunit; - public class DelegateValidatorTest + public class DelegateValidatorTest : ValidatorDelegationTestBase { [Fact] public void Serialization() @@ -138,5 +139,28 @@ public void CannotDelegateWithInsufficientBalance() Signer = publicKey.Address, })); } + + [Fact] + public void CannotDelegateToInvalidValidator() + { + // Given + var world = World; + var validatorPrivateKey = new PrivateKey(); + var delegatorPrivateKey = new PrivateKey(); + var blockHeight = 1L; + world = MintAsset(world, delegatorPrivateKey, NCG * 100, blockHeight++); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = delegatorPrivateKey.Address, + }; + var delegateValidator = new DelegateValidator(validatorPrivateKey.Address, NCG * 10); + + // Then + Assert.Throws( + () => delegateValidator.Execute(actionContext)); + } } } diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/RedelegateValidatorTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/RedelegateValidatorTest.cs index 1f5ed59d0e..ee4c8b73da 100644 --- a/.Lib9c.Tests/Action/ValidatorDelegation/RedelegateValidatorTest.cs +++ b/.Lib9c.Tests/Action/ValidatorDelegation/RedelegateValidatorTest.cs @@ -1,18 +1,20 @@ namespace Lib9c.Tests.Action.ValidatorDelegation { + using System; using System.Numerics; using Libplanet.Action.State; using Libplanet.Crypto; using Libplanet.Mocks; using Libplanet.Types.Assets; using Nekoyume; + using Nekoyume.Action; using Nekoyume.Action.ValidatorDelegation; using Nekoyume.Model.State; using Nekoyume.Module; using Nekoyume.ValidatorDelegation; using Xunit; - public class RedelegateValidatorTest + public class RedelegateValidatorTest : ValidatorDelegationTestBase { [Fact] public void Serialization() @@ -86,5 +88,160 @@ public void Execute() Assert.Equal((gg * 20).RawValue, dstValidator.TotalShares); Assert.Equal((gg * 20).RawValue, dstValidator.Power); } + + [Fact] + public void Redelegate_ToInvalidValidator_Throw() + { + // Given + var world = World; + var validatorPrivateKey = new PrivateKey(); + var delegatorPrivateKey = new PrivateKey(); + var blockHeight = 1L; + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey, NCG * 10, blockHeight++); + world = EnsureDelegatorToBeBond( + world, delegatorPrivateKey, validatorPrivateKey, NCG * 10, blockHeight++); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight++, + Signer = delegatorPrivateKey.Address, + }; + var redelegateValidator = new RedelegateValidator( + validatorPrivateKey.PublicKey.Address, + new PrivateKey().PublicKey.Address, + 10); + + // Then + Assert.Throws( + () => redelegateValidator.Execute(actionContext)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public void Redelegate_NotPositiveShare_Throw(long share) + { + // Given + var world = World; + var validatorPrivateKey1 = new PrivateKey(); + var validatorPrivateKey2 = new PrivateKey(); + var delegatorPrivateKey = new PrivateKey(); + var blockHeight = 1L; + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey1, NCG * 10, blockHeight++); + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey2, NCG * 10, blockHeight++); + world = EnsureDelegatorToBeBond( + world, delegatorPrivateKey, validatorPrivateKey1, NCG * 10, blockHeight++); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight++, + Signer = delegatorPrivateKey.Address, + }; + var redelegateValidator = new RedelegateValidator( + validatorPrivateKey1.PublicKey.Address, + validatorPrivateKey2.PublicKey.Address, + share); + + // Then + Assert.Throws( + () => redelegateValidator.Execute(actionContext)); + } + + [Fact] + public void Redelegate_OverShare_Throw() + { + // Given + var world = World; + var validatorPrivateKey1 = new PrivateKey(); + var validatorPrivateKey2 = new PrivateKey(); + var delegatorPrivateKey = new PrivateKey(); + var blockHeight = 1L; + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey1, NCG * 10, blockHeight++); + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey2, NCG * 10, blockHeight++); + world = EnsureDelegatorToBeBond( + world, delegatorPrivateKey, validatorPrivateKey1, NCG * 10, blockHeight++); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight++, + Signer = delegatorPrivateKey.Address, + }; + var repository = new ValidatorRepository(world, actionContext); + var delegatee = repository.GetDelegatee(validatorPrivateKey1.PublicKey.Address); + var bond = repository.GetBond(delegatee, delegatorPrivateKey.Address); + var redelegateValidator = new RedelegateValidator( + validatorPrivateKey1.PublicKey.Address, + validatorPrivateKey2.PublicKey.Address, + bond.Share + 1); + + // Then + Assert.Throws( + () => redelegateValidator.Execute(actionContext)); + } + + [Fact] + public void Redelegate_FromJailedValidator_Throw() + { + // Given + var world = World; + var delegatorPrivateKey = new PrivateKey(); + var validatorPrivateKey1 = new PrivateKey(); + var validatorPrivateKey2 = new PrivateKey(); + var blockHeight = 1L; + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey1, NCG * 10, blockHeight++); + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey2, NCG * 10, blockHeight++); + world = EnsureDelegatorToBeBond( + world, delegatorPrivateKey, validatorPrivateKey1, NCG * 10, blockHeight++); + world = EnsureValidatorToBeJailed( + world, validatorPrivateKey1, ref blockHeight); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = delegatorPrivateKey.Address, + BlockIndex = blockHeight++, + }; + var expectedRepository = new ValidatorRepository(world, actionContext); + var expectedDelegatee1 = expectedRepository.GetValidatorDelegatee( + validatorPrivateKey1.PublicKey.Address); + var expectedDelegatee2 = expectedRepository.GetValidatorDelegatee( + validatorPrivateKey2.PublicKey.Address); + var expectedBond1 = expectedRepository.GetBond( + expectedDelegatee1, delegatorPrivateKey.Address); + var expectedBond2 = expectedRepository.GetBond( + expectedDelegatee2, delegatorPrivateKey.Address); + + var redelegateValidator = new RedelegateValidator( + validatorPrivateKey1.Address, validatorPrivateKey2.Address, 10); + world = redelegateValidator.Execute(actionContext); + + // Then + var actualRepository = new ValidatorRepository(world, actionContext); + var actualDelegatee1 = actualRepository.GetValidatorDelegatee( + validatorPrivateKey1.PublicKey.Address); + var actualDelegatee2 = actualRepository.GetValidatorDelegatee( + validatorPrivateKey2.PublicKey.Address); + var actualBond1 = actualRepository.GetBond( + actualDelegatee1, delegatorPrivateKey.Address); + var actualBond2 = actualRepository.GetBond( + actualDelegatee2, delegatorPrivateKey.Address); + + Assert.Equal(expectedBond1.Share - 10, actualBond1.Share); + Assert.Equal(expectedBond2.Share + 10, actualBond2.Share); + } } } diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/UndelegateValidatorTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/UndelegateValidatorTest.cs index 3a9afdd9a9..2a78ba198e 100644 --- a/.Lib9c.Tests/Action/ValidatorDelegation/UndelegateValidatorTest.cs +++ b/.Lib9c.Tests/Action/ValidatorDelegation/UndelegateValidatorTest.cs @@ -1,18 +1,20 @@ namespace Lib9c.Tests.Action.ValidatorDelegation { + using System; using System.Numerics; using Libplanet.Action.State; using Libplanet.Crypto; using Libplanet.Mocks; using Libplanet.Types.Assets; using Nekoyume; + using Nekoyume.Action; using Nekoyume.Action.ValidatorDelegation; using Nekoyume.Model.State; using Nekoyume.Module; using Nekoyume.ValidatorDelegation; using Xunit; - public class UndelegateValidatorTest + public class UndelegateValidatorTest : ValidatorDelegationTestBase { [Fact] public void Serialization() @@ -80,5 +82,130 @@ public void Execute() Assert.Equal(gg * 100, world.GetBalance(publicKey.Address, gg)); } + + [Fact] + public void Undelegate_FromInvalidValidtor_Throw() + { + // Given + var world = World; + var delegatorPrivateKey = new PrivateKey(); + var validatorPrivateKey = new PrivateKey(); + var blockHeight = 1L; + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey, NCG * 10, blockHeight++); + world = EnsureDelegatorToBeBond( + world, delegatorPrivateKey, validatorPrivateKey, NCG * 10, blockHeight++); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = delegatorPrivateKey.Address, + BlockIndex = blockHeight++, + }; + var undelegateValidator = new UndelegateValidator( + new PrivateKey().Address, 10); + + // Then + Assert.Throws( + () => undelegateValidator.Execute(actionContext)); + } + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public void Undelegate_NotPositiveShare_Throw(long share) + { + // Given + var world = World; + var delegatorPrivateKey = new PrivateKey(); + var validatorPrivateKey = new PrivateKey(); + var blockHeight = 1L; + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey, NCG * 10, blockHeight++); + world = EnsureDelegatorToBeBond( + world, delegatorPrivateKey, validatorPrivateKey, NCG * 10, blockHeight++); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = delegatorPrivateKey.Address, + BlockIndex = blockHeight++, + }; + var undelegateValidator = new UndelegateValidator( + validatorPrivateKey.Address, share); + + // Then + Assert.Throws( + () => undelegateValidator.Execute(actionContext)); + } + + [Fact] + public void Undelegate_NotDelegated_Throw() + { + // Given + var world = World; + var delegatorPrivateKey = new PrivateKey(); + var validatorPrivateKey = new PrivateKey(); + var blockHeight = 1L; + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey, NCG * 10, blockHeight++); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = delegatorPrivateKey.Address, + BlockIndex = blockHeight++, + }; + var undelegateValidator = new UndelegateValidator( + validatorPrivateKey.Address, 10); + + // Then + Assert.Throws( + () => undelegateValidator.Execute(actionContext)); + } + + [Fact] + public void Undelegate_FromJailedValidator() + { + // Given + var world = World; + var delegatorPrivateKey = new PrivateKey(); + var validatorPrivateKey = new PrivateKey(); + var blockHeight = 1L; + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey, NCG * 10, blockHeight++); + world = EnsureDelegatorToBeBond( + world, delegatorPrivateKey, validatorPrivateKey, NCG * 10, blockHeight++); + world = EnsureValidatorToBeJailed( + world, validatorPrivateKey, ref blockHeight); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + Signer = delegatorPrivateKey.Address, + BlockIndex = blockHeight, + }; + var expectedRepository = new ValidatorRepository(world, actionContext); + var expectedDelegatee = expectedRepository.GetValidatorDelegatee( + validatorPrivateKey.PublicKey.Address); + var expectedBond = expectedRepository.GetBond( + expectedDelegatee, delegatorPrivateKey.Address); + + var undelegateValidator = new UndelegateValidator( + validatorPrivateKey.Address, 10); + world = undelegateValidator.Execute(actionContext); + + // Then + var actualRepository = new ValidatorRepository(world, actionContext); + var actualDelegatee = actualRepository.GetValidatorDelegatee( + validatorPrivateKey.PublicKey.Address); + var actualBond = actualRepository.GetBond(actualDelegatee, delegatorPrivateKey.Address); + + Assert.Equal(expectedBond.Share - 10, actualBond.Share); + } } } From a898836c6168e73042b115757bd3f804e2cf3f95 Mon Sep 17 00:00:00 2001 From: s2quake Date: Tue, 24 Sep 2024 11:05:36 +0900 Subject: [PATCH 4/5] test: Add RecordProposer, Update/Release Validator test code --- .../ValidatorDelegation/RecordProposerTest.cs | 50 ++++++++++++++ .../ReleaseValidatorUnbondingsTest.cs | 20 ++++++ .../UpdateValidatorsTest.cs | 69 +++++++++++++++++++ 3 files changed, 139 insertions(+) create mode 100644 .Lib9c.Tests/Action/ValidatorDelegation/RecordProposerTest.cs create mode 100644 .Lib9c.Tests/Action/ValidatorDelegation/ReleaseValidatorUnbondingsTest.cs create mode 100644 .Lib9c.Tests/Action/ValidatorDelegation/UpdateValidatorsTest.cs diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/RecordProposerTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/RecordProposerTest.cs new file mode 100644 index 0000000000..efda15a76c --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/RecordProposerTest.cs @@ -0,0 +1,50 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation +{ + using System; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Nekoyume.Action.ValidatorDelegation; + using Nekoyume.Module.ValidatorDelegation; + using Nekoyume.ValidatorDelegation; + using Xunit; + + public class RecordProposerTest : ValidatorDelegationTestBase + { + [Fact] + public void Serialization() + { + var action = new RecordProposer(); + var plainValue = action.PlainValue; + + var deserialized = new RecordProposer(); + deserialized.LoadPlainValue(plainValue); + } + + [Fact] + public void Execute() + { + // Given + var world = World; + var minerPrivateKey = new PrivateKey(); + var blockIndex = (long)Random.Shared.Next(1, 100); + + // When + var actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockIndex++, + Miner = minerPrivateKey.Address, + }; + var recordProposer = new RecordProposer(); + world = recordProposer.Execute(actionContext); + + // Then + var repository = new ValidatorRepository(world, actionContext); + var proposerInfo = repository.GetProposerInfo(); + + Assert.Equal(blockIndex - 1, proposerInfo.BlockIndex); + Assert.Equal(minerPrivateKey.Address, proposerInfo.Proposer); + } + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/ReleaseValidatorUnbondingsTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/ReleaseValidatorUnbondingsTest.cs new file mode 100644 index 0000000000..62b7569597 --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/ReleaseValidatorUnbondingsTest.cs @@ -0,0 +1,20 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation +{ + using System; + using Nekoyume.Action.ValidatorDelegation; + using Xunit; + + public class ReleaseValidatorUnbondingsTest + { + [Fact] + public void Serialization() + { + var action = new ReleaseValidatorUnbondings(); + var plainValue = action.PlainValue; + + var deserialized = new ReleaseValidatorUnbondings(); + deserialized.LoadPlainValue(plainValue); + } + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/UpdateValidatorsTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/UpdateValidatorsTest.cs new file mode 100644 index 0000000000..22a924dc08 --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/UpdateValidatorsTest.cs @@ -0,0 +1,69 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation +{ + using System; + using System.Linq; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Nekoyume.Action.ValidatorDelegation; + using Nekoyume.Model.State; + using Nekoyume.ValidatorDelegation; + using Xunit; + + public class UpdateValidatorsTest : ValidatorDelegationTestBase + { + [Fact] + public void Serialization() + { + var action = new UpdateValidators(); + var plainValue = action.PlainValue; + + var deserialized = new UpdateValidators(); + deserialized.LoadPlainValue(plainValue); + } + + [Fact] + public void Execute() + { + const int length = 10; + var world = World; + var privateKeys = GetRandomArray(length, _ => new PrivateKey()); + var favs = GetRandomArray(length, i => NCG * Random.Shared.Next(1, length + 1)); + + for (int i = 0; i < length; i++) + { + var signer = privateKeys[i]; + var fav = favs[i]; + var promoteValidator = new PromoteValidator(signer.PublicKey, fav); + var actionContext = new ActionContext + { + PreviousState = world.MintAsset(new ActionContext(), signer.Address, NCG * 1000), + Signer = signer.Address, + BlockIndex = 10L, + }; + world = promoteValidator.Execute(actionContext); + } + + var blockActionContext = new ActionContext + { + BlockIndex = 10L, + PreviousState = world, + Signer = AdminKey.Address, + }; + var expectedRepository = new ValidatorRepository(world, blockActionContext); + var expectedValidators = expectedRepository.GetValidatorList() + .GetBonded().OrderBy(item => item.OperatorAddress).ToList(); + + world = new UpdateValidators().Execute(blockActionContext); + + var actualValidators = world.GetValidatorSet().Validators; + Assert.Equal(expectedValidators.Count, actualValidators.Count); + for (var i = 0; i < expectedValidators.Count; i++) + { + var expectedValidator = expectedValidators[i]; + var actualValidator = actualValidators[i]; + Assert.Equal(expectedValidator, actualValidator); + } + } + } +} From 2dc00e4b02f1f4a4d1a17de723437f347c6d595c Mon Sep 17 00:00:00 2001 From: s2quake Date: Tue, 24 Sep 2024 11:06:15 +0900 Subject: [PATCH 5/5] test: Add test for ClaimRewardValidator --- .../ClaimRewardValidatorTest.cs | 303 ++++++++++++++++++ .../ValidatorDelegationTestBase.cs | 59 ++++ 2 files changed, 362 insertions(+) create mode 100644 .Lib9c.Tests/Action/ValidatorDelegation/ClaimRewardValidatorTest.cs diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/ClaimRewardValidatorTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/ClaimRewardValidatorTest.cs new file mode 100644 index 0000000000..98048b2236 --- /dev/null +++ b/.Lib9c.Tests/Action/ValidatorDelegation/ClaimRewardValidatorTest.cs @@ -0,0 +1,303 @@ +#nullable enable +namespace Lib9c.Tests.Action.ValidatorDelegation +{ + using System; + using System.Linq; + using System.Numerics; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume.Action.ValidatorDelegation; + using Nekoyume.ValidatorDelegation; + using Xunit; + + public class ClaimRewardValidatorTest : ValidatorDelegationTestBase + { + [Fact] + public void Serialization() + { + var action = new ClaimRewardValidator(); + var plainValue = action.PlainValue; + + var deserialized = new ClaimRewardValidator(); + deserialized.LoadPlainValue(plainValue); + } + + [Fact] + public void Execute() + { + // Given + var world = World; + var validatorPrivateKey = new PrivateKey(); + var blockHeight = 1L; + ActionContext actionContext; + var validatorGold = NCG * 10; + var allocatedReward = NCG * 100; + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey, validatorGold, blockHeight++); + world = EnsureValidatorToBeAllocatedReward( + world, validatorPrivateKey, allocatedReward, ref blockHeight); + + // When + var expectedBalance = allocatedReward; + var lastCommit = CreateLastCommit(validatorPrivateKey, blockHeight - 1); + var claimRewardValidator = new ClaimRewardValidator(validatorPrivateKey.Address); + actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight++, + Signer = validatorPrivateKey.Address, + LastCommit = lastCommit, + }; + world = claimRewardValidator.Execute(actionContext); + + // Then + var actualBalance = world.GetBalance(validatorPrivateKey.Address, NCG); + + Assert.Equal(expectedBalance, actualBalance); + } + + [Theory] + [InlineData(33.33)] + [InlineData(11.11)] + [InlineData(10)] + [InlineData(1)] + public void Execute_OneDelegator(double reward) + { + // Given + var world = World; + var validatorPrivateKey = new PrivateKey(); + var delegatorPrivateKey = new PrivateKey(); + var blockHeight = 1L; + var actionContext = new ActionContext { }; + var promotedGold = NCG * 10; + var allocatedReward = FungibleAssetValue.Parse(NCG, $"{reward:R}"); + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey, promotedGold, blockHeight++); + world = EnsureDelegatorToBeBond( + world, delegatorPrivateKey, validatorPrivateKey, NCG * 10, blockHeight++); + world = EnsureValidatorToBeAllocatedReward( + world, validatorPrivateKey, allocatedReward, ref blockHeight); + + // When + var expectedRepository = new ValidatorRepository(world, actionContext); + var expectedDelegatee = expectedRepository.GetValidatorDelegatee( + validatorPrivateKey.Address); + var expectedCommission = GetCommission( + allocatedReward, expectedDelegatee.CommissionPercentage); + var expectedReward = allocatedReward - expectedCommission; + var expectedValidatorBalance = expectedCommission + expectedReward.DivRem(2).Quotient; + var expectedDelegatorBalance = expectedReward.DivRem(2).Quotient; + var expectedRemainReward = allocatedReward; + expectedRemainReward -= expectedValidatorBalance; + expectedRemainReward -= expectedDelegatorBalance; + + var lastCommit = CreateLastCommit(validatorPrivateKey, blockHeight - 1); + actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight++, + Signer = validatorPrivateKey.Address, + LastCommit = lastCommit, + }; + world = new ClaimRewardValidator(validatorPrivateKey.Address).Execute(actionContext); + actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight++, + Signer = delegatorPrivateKey.Address, + LastCommit = lastCommit, + }; + world = new ClaimRewardValidator(validatorPrivateKey.Address).Execute(actionContext); + + // Then + var repository = new ValidatorRepository(world, actionContext); + var delegatee = repository.GetValidatorDelegatee(validatorPrivateKey.Address); + var actualRemainReward = world.GetBalance(delegatee.RewardDistributorAddress, NCG); + var actualValidatorBalance = world.GetBalance(validatorPrivateKey.Address, NCG); + var actualDelegatorBalance = world.GetBalance(delegatorPrivateKey.Address, NCG); + + Assert.Equal(expectedRemainReward, actualRemainReward); + Assert.Equal(expectedValidatorBalance, actualValidatorBalance); + Assert.Equal(expectedDelegatorBalance, actualDelegatorBalance); + } + + [Fact] + public void Execute_TwoDelegators() + { + // Given + var world = World; + var validatorPrivateKey = new PrivateKey(); + var delegatorPrivateKey1 = new PrivateKey(); + var delegatorPrivateKey2 = new PrivateKey(); + var blockHeight = 1L; + var actionContext = new ActionContext { }; + var promotedGold = NCG * 10; + var allocatedReward = FungibleAssetValue.Parse(NCG, $"{34.27:R}"); + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey, promotedGold, blockHeight++); + world = EnsureDelegatorToBeBond( + world, delegatorPrivateKey1, validatorPrivateKey, NCG * 10, blockHeight++); + world = EnsureDelegatorToBeBond( + world, delegatorPrivateKey2, validatorPrivateKey, NCG * 10, blockHeight++); + world = EnsureValidatorToBeAllocatedReward( + world, validatorPrivateKey, allocatedReward, ref blockHeight); + + // When + var expectedRepository = new ValidatorRepository(world, actionContext); + var expectedDelegatee = expectedRepository.GetValidatorDelegatee( + validatorPrivateKey.Address); + var expectedCommission = GetCommission( + allocatedReward, expectedDelegatee.CommissionPercentage); + var expectedReward = allocatedReward - expectedCommission; + var expectedValidatorBalance = expectedCommission + expectedReward.DivRem(3).Quotient; + var expectedDelegator1Balance = expectedReward.DivRem(3).Quotient; + var expectedDelegator2Balance = expectedReward.DivRem(3).Quotient; + var expectedRemainReward = allocatedReward; + expectedRemainReward -= expectedValidatorBalance; + expectedRemainReward -= expectedDelegator1Balance; + expectedRemainReward -= expectedDelegator2Balance; + + var lastCommit = CreateLastCommit(validatorPrivateKey, blockHeight - 1); + actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight++, + Signer = validatorPrivateKey.Address, + LastCommit = lastCommit, + }; + world = new ClaimRewardValidator(validatorPrivateKey.Address).Execute(actionContext); + actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight++, + Signer = delegatorPrivateKey1.Address, + LastCommit = lastCommit, + }; + world = new ClaimRewardValidator(validatorPrivateKey.Address).Execute(actionContext); + actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight++, + Signer = delegatorPrivateKey2.Address, + LastCommit = lastCommit, + }; + world = new ClaimRewardValidator(validatorPrivateKey.Address).Execute(actionContext); + + // Then + var repository = new ValidatorRepository(world, actionContext); + var delegatee = repository.GetValidatorDelegatee(validatorPrivateKey.Address); + var actualRemainReward = world.GetBalance(delegatee.RewardDistributorAddress, NCG); + var actualValidatorBalance = world.GetBalance(validatorPrivateKey.Address, NCG); + var actualDelegator1Balance = world.GetBalance(delegatorPrivateKey1.Address, NCG); + var actualDelegator2Balance = world.GetBalance(delegatorPrivateKey2.Address, NCG); + + Assert.Equal(expectedRemainReward, actualRemainReward); + Assert.Equal(expectedValidatorBalance, actualValidatorBalance); + Assert.Equal(expectedDelegator1Balance, actualDelegator1Balance); + Assert.Equal(expectedDelegator2Balance, actualDelegator2Balance); + } + + [Fact] + public void Execute_MultipleDelegators() + { + // Given + var length = Random.Shared.Next(3, 100); + var world = World; + var validatorPrivateKey = new PrivateKey(); + var delegatorPrivateKeys = GetRandomArray(length, _ => new PrivateKey()); + var delegatorNCGs = GetRandomArray(length, _ => GetRandomNCG()); + var blockHeight = 1L; + var actionContext = new ActionContext(); + var promotedGold = GetRandomNCG(); + var allocatedReward = GetRandomNCG(); + world = EnsureValidatorToBePromoted( + world, validatorPrivateKey, promotedGold, blockHeight++); + world = EnsureDelegatorsToBeBond( + world, delegatorPrivateKeys, validatorPrivateKey, delegatorNCGs, blockHeight++); + world = EnsureValidatorToBeAllocatedReward( + world, validatorPrivateKey, allocatedReward, ref blockHeight); + + // When + var expectedRepository = new ValidatorRepository(world, actionContext); + var expectedDelegatee = expectedRepository.GetValidatorDelegatee( + validatorPrivateKey.Address); + var expectedCommission = GetCommission( + allocatedReward, expectedDelegatee.CommissionPercentage); + var expectedReward = allocatedReward - expectedCommission; + var expectedValidatorBalance = expectedCommission + CalculateReward( + expectedRepository, validatorPrivateKey, validatorPrivateKey, expectedReward); + var expectedDelegatorBalances = CalculateRewards( + expectedRepository, validatorPrivateKey, delegatorPrivateKeys, expectedReward); + var expectedRemainReward = allocatedReward; + expectedRemainReward -= expectedValidatorBalance; + for (var i = 0; i < length; i++) + { + expectedRemainReward -= expectedDelegatorBalances[i]; + } + + var lastCommit = CreateLastCommit(validatorPrivateKey, blockHeight - 1); + actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight++, + Signer = validatorPrivateKey.Address, + LastCommit = lastCommit, + }; + world = new ClaimRewardValidator(validatorPrivateKey.Address).Execute(actionContext); + for (var i = 0; i < length; i++) + { + actionContext = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight++, + Signer = delegatorPrivateKeys[i].Address, + LastCommit = lastCommit, + }; + world = new ClaimRewardValidator(validatorPrivateKey.Address).Execute(actionContext); + } + + // Then + var repository = new ValidatorRepository(world, actionContext); + var delegatee = repository.GetValidatorDelegatee(validatorPrivateKey.Address); + var actualRemainReward = world.GetBalance(delegatee.RewardDistributorAddress, NCG); + var actualValidatorBalance = world.GetBalance(validatorPrivateKey.Address, NCG); + Assert.Equal(expectedRemainReward, actualRemainReward); + Assert.Equal(expectedValidatorBalance, actualValidatorBalance); + + for (var i = 0; i < length; i++) + { + var actualDelegatorBalance = world.GetBalance(delegatorPrivateKeys[i].Address, NCG); + Assert.Equal(expectedDelegatorBalances[i], actualDelegatorBalance); + } + } + + private static FungibleAssetValue CalculateReward( + ValidatorRepository repository, + PrivateKey validatorPrivateKey, + PrivateKey delegatorPrivateKey, + FungibleAssetValue reward) + { + var delegatee = repository.GetValidatorDelegatee(validatorPrivateKey.Address); + var bond = repository.GetBond(delegatee, delegatorPrivateKey.Address); + return CalculateReward(reward, bond.Share, delegatee.TotalShares); + } + + private static FungibleAssetValue[] CalculateRewards( + ValidatorRepository repository, + PrivateKey validatorPrivateKey, + PrivateKey[] delegatorPrivateKeys, + FungibleAssetValue reward) + { + return delegatorPrivateKeys + .Select(item => CalculateReward(repository, validatorPrivateKey, item, reward)) + .ToArray(); + } + + private static FungibleAssetValue CalculateReward( + FungibleAssetValue reward, BigInteger share, BigInteger totalShares) + { + return (reward * share).DivRem(totalShares).Quotient; + } + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs b/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs index 83529d8445..5f1df83ae9 100644 --- a/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs +++ b/.Lib9c.Tests/Action/ValidatorDelegation/ValidatorDelegationTestBase.cs @@ -203,6 +203,51 @@ protected static IWorld EnsureValidatorToBeTombstoned( return slashValidator.Execute(actionContext); } + protected static IWorld EnsureValidatorToBeAllocatedReward( + IWorld world, + PrivateKey validatorPrivateKey, + FungibleAssetValue reward, + ref long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException(nameof(blockHeight)); + } + + var actionContext1 = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight++, + Signer = validatorPrivateKey.Address, + Miner = validatorPrivateKey.Address, + }; + world = new RecordProposer().Execute(actionContext1); + + var lastCommit2 = CreateLastCommit(validatorPrivateKey, blockHeight - 1); + var actionContext2 = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight++, + Signer = validatorPrivateKey.Address, + LastCommit = lastCommit2, + }; + world = world.MintAsset(actionContext2, GoldCurrencyState.Address, reward); + world = world.TransferAsset( + actionContext2, GoldCurrencyState.Address, Addresses.RewardPool, reward); + + var lastCommit3 = CreateLastCommit(validatorPrivateKey, blockHeight - 1); + var actionContext3 = new ActionContext + { + PreviousState = world, + BlockIndex = blockHeight++, + Signer = validatorPrivateKey.Address, + LastCommit = lastCommit3, + }; + world = new AllocateReward().Execute(actionContext3); + + return world; + } + protected static Vote CreateNullVote( PrivateKey privateKey, long blockHeight) { @@ -301,5 +346,19 @@ protected static DuplicateVoteEvidence CreateDuplicateVoteEvidence( return evidence; } + + protected static FungibleAssetValue GetCommission( + FungibleAssetValue fav, BigInteger percentage) + => (fav * percentage).DivRem(100).Quotient; + + protected static FungibleAssetValue GetWithoutCommission( + FungibleAssetValue fav, BigInteger percentage) + => fav - (fav * percentage).DivRem(100).Quotient; + + protected static FungibleAssetValue GetRandomNCG() + { + var value = Math.Round(Random.Shared.Next(1, 100000) / 100.0, 2); + return FungibleAssetValue.Parse(NCG, $"{value:R}"); + } } }