From a37c1d76c4a0fcdf54549af6db7bcac344b04a9d Mon Sep 17 00:00:00 2001 From: s2quake Date: Tue, 9 Apr 2024 14:06:38 +0900 Subject: [PATCH 1/2] feat: Add slashing-related code. --- Lib9c/Action/DPoS/Control/DelegateCtrl.cs | 7 + Lib9c/Action/DPoS/Control/Environment.cs | 30 ++ Lib9c/Action/DPoS/Control/EvidenceCtrl.cs | 97 ++++ Lib9c/Action/DPoS/Control/RedelegateCtrl.cs | 41 +- Lib9c/Action/DPoS/Control/SlashCtrl.cs | 497 ++++++++++++++++++ Lib9c/Action/DPoS/Control/UnbondingSetCtrl.cs | 1 - Lib9c/Action/DPoS/Control/UndelegateCtrl.cs | 62 ++- Lib9c/Action/DPoS/Control/ValidatorCtrl.cs | 88 +++- .../Control/ValidatorDelegationSetCtrl.cs | 2 - .../DPoS/Control/ValidatorPowerIndexCtrl.cs | 15 +- .../DPoS/Control/ValidatorRewardsCtrl.cs | 2 - Lib9c/Action/DPoS/Control/ValidatorSetCtrl.cs | 1 - .../DPoS/Control/ValidatorSigningInfoCtrl.cs | 74 +++ Lib9c/Action/DPoS/Model/Delegation.cs | 9 + Lib9c/Action/DPoS/Model/Evidence.cs | 41 ++ Lib9c/Action/DPoS/Model/Infraction.cs | 20 + Lib9c/Action/DPoS/Model/RedelegationEntry.cs | 24 +- Lib9c/Action/DPoS/Model/UndelegationEntry.cs | 22 +- .../Action/DPoS/Model/ValidatorSigningInfo.cs | 86 +++ Lib9c/Action/DPoS/Unjail.cs | 75 +++ Lib9c/Action/DPoS/Util/DPoSModule.cs | 9 +- 21 files changed, 1180 insertions(+), 23 deletions(-) create mode 100644 Lib9c/Action/DPoS/Control/Environment.cs create mode 100644 Lib9c/Action/DPoS/Control/EvidenceCtrl.cs create mode 100644 Lib9c/Action/DPoS/Control/SlashCtrl.cs create mode 100644 Lib9c/Action/DPoS/Control/ValidatorSigningInfoCtrl.cs create mode 100644 Lib9c/Action/DPoS/Model/Evidence.cs create mode 100644 Lib9c/Action/DPoS/Model/Infraction.cs create mode 100644 Lib9c/Action/DPoS/Model/ValidatorSigningInfo.cs create mode 100644 Lib9c/Action/DPoS/Unjail.cs diff --git a/Lib9c/Action/DPoS/Control/DelegateCtrl.cs b/Lib9c/Action/DPoS/Control/DelegateCtrl.cs index 2f3da3c31d..c637965bcc 100644 --- a/Lib9c/Action/DPoS/Control/DelegateCtrl.cs +++ b/Lib9c/Action/DPoS/Control/DelegateCtrl.cs @@ -25,6 +25,13 @@ internal static class DelegateCtrl return null; } + internal static Delegation? GetDelegation(IWorldState states, Address delegatorAddress, Address validatorAddress) + { + Address delegationAddress = Delegation.DeriveAddress( + delegatorAddress, validatorAddress); + return GetDelegation(states, delegationAddress); + } + internal static (IWorld, Delegation) FetchDelegation( IWorld states, Address delegatorAddress, diff --git a/Lib9c/Action/DPoS/Control/Environment.cs b/Lib9c/Action/DPoS/Control/Environment.cs new file mode 100644 index 0000000000..eec213f2a6 --- /dev/null +++ b/Lib9c/Action/DPoS/Control/Environment.cs @@ -0,0 +1,30 @@ +using System; +using System.Numerics; + +namespace Nekoyume.Action.DPoS.Control +{ + public static class Environment + { + public const long SignedBlocksWindow = 10000; + + public const double MinSignedPerWindow = 0.5; + + public static readonly TimeSpan DowntimeJailDuration = TimeSpan.FromSeconds(60); + + public static readonly BigInteger SlashFractionDoubleSign = new BigInteger(20); // 0.05 + + public static readonly BigInteger SlashFractionDowntime = new BigInteger(10000); // 0.0001 + + public const long ValidatorUpdateDelay = 1; + + public const long MaxAgeNumBlocks = 100000; + + public static readonly TimeSpan MaxAgeDuration = TimeSpan.FromSeconds(172800); // s (즉, 48시간) + + public const long MaxBytes = 1048576; // (즉, 1MB) + + public static readonly DateTimeOffset DoubleSignJailEndTime = DateTimeOffset.FromUnixTimeSeconds(253402300799); + + public const int MissedBlockBitmapChunkSize = 1024; + } +} diff --git a/Lib9c/Action/DPoS/Control/EvidenceCtrl.cs b/Lib9c/Action/DPoS/Control/EvidenceCtrl.cs new file mode 100644 index 0000000000..94e75a172a --- /dev/null +++ b/Lib9c/Action/DPoS/Control/EvidenceCtrl.cs @@ -0,0 +1,97 @@ +#nullable enable +using Nekoyume.Action.DPoS.Exception; +using Nekoyume.Action.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using System.Collections.Immutable; +using Libplanet.Types.Assets; + +namespace Nekoyume.Action.DPoS.Control +{ + internal static class EvidenceCtrl + { + internal static Evidence? GetEvidence(IWorldState states, Address validatorAddress) + { + var address = Evidence.DeriveAddress(validatorAddress); + if (states.GetDPoSState(address) is { } value) + { + return new Evidence(value); + } + + return null; + } + + internal static IWorld SetEvidence(IWorld world, Evidence evidence) + { + var address = Evidence.DeriveAddress(evidence.Address); + var value = evidence.Serialize(); + return world.SetDPoSState(address, value); + } + + internal static IWorld Remove(IWorld world, Evidence evidence) + { + var address = Evidence.DeriveAddress(evidence.Address); + return world.RemoveDPoSState(address); + } + + internal static IWorld Execute( + IWorld world, + IActionContext actionContext, + Address validatorAddress, + Evidence evidence, + IImmutableSet nativeTokens) + { + if (!(ValidatorCtrl.GetValidator(world, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + if (validator.Status == BondingStatus.Unbonded) + { + return world; + } + + var blockHeight = actionContext.BlockIndex; + var infractionHeight = evidence.Height; + var ageBlocks = blockHeight - infractionHeight; + + if (ageBlocks > Environment.MaxAgeNumBlocks) + { + return world; + } + + if (ValidatorCtrl.IsTombstoned(world, validatorAddress)) + { + return world; + } + + var distributionHeight = infractionHeight - Environment.ValidatorUpdateDelay; + var slashFractionDoubleSign = Environment.SlashFractionDoubleSign; + + world = SlashCtrl.SlashWithInfractionReason( + world, + actionContext, + validatorAddress, + distributionHeight, + evidence.Power, + slashFractionDoubleSign, + Infraction.DoubleSign, + nativeTokens + ); + + if (!validator.Jailed) + { + world = ValidatorCtrl.Jail(world, validatorAddress); + } + + world = ValidatorCtrl.JailUntil( + world: world, + validatorAddress: validatorAddress, + blockHeight: long.MaxValue); + world = ValidatorCtrl.Tombstone(world, validatorAddress); + world = Remove(world, evidence); + return world; + } + } +} diff --git a/Lib9c/Action/DPoS/Control/RedelegateCtrl.cs b/Lib9c/Action/DPoS/Control/RedelegateCtrl.cs index 44a5ae58e2..57080c8126 100644 --- a/Lib9c/Action/DPoS/Control/RedelegateCtrl.cs +++ b/Lib9c/Action/DPoS/Control/RedelegateCtrl.cs @@ -1,4 +1,5 @@ #nullable enable +using System; using System.Collections.Generic; using System.Collections.Immutable; using Bencodex.Types; @@ -9,7 +10,6 @@ using Nekoyume.Action.DPoS.Exception; using Nekoyume.Action.DPoS.Misc; using Nekoyume.Action.DPoS.Model; -using Nekoyume.Action.DPoS.Util; using Nekoyume.Module; namespace Nekoyume.Action.DPoS.Control @@ -28,6 +28,45 @@ internal static class RedelegateCtrl return null; } + internal static Redelegation? GetRedelegation( + IWorldState states, + Address delegatorAddress, Address srcValidatorAddress, Address dstValidatorAddress) + { + Address redelegationAddress = Redelegation.DeriveAddress( + delegatorAddress, srcValidatorAddress, dstValidatorAddress); + return GetRedelegation(states, redelegationAddress); + } + + internal static Redelegation[] GetRedelegationsByDelegator(IWorldState worldState, Address delegatorAddress) + { + var redelegationList = new List(); + var unbondingSet = UnbondingSetCtrl.GetUnbondingSet(worldState)!; + foreach (var item in unbondingSet.RedelegationAddressSet) + { + if (GetRedelegation(worldState, item) is not { } redelegation) + { + throw new InvalidOperationException("undelegation is null."); + } + + if (redelegation.DelegatorAddress.Equals(delegatorAddress)) + { + redelegationList.Add(redelegation); + } + } + + return redelegationList.ToArray(); + } + + internal static RedelegationEntry? GetRedelegationEntry(IWorldState worldState, Address redelegationEntryAddress) + { + if (worldState.GetDPoSState(redelegationEntryAddress) is { } value) + { + return new RedelegationEntry(value); + } + + return null; + } + internal static (IWorld, Redelegation) FetchRedelegation( IWorld states, Address delegatorAddress, diff --git a/Lib9c/Action/DPoS/Control/SlashCtrl.cs b/Lib9c/Action/DPoS/Control/SlashCtrl.cs new file mode 100644 index 0000000000..c2ea90d5ec --- /dev/null +++ b/Lib9c/Action/DPoS/Control/SlashCtrl.cs @@ -0,0 +1,497 @@ +#nullable enable +using System.Numerics; +using Bencodex.Types; +using Nekoyume.Action.DPoS.Exception; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; +using System; +using System.Linq; +using System.Collections.Immutable; + +namespace Nekoyume.Action.DPoS.Control +{ + internal static class SlashCtrl + { + public static IWorld Slash( + IWorld world, + IActionContext actionContext, + Address validatorAddress, + long infractionHeight, + BigInteger power, + BigInteger slashFactor, + IImmutableSet nativeTokens) + { + if (slashFactor < 0) + { + throw new ArgumentOutOfRangeException(nameof(slashFactor), "Slash factor must be greater than or equal to 0."); + } + + if (!(ValidatorCtrl.GetValidator(world, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + var amount = FungibleAssetValue.FromRawValue(Asset.ConsensusToken, power); + var (slashAmount, r) = amount.DivRem(slashFactor); + + if (validator.Status == BondingStatus.Unbonded) + { + throw new InvalidOperationException("should not be slashing unbonded validator"); + } + + var remainingSlashAmount = slashAmount; + + if (infractionHeight > actionContext.BlockIndex) + { + throw new ArgumentOutOfRangeException( + "impossible attempt to slash future infraction", + nameof(infractionHeight)); + } + else if (infractionHeight < actionContext.BlockIndex) + { + world = SlashUndelegations(world, actionContext, validatorAddress, + infractionHeight, slashFactor, ref remainingSlashAmount); + world = SlashRedelegations(world, actionContext, validatorAddress, + infractionHeight, slashFactor, nativeTokens, ref remainingSlashAmount); + } + + if (ValidatorCtrl.ConsensusTokenFromShare(world, validatorAddress, validator.DelegatorShares) is not { } consensusToken) + { + throw new InvalidOperationException(); + } + + var tokensToBurn = Min(remainingSlashAmount, consensusToken); + + if (tokensToBurn.RawValue == 0) + { + return world; + } + + world = ValidatorCtrl.RemoveValidatorTokens(world, actionContext, validatorAddress, tokensToBurn); + + world = validator.Status switch + { + BondingStatus.Bonded + => BurnBondedTokens(world, actionContext, amount: tokensToBurn), + BondingStatus.Unbonding or BondingStatus.Unbonded + => BurnNotBondedTokens(world, actionContext, amount: tokensToBurn), + _ + => throw new InvalidOperationException("Invalid validator status"), + }; + + return world; + } + + public static IWorld SlashWithInfractionReason( + IWorld world, + IActionContext actionContext, + Address validatorAddress, + long infractionHeight, + BigInteger power, + BigInteger slashFractionDowntime, + Infraction infraction, + IImmutableSet nativeTokens) + { + return Slash(world, actionContext, validatorAddress, infractionHeight, power, slashFractionDowntime, nativeTokens); + } + + public static IWorld Execute( + IWorld world, + IActionContext actionContext, + Address operatorAddress, + BigInteger power, + bool signed, + IImmutableSet nativeTokens) + { + var height = actionContext.BlockIndex; + var validatorAddress = Validator.DeriveAddress(operatorAddress); + + if (!(ValidatorCtrl.GetValidator(world, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + if (validator.Jailed) + { + return world; + } + + if (!(ValidatorSigningInfoCtrl.GetSigningInfo(world, validatorAddress) is { } signInfo)) + { + throw new NullValidatorException(validatorAddress); + } + + var signedBlocksWindow = Environment.SignedBlocksWindow; + + var index = signInfo.IndexOffset % signedBlocksWindow; + signInfo.IndexOffset++; + + var previous = GetMissedBlockBitmapValue(world, validatorAddress, index); + var missed = signed; + if (!previous && missed) + { + SetMissedBlockBitmapValue(world, validatorAddress, index, true); + signInfo.MissedBlocksCounter++; + } + else if (previous && !missed) + { + SetMissedBlockBitmapValue(world, validatorAddress, index, false); + signInfo.MissedBlocksCounter--; + } + + var minSignedPerWindow = Environment.MinSignedPerWindow; + var minHeight = signInfo.StartHeight + signedBlocksWindow; + var maxMissed = signedBlocksWindow - minSignedPerWindow; + + if (height > minHeight && signInfo.MissedBlocksCounter > maxMissed) + { + if (!validator.Jailed) + { + var distributionHeight = height - Environment.ValidatorUpdateDelay - 1; + var slashFractionDowntime = Environment.SlashFractionDowntime; + world = SlashCtrl.SlashWithInfractionReason( + world, + actionContext, + validatorAddress, + distributionHeight, + power, + slashFractionDowntime, + Infraction.Downtime, + nativeTokens + ); + world = ValidatorCtrl.Jail(world, validatorAddress); + + var downtimeJailDur = Environment.DowntimeJailDuration; + // signInfo.JailedUntil = blockContext.Timestamp + downtimeJailDur; + signInfo.MissedBlocksCounter = 0; + signInfo.IndexOffset = 0; + DeleteMissedBlockBitmap(world); + } + } + + return ValidatorSigningInfoCtrl.SetSigningInfo(world, signInfo); + } + + public static IWorld Unjail( + IWorld world, + IActionContext actionContext, + Address validatorAddress + ) + { + if (!(ValidatorCtrl.GetValidator(world, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + if (!validator.Jailed) + { + throw new InvalidOperationException("validator is not jailed"); + } + + var signingInfo = ValidatorSigningInfoCtrl.GetSigningInfo(world, validatorAddress); + if (signingInfo is null) + { + throw new NullValidatorException(validatorAddress); + } + + if (signingInfo.Tombstoned) + { + throw new InvalidOperationException("validator is tombstoned"); + } + + if (actionContext.BlockIndex < signingInfo.JailedUntil) + { + throw new InvalidOperationException("validator is still jailed"); + } + + var consensusToken = world.GetBalance(validatorAddress, Asset.ConsensusToken); + if (consensusToken < Validator.MinSelfDelegation) + { + throw new InvalidOperationException("validator has insufficient self-delegation"); + } + + var operatorAddress = validator.OperatorAddress; + var delegationAddress = Delegation.DeriveAddress(operatorAddress, validator.Address); + if (!(DelegateCtrl.GetDelegation(world, delegationAddress) is { })) + { + throw new NullDelegationException(delegationAddress); + } + + return ValidatorCtrl.Unjail(world, validatorAddress); + } + + private static IWorld SlashUnbondingDelegation( + IWorld world, + IActionContext actionContext, + Undelegation undelegation, + long infractionHeight, + BigInteger slashFactor, + out FungibleAssetValue amountSlashed) + { + var totalSlashAmount = new FungibleAssetValue(Asset.ConsensusToken); + var burnedAmount = new FungibleAssetValue(Asset.ConsensusToken); + +#pragma warning disable LAA1002 + foreach (var (index, entryAddress) in undelegation.UndelegationEntryAddresses) + { + var entryValue = world.GetDPoSState(entryAddress)!; + var entry = new UndelegationEntry(entryValue); + + if (entry.CreationHeight < infractionHeight) + { + continue; + } + if (entry.IsMatured(infractionHeight)) + { + continue; + } + + var (q, r) = entry.InitialConsensusToken.DivRem(slashFactor); + var slashAmount = q; + totalSlashAmount += slashAmount; + + var unbondingSlashAmount = Min(slashAmount, entry.UnbondingConsensusToken); + burnedAmount += unbondingSlashAmount; + entry.UnbondingConsensusToken -= unbondingSlashAmount; + world = world.SetDPoSState(entry.Address, entry.Serialize()); + } +#pragma warning restore LAA1002 + + if (burnedAmount.RawValue > 0) + { + world = BurnNotBondedTokens(world, actionContext, amount: burnedAmount); + } + + amountSlashed = burnedAmount; + return world; + } + + private static IWorld SlashRedelegation( + IWorld world, + IActionContext actionContext, + Redelegation redelegation, + long infractionHeight, + BigInteger slashFactor, + IImmutableSet nativeTokens, + out FungibleAssetValue amountSlashed) + { + var totalSlashAmount = new FungibleAssetValue(Asset.ConsensusToken); + var bondedBurnedAmount = new FungibleAssetValue(Asset.ConsensusToken); + var notBondedBurnedAmount = new FungibleAssetValue(Asset.ConsensusToken); + + var valDstAddr = redelegation.DstValidatorAddress; + var delegatorAddress = redelegation.DelegatorAddress; + +#pragma warning disable LAA1002 + foreach (var (index, entryAddress) in redelegation.RedelegationEntryAddresses) + { + var entryValue = world.GetDPoSState(entryAddress)!; + var entry = new RedelegationEntry(entryValue); + + if (entry.CreationHeight < infractionHeight) + { + continue; + } + if (entry.IsMatured(infractionHeight)) + { + continue; + } + + var (slashAmount, _) = entry.InitialConsensusToken.DivRem(slashFactor); + totalSlashAmount += slashAmount; + + var (sharesToUnbond, _) = entry.RedelegatingShare.DivRem(slashFactor); + if (sharesToUnbond.RawValue == 0) + { + continue; + } + + var delegationAddress = Delegation.DeriveAddress(delegatorAddress, valDstAddr); + var delegation = DelegateCtrl.GetDelegation(world, delegationAddress)!; + + if (sharesToUnbond > delegation.GetShares(world)) + { + sharesToUnbond = delegation.GetShares(world); + } + + FungibleAssetValue tokensToBurn; + (world, tokensToBurn) = Bond.Cancel(world, actionContext, sharesToUnbond, valDstAddr, delegationAddress, nativeTokens); + + if (ValidatorCtrl.GetValidator(world, valDstAddr) is not { } dstValidator) + { + throw new NullValidatorException(valDstAddr); + } + + if (dstValidator.Status == BondingStatus.Bonded) + { + bondedBurnedAmount += tokensToBurn; + } + else if (dstValidator.Status == BondingStatus.Unbonding || dstValidator.Status == BondingStatus.Unbonded) + { + notBondedBurnedAmount += tokensToBurn; + } + else + { + throw new InvalidOperationException("unknown validator status"); + } + + if (bondedBurnedAmount.RawValue > 0) + { + world = BurnBondedTokens(world, actionContext, amount: bondedBurnedAmount); + } + + if (notBondedBurnedAmount.RawValue > 0) + { + world = BurnNotBondedTokens(world, actionContext, amount: notBondedBurnedAmount); + } + } +#pragma warning restore LAA1002 + + amountSlashed = totalSlashAmount; + return world; + } + + private static FungibleAssetValue Min(FungibleAssetValue f1, FungibleAssetValue f2) + { + return f1 < f2 ? f1 : f2; + } + + private static IWorld SlashUndelegations( + IWorld world, + IActionContext actionContext, + Address validatorAddress, + long infractionHeight, + BigInteger slashFactor, + ref FungibleAssetValue remainingSlashAmount) + { + var unbondingSet = UnbondingSetCtrl.GetUnbondingSet(world)!; + foreach (var item in unbondingSet.UndelegationAddressSet) + { + var undelegation = UndelegateCtrl.GetUndelegation(world, item)!; + if (undelegation.ValidatorAddress == validatorAddress) + { + world = SlashUnbondingDelegation(world, actionContext, undelegation, infractionHeight, slashFactor, out var amountSlashed); + remainingSlashAmount -= amountSlashed; + } + } + + return world; + } + + private static IWorld SlashRedelegations( + IWorld world, + IActionContext actionContext, + Address validatorAddress, + long infractionHeight, + BigInteger slashFactor, + IImmutableSet nativeTokens, + ref FungibleAssetValue remainingSlashAmount) + { + var unbondingSet = UnbondingSetCtrl.GetUnbondingSet(world)!; + foreach (var item in unbondingSet.RedelegationAddressSet) + { + var redelegation = RedelegateCtrl.GetRedelegation(world, item)!; + if (redelegation.SrcValidatorAddress == validatorAddress) + { + world = SlashRedelegation(world, actionContext, redelegation, infractionHeight, slashFactor, nativeTokens, out var amountSlashed); + remainingSlashAmount -= amountSlashed; + } + } + + return world; + } + + private static bool GetMissedBlockBitmapValue( + IWorld world, + Address validatorAddress, + long index) + { + var chunkIndex = index / Environment.MissedBlockBitmapChunkSize; + var chunk = GetMissedBlockBitmapChunk(world, validatorAddress, chunkIndex); + if (chunk == null) + { + return false; + } + + var bitIndex = index % Environment.MissedBlockBitmapChunkSize; + return chunk[bitIndex] == 1; + } + + private static IWorld SetMissedBlockBitmapValue( + IWorld world, + Address validatorAddress, + long index, + bool missed) + { + var chunkIndex = index / Environment.MissedBlockBitmapChunkSize; + var chunk = GetMissedBlockBitmapChunk(world, validatorAddress, chunkIndex) ?? new byte[Environment.MissedBlockBitmapChunkSize]; + var bitIndex = index % Environment.MissedBlockBitmapChunkSize; + chunk[bitIndex] = (byte)(missed ? 1 : 0); + + return SetMissedBlockBitmapChunk(world, validatorAddress, chunkIndex, chunk); + } + + private static IWorld DeleteMissedBlockBitmap(IWorld world) + { + return world; + } + + private static byte[]? GetMissedBlockBitmapChunk( + IWorld world, + Address validatorAddress, + long chunkIndex) + { + var address = validatorAddress.Derive($"{chunkIndex}"); + if (world.GetDPoSState(address) is Binary binary) + { + return binary.ByteArray.ToArray(); + } + return null; + } + + private static IWorld SetMissedBlockBitmapChunk( + IWorld world, + Address validatorAddress, + long chunkIndex, + byte[] chunk) + { + var address = validatorAddress.Derive($"{chunkIndex}"); + return world.SetDPoSState(address, new Binary(chunk.ToArray())); + } + + private static IWorld BurnBondedTokens( + IWorld world, + IActionContext actionContext, + FungibleAssetValue amount) + { + if (!amount.Currency.Equals(Asset.ConsensusToken)) + { + throw new Exception.InvalidCurrencyException(Asset.ConsensusToken, amount.Currency); + } + + var tokensToBurn = Asset.GovernanceFromConsensus(amount); + return world.TransferAsset(actionContext, ReservedAddress.BondedPool, ReservedAddress.CommunityPool, tokensToBurn); + // return world.BurnAsset(actionContext, ReservedAddress.BondedPool, tokensToBurn); + } + + private static IWorld BurnNotBondedTokens( + IWorld world, + IActionContext actionContext, + FungibleAssetValue amount) + { + if (!amount.Currency.Equals(Asset.ConsensusToken)) + { + throw new Exception.InvalidCurrencyException(Asset.ConsensusToken, amount.Currency); + } + + var tokensToBurn = Asset.GovernanceFromConsensus(amount); + return world.TransferAsset(actionContext, ReservedAddress.UnbondedPool, ReservedAddress.CommunityPool, tokensToBurn); + // return world.BurnAsset(actionContext, ReservedAddress.UnbondedPool, tokensToBurn); + } + } +} diff --git a/Lib9c/Action/DPoS/Control/UnbondingSetCtrl.cs b/Lib9c/Action/DPoS/Control/UnbondingSetCtrl.cs index 37b01f1c0a..e372052f74 100644 --- a/Lib9c/Action/DPoS/Control/UnbondingSetCtrl.cs +++ b/Lib9c/Action/DPoS/Control/UnbondingSetCtrl.cs @@ -4,7 +4,6 @@ using Libplanet.Crypto; using Nekoyume.Action.DPoS.Misc; using Nekoyume.Action.DPoS.Model; -using Nekoyume.Action.DPoS.Util; namespace Nekoyume.Action.DPoS.Control { diff --git a/Lib9c/Action/DPoS/Control/UndelegateCtrl.cs b/Lib9c/Action/DPoS/Control/UndelegateCtrl.cs index bc0a68979f..a42c0b3450 100644 --- a/Lib9c/Action/DPoS/Control/UndelegateCtrl.cs +++ b/Lib9c/Action/DPoS/Control/UndelegateCtrl.cs @@ -1,4 +1,5 @@ #nullable enable +using System; using System.Collections.Generic; using System.Collections.Immutable; using Bencodex.Types; @@ -9,13 +10,22 @@ using Nekoyume.Action.DPoS.Exception; using Nekoyume.Action.DPoS.Misc; using Nekoyume.Action.DPoS.Model; -using Nekoyume.Action.DPoS.Util; using Nekoyume.Module; namespace Nekoyume.Action.DPoS.Control { internal static class UndelegateCtrl { + internal static Undelegation? GetUndelegation( + IWorldState state, + Address delegatorAddress, + Address validatorAddress) + { + Address undelegationAddress = Undelegation.DeriveAddress( + delegatorAddress, validatorAddress); + return GetUndelegation(state, undelegationAddress); + } + internal static Undelegation? GetUndelegation( IWorldState state, Address undelegationAddress) @@ -28,6 +38,56 @@ internal static class UndelegateCtrl return null; } + internal static Undelegation[] GetUndelegations(IWorldState worldState, Address validatorAddress) + { + var undelegationList = new List(); + var unbondingSet = UnbondingSetCtrl.GetUnbondingSet(worldState)!; + foreach (var item in unbondingSet.UndelegationAddressSet) + { + if (GetUndelegation(worldState, item) is not { } undelegation) + { + throw new InvalidOperationException("undelegation is null."); + } + + if (undelegation.ValidatorAddress.Equals(validatorAddress)) + { + undelegationList.Add(undelegation); + } + } + + return undelegationList.ToArray(); + } + + internal static Undelegation[] GetUndelegationsByDelegator(IWorldState worldState, Address delegatorAddress) + { + var undelegationList = new List(); + var unbondingSet = UnbondingSetCtrl.GetUnbondingSet(worldState)!; + foreach (var item in unbondingSet.UndelegationAddressSet) + { + if (GetUndelegation(worldState, item) is not { } undelegation) + { + throw new InvalidOperationException("undelegation is null."); + } + + if (undelegation.DelegatorAddress.Equals(delegatorAddress)) + { + undelegationList.Add(undelegation); + } + } + + return undelegationList.ToArray(); + } + + internal static UndelegationEntry? GetUndelegationEntry(IWorldState worldState, Address undelegationEntryAddress) + { + if (worldState.GetDPoSState(undelegationEntryAddress) is { } value) + { + return new UndelegationEntry(value); + } + + return null; + } + internal static (IWorld, Undelegation) FetchUndelegation( IWorld state, Address delegatorAddress, diff --git a/Lib9c/Action/DPoS/Control/ValidatorCtrl.cs b/Lib9c/Action/DPoS/Control/ValidatorCtrl.cs index 36536cea6f..ea4290db82 100644 --- a/Lib9c/Action/DPoS/Control/ValidatorCtrl.cs +++ b/Lib9c/Action/DPoS/Control/ValidatorCtrl.cs @@ -1,5 +1,7 @@ #nullable enable +using System; using System.Collections.Immutable; +using System.Linq; using Libplanet.Action; using Libplanet.Action.State; using Libplanet.Crypto; @@ -7,7 +9,6 @@ using Nekoyume.Action.DPoS.Exception; using Nekoyume.Action.DPoS.Misc; using Nekoyume.Action.DPoS.Model; -using Nekoyume.Action.DPoS.Util; using Nekoyume.Module; namespace Nekoyume.Action.DPoS.Control @@ -44,6 +45,9 @@ internal static (IWorld, Validator) FetchValidator( { validator = new Validator(operatorAddress, operatorPublicKey); states = states.SetDPoSState(validator.Address, validator.Serialize()); + states = ValidatorSigningInfoCtrl.SetSigningInfo( + states, + new ValidatorSigningInfo { Address = validator.Address }); } return (states, validator); @@ -86,12 +90,23 @@ internal static IWorld Create( governanceToken, nativeTokens); + Address delegationAddress = Delegation.DeriveAddress( + operatorAddress, validator.Address); + if (DelegateCtrl.GetDelegation(states, delegationAddress) is { } delegation) + { + states = ValidatorDelegationSetCtrl.Add( + states: states, + validatorAddress: validator.Address, + delegationAddress: delegationAddress + ); + } + // Does not save current instance, since it's done on delegation return states; } internal static FungibleAssetValue? ShareFromConsensusToken( - IWorld states, Address validatorAddress, FungibleAssetValue consensusToken) + IWorldState states, Address validatorAddress, FungibleAssetValue consensusToken) { if (!(GetValidator(states, validatorAddress) is { } validator)) { @@ -263,5 +278,74 @@ internal static IWorld Complete( return states; } + + internal static FungibleAssetValue GetPower(IWorldState worldState, Address validatorAddress) + { + if (ValidatorPowerIndexCtrl.GetValidatorPowerIndex(worldState) is { } powerIndex) + { + return powerIndex.Index.First(item => item.ValidatorAddress == validatorAddress).ConsensusToken; + } + + throw new ArgumentException("Validator power index not found.", nameof(validatorAddress)); + } + + internal static IWorld JailUntil(IWorld world, Address validatorAddress, long blockHeight) + => ValidatorSigningInfoCtrl.JailUntil(world, validatorAddress, blockHeight); + + internal static IWorld Tombstone(IWorld world, Address validatorAddress) + => ValidatorSigningInfoCtrl.Tombstone(world, validatorAddress); + + internal static bool IsTombstoned(IWorld world, Address validatorAddress) + => ValidatorSigningInfoCtrl.IsTombstoned(world, validatorAddress); + + internal static IWorld Jail(IWorld world, Address validatorAddress) + { + if (!(GetValidator(world, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + if (validator.Jailed) + { + throw new JailedValidatorException(validator.Address); + } + + validator.Jailed = true; + world = world.SetDPoSState(validator.Address, validator.Serialize()); + world = ValidatorPowerIndexCtrl.Remove(world, validator.Address); + return world; + } + + internal static IWorld Unjail(IWorld world, Address validatorAddress) + { + if (!(GetValidator(world, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + if (!validator.Jailed) + { + throw new JailedValidatorException(validator.Address); + } + + validator.Jailed = false; + world = world.SetDPoSState(validator.Address, validator.Serialize()); + world = ValidatorPowerIndexCtrl.Update(world, validator.Address); + return world; + } + + internal static IWorld RemoveValidatorTokens( + IWorld world, + IActionContext actionContext, + Address validatorAddress, + FungibleAssetValue tokensToRemove) + { + if (GetValidator(world, validatorAddress) is not { } validator) + { + throw new NullValidatorException(validatorAddress); + } + + world = world.BurnAsset(actionContext, validatorAddress, tokensToRemove); + world = ValidatorPowerIndexCtrl.Update(world, validatorAddress); + return world; + } } } diff --git a/Lib9c/Action/DPoS/Control/ValidatorDelegationSetCtrl.cs b/Lib9c/Action/DPoS/Control/ValidatorDelegationSetCtrl.cs index 412a83574f..888b999dd3 100644 --- a/Lib9c/Action/DPoS/Control/ValidatorDelegationSetCtrl.cs +++ b/Lib9c/Action/DPoS/Control/ValidatorDelegationSetCtrl.cs @@ -1,9 +1,7 @@ #nullable enable using Libplanet.Action.State; using Libplanet.Crypto; -using Nekoyume.Action.DPoS.Misc; using Nekoyume.Action.DPoS.Model; -using Nekoyume.Action.DPoS.Util; namespace Nekoyume.Action.DPoS.Control { diff --git a/Lib9c/Action/DPoS/Control/ValidatorPowerIndexCtrl.cs b/Lib9c/Action/DPoS/Control/ValidatorPowerIndexCtrl.cs index d78e669309..be8fc07de7 100644 --- a/Lib9c/Action/DPoS/Control/ValidatorPowerIndexCtrl.cs +++ b/Lib9c/Action/DPoS/Control/ValidatorPowerIndexCtrl.cs @@ -6,7 +6,6 @@ using Nekoyume.Action.DPoS.Exception; using Nekoyume.Action.DPoS.Misc; using Nekoyume.Action.DPoS.Model; -using Nekoyume.Action.DPoS.Util; using Nekoyume.Module; namespace Nekoyume.Action.DPoS.Control @@ -76,5 +75,19 @@ internal static IWorld Update(IWorld states, IEnumerable
validatorAddre return states; } + + internal static IWorld Remove(IWorld states, Address validatorAddress) + { + ValidatorPowerIndex validatorPowerIndex; + (states, validatorPowerIndex) = FetchValidatorPowerIndex(states); + var index = validatorPowerIndex.Index.RemoveWhere( + key => key.ValidatorAddress.Equals(validatorAddress)); + if (index < 0) + { + throw new NullValidatorException(validatorAddress); + } + states = states.SetDPoSState(validatorPowerIndex.Address, validatorPowerIndex.Serialize()); + return states; + } } } diff --git a/Lib9c/Action/DPoS/Control/ValidatorRewardsCtrl.cs b/Lib9c/Action/DPoS/Control/ValidatorRewardsCtrl.cs index e85187f8e9..a9bc7c14ab 100644 --- a/Lib9c/Action/DPoS/Control/ValidatorRewardsCtrl.cs +++ b/Lib9c/Action/DPoS/Control/ValidatorRewardsCtrl.cs @@ -4,9 +4,7 @@ using Libplanet.Action.State; using Libplanet.Crypto; using Libplanet.Types.Assets; -using Nekoyume.Action.DPoS.Misc; using Nekoyume.Action.DPoS.Model; -using Nekoyume.Action.DPoS.Util; namespace Nekoyume.Action.DPoS.Control { diff --git a/Lib9c/Action/DPoS/Control/ValidatorSetCtrl.cs b/Lib9c/Action/DPoS/Control/ValidatorSetCtrl.cs index 1a7f3f63f3..3440bf5f6d 100644 --- a/Lib9c/Action/DPoS/Control/ValidatorSetCtrl.cs +++ b/Lib9c/Action/DPoS/Control/ValidatorSetCtrl.cs @@ -6,7 +6,6 @@ using Nekoyume.Action.DPoS.Exception; using Nekoyume.Action.DPoS.Misc; using Nekoyume.Action.DPoS.Model; -using Nekoyume.Action.DPoS.Util; using Nekoyume.Module; namespace Nekoyume.Action.DPoS.Control diff --git a/Lib9c/Action/DPoS/Control/ValidatorSigningInfoCtrl.cs b/Lib9c/Action/DPoS/Control/ValidatorSigningInfoCtrl.cs new file mode 100644 index 0000000000..2dacd4170e --- /dev/null +++ b/Lib9c/Action/DPoS/Control/ValidatorSigningInfoCtrl.cs @@ -0,0 +1,74 @@ +#nullable enable +using Nekoyume.Action.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Nekoyume.Action.DPoS.Exception; +using System; + +namespace Nekoyume.Action.DPoS.Control +{ + internal static class ValidatorSigningInfoCtrl + { + internal static ValidatorSigningInfo? GetSigningInfo(IWorldState states, Address validatorAddress) + { + var address = ValidatorSigningInfo.DeriveAddress(validatorAddress); + if (states.GetDPoSState(address) is { } value) + { + return new ValidatorSigningInfo(value); + } + + return null; + } + + internal static IWorld SetSigningInfo(IWorld world, ValidatorSigningInfo signingInfo) + { + var address = ValidatorSigningInfo.DeriveAddress(signingInfo.Address); + var value = signingInfo.Serialize(); + return world.SetDPoSState(address, value); + } + + + internal static IWorld Tombstone(IWorld world, Address validatorAddress) + { + if (!(GetSigningInfo(world, validatorAddress) is { } signInfo)) + { + throw new NullValidatorException(validatorAddress); + } + if (signInfo.Tombstoned) + { + throw new InvalidOperationException(); + } + + signInfo.Tombstoned = true; + return SetSigningInfo(world, signInfo); + } + + internal static bool IsTombstoned(IWorld world, Address validatorAddress) + { + if (!(GetSigningInfo(world, validatorAddress) is { } signInfo)) + { + throw new NullValidatorException(validatorAddress); + } + + return signInfo.Tombstoned; + } + + internal static IWorld JailUntil(IWorld world, Address validatorAddress, long blockHeight) + { + if (blockHeight < 0) + { + throw new ArgumentOutOfRangeException( + paramName: nameof(blockHeight), + message: "blockHeight must be greater than or equal to 0."); + } + + if (!(GetSigningInfo(world, validatorAddress) is { } signInfo)) + { + throw new NullValidatorException(validatorAddress); + } + + signInfo.JailedUntil = blockHeight; + return SetSigningInfo(world, signInfo); + } + } +} diff --git a/Lib9c/Action/DPoS/Model/Delegation.cs b/Lib9c/Action/DPoS/Model/Delegation.cs index 8a30f2cb75..a204ab5629 100644 --- a/Lib9c/Action/DPoS/Model/Delegation.cs +++ b/Lib9c/Action/DPoS/Model/Delegation.cs @@ -1,9 +1,13 @@ #nullable enable using System; using Bencodex.Types; +using Libplanet.Action.State; using Libplanet.Common; using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Action.DPoS.Misc; using Nekoyume.Action.DPoS.Util; +using Nekoyume.Module; namespace Nekoyume.Action.DPoS.Model { @@ -42,6 +46,11 @@ public Delegation(Delegation delegation) public long LatestDistributeHeight { get; set; } + public FungibleAssetValue GetShares(IWorldState worldState) + { + return worldState.GetBalance(Address, Asset.Share); + } + public static bool operator ==(Delegation obj, Delegation other) { return obj.Equals(other); diff --git a/Lib9c/Action/DPoS/Model/Evidence.cs b/Lib9c/Action/DPoS/Model/Evidence.cs new file mode 100644 index 0000000000..bb0564b25d --- /dev/null +++ b/Lib9c/Action/DPoS/Model/Evidence.cs @@ -0,0 +1,41 @@ +using System.Numerics; +using Bencodex.Types; +using Nekoyume.Action.DPoS.Util; +using Libplanet.Crypto; + +namespace Nekoyume.Action.DPoS.Model +{ + public class Evidence + { + public Evidence() + { + } + + public Evidence(IValue serialized) + { + var dict = (Bencodex.Types.Dictionary)serialized; + Height = dict["height"].ToLong(); + Power = dict["power"].ToBigInteger(); + Address = dict["address"].ToAddress(); + } + + public long Height { get; set; } + + public BigInteger Power { get; set; } + + public Address Address { get; set; } + + public static Address DeriveAddress(Address validatorAddress) + { + return validatorAddress.Derive(nameof(Evidence)); + } + + public IValue Serialize() + { + return Dictionary.Empty + .Add("height", Height.Serialize()) + .Add("power", Power.Serialize()) + .Add("address", Address.Serialize()); + } + } +} diff --git a/Lib9c/Action/DPoS/Model/Infraction.cs b/Lib9c/Action/DPoS/Model/Infraction.cs new file mode 100644 index 0000000000..f694b0f060 --- /dev/null +++ b/Lib9c/Action/DPoS/Model/Infraction.cs @@ -0,0 +1,20 @@ +namespace Nekoyume.Action.DPoS.Model +{ + public enum Infraction : byte + { + /// + /// an empty infraction. + /// + Unspecified = 0, + + /// + /// a validator that double-signs a block. + /// + DoubleSign = 1, + + /// + /// a validator that missed signing too many blocks. + /// + Downtime = 2, + } +} diff --git a/Lib9c/Action/DPoS/Model/RedelegationEntry.cs b/Lib9c/Action/DPoS/Model/RedelegationEntry.cs index d2ae175931..dfbe2fc8eb 100644 --- a/Lib9c/Action/DPoS/Model/RedelegationEntry.cs +++ b/Lib9c/Action/DPoS/Model/RedelegationEntry.cs @@ -26,10 +26,12 @@ public RedelegationEntry( Address = DeriveAddress(redelegationAddress, index); RedelegationAddress = redelegationAddress; RedelegatingShare = redelegatingShare; + InitialConsensusToken = unbondingConsensusToken; UnbondingConsensusToken = unbondingConsensusToken; IssuedShare = issuedShare; Index = index; CompletionBlockHeight = blockHeight + UnbondingSet.Period; + CreationHeight = blockHeight; } public RedelegationEntry(IValue serialized) @@ -38,10 +40,12 @@ public RedelegationEntry(IValue serialized) Address = serializedList[0].ToAddress(); RedelegationAddress = serializedList[1].ToAddress(); RedelegatingShare = serializedList[2].ToFungibleAssetValue(); - UnbondingConsensusToken = serializedList[3].ToFungibleAssetValue(); - IssuedShare = serializedList[4].ToFungibleAssetValue(); - Index = serializedList[5].ToLong(); - CompletionBlockHeight = serializedList[6].ToLong(); + InitialConsensusToken = serializedList[3].ToFungibleAssetValue(); + UnbondingConsensusToken = serializedList[4].ToFungibleAssetValue(); + IssuedShare = serializedList[5].ToFungibleAssetValue(); + Index = serializedList[6].ToLong(); + CompletionBlockHeight = serializedList[7].ToLong(); + CreationHeight = serializedList[8].ToLong(); } public Address Address { get; set; } @@ -62,6 +66,8 @@ public FungibleAssetValue RedelegatingShare } } + public FungibleAssetValue InitialConsensusToken { get; set; } + public FungibleAssetValue UnbondingConsensusToken { get => _unbondingConsensusToken; @@ -92,6 +98,8 @@ public FungibleAssetValue IssuedShare public long Index { get; set; } + public long CreationHeight { get; set; } + public long CompletionBlockHeight { get; set; } public static bool operator ==(RedelegationEntry obj, RedelegationEntry other) @@ -117,10 +125,12 @@ public IValue Serialize() .Add(Address.Serialize()) .Add(RedelegationAddress.Serialize()) .Add(RedelegatingShare.Serialize()) + .Add(InitialConsensusToken.Serialize()) .Add(UnbondingConsensusToken.Serialize()) .Add(IssuedShare.Serialize()) .Add(Index.Serialize()) - .Add(CompletionBlockHeight.Serialize()); + .Add(CompletionBlockHeight.Serialize()) + .Add(CreationHeight.Serialize()); } public override bool Equals(object? obj) @@ -134,10 +144,12 @@ public bool Equals(RedelegationEntry? other) Address.Equals(other.Address) && RedelegationAddress.Equals(other.RedelegationAddress) && RedelegatingShare.Equals(other.RedelegatingShare) && + InitialConsensusToken.Equals(other.InitialConsensusToken) && UnbondingConsensusToken.Equals(other.UnbondingConsensusToken) && IssuedShare.Equals(other.IssuedShare) && Index == other.Index && - CompletionBlockHeight == other.CompletionBlockHeight; + CompletionBlockHeight == other.CompletionBlockHeight && + CreationHeight == other.CreationHeight; } public override int GetHashCode() diff --git a/Lib9c/Action/DPoS/Model/UndelegationEntry.cs b/Lib9c/Action/DPoS/Model/UndelegationEntry.cs index 5b379e59e6..2130e14942 100644 --- a/Lib9c/Action/DPoS/Model/UndelegationEntry.cs +++ b/Lib9c/Action/DPoS/Model/UndelegationEntry.cs @@ -21,9 +21,11 @@ public UndelegationEntry( { Address = DeriveAddress(undelegationAddress, index); UndelegationAddress = undelegationAddress; + InitialConsensusToken = unbondingConsensusToken; UnbondingConsensusToken = unbondingConsensusToken; Index = index; CompletionBlockHeight = blockHeight + UnbondingSet.Period; + CreationHeight = blockHeight; } public UndelegationEntry(IValue serialized) @@ -31,15 +33,19 @@ public UndelegationEntry(IValue serialized) List serializedList = (List)serialized; Address = serializedList[0].ToAddress(); UndelegationAddress = serializedList[1].ToAddress(); - UnbondingConsensusToken = serializedList[2].ToFungibleAssetValue(); - Index = serializedList[3].ToLong(); - CompletionBlockHeight = serializedList[4].ToLong(); + InitialConsensusToken = serializedList[2].ToFungibleAssetValue(); + UnbondingConsensusToken = serializedList[3].ToFungibleAssetValue(); + Index = serializedList[4].ToLong(); + CompletionBlockHeight = serializedList[5].ToLong(); + CreationHeight = serializedList[6].ToLong(); } public Address Address { get; set; } public Address UndelegationAddress { get; set; } + public FungibleAssetValue InitialConsensusToken { get; set; } + public FungibleAssetValue UnbondingConsensusToken { get => _unbondingConsensusToken; @@ -56,6 +62,8 @@ public FungibleAssetValue UnbondingConsensusToken public long Index { get; set; } + public long CreationHeight { get; set; } + public long CompletionBlockHeight { get; set; } public static bool operator ==(UndelegationEntry obj, UndelegationEntry other) @@ -80,9 +88,11 @@ public IValue Serialize() return List.Empty .Add(Address.Serialize()) .Add(UndelegationAddress.Serialize()) + .Add(InitialConsensusToken.Serialize()) .Add(UnbondingConsensusToken.Serialize()) .Add(Index.Serialize()) - .Add(CompletionBlockHeight.Serialize()); + .Add(CompletionBlockHeight.Serialize()) + .Add(CreationHeight.Serialize()); } public override bool Equals(object? obj) @@ -95,9 +105,11 @@ public bool Equals(UndelegationEntry? other) return !(other is null) && Address.Equals(other.Address) && UndelegationAddress.Equals(other.UndelegationAddress) && + InitialConsensusToken.Equals(other.InitialConsensusToken) && UnbondingConsensusToken.Equals(other.UnbondingConsensusToken) && Index == other.Index && - CompletionBlockHeight == other.CompletionBlockHeight; + CompletionBlockHeight == other.CompletionBlockHeight && + CreationHeight == other.CreationHeight; } public override int GetHashCode() diff --git a/Lib9c/Action/DPoS/Model/ValidatorSigningInfo.cs b/Lib9c/Action/DPoS/Model/ValidatorSigningInfo.cs new file mode 100644 index 0000000000..fb77150bbe --- /dev/null +++ b/Lib9c/Action/DPoS/Model/ValidatorSigningInfo.cs @@ -0,0 +1,86 @@ +#nullable enable +using Bencodex.Types; +using Nekoyume.Action.DPoS.Util; +using Libplanet.Crypto; +using System; +using Libplanet.Common; + +namespace Nekoyume.Action.DPoS.Model +{ + public class ValidatorSigningInfo : IEquatable + { + public ValidatorSigningInfo() + { + } + + public ValidatorSigningInfo(IValue serialized) + { + var dict = (Bencodex.Types.Dictionary)serialized; + Address = dict["addr"].ToAddress(); + StartHeight = dict["start_height"].ToLong(); + IndexOffset = dict["index_offset"].ToLong(); + JailedUntil = dict["jailed_until"].ToLong(); + Tombstoned = dict["tombstoned"].ToBoolean(); + MissedBlocksCounter = dict["missed_blocks_counter"].ToLong(); + } + + public Address Address { get; set; } + + public long StartHeight { get; set; } + + public long IndexOffset { get; set; } + + public long JailedUntil { get; set; } + + public bool Tombstoned { get; set; } + + public long MissedBlocksCounter { get; set; } + + public static bool operator ==(ValidatorSigningInfo obj, ValidatorSigningInfo other) + { + return obj.Equals(other); + } + + public static bool operator !=(ValidatorSigningInfo obj, ValidatorSigningInfo other) + { + return !(obj == other); + } + + public static Address DeriveAddress(Address validatorAddress) + { + return validatorAddress.Derive(nameof(ValidatorSigningInfo)); + } + + public IValue Serialize() + { + return Dictionary.Empty + .Add("addr", Address.Serialize()) + .Add("start_height", StartHeight.Serialize()) + .Add("index_offset", IndexOffset.Serialize()) + .Add("jailed_until", JailedUntil.Serialize()) + .Add("tombstoned", Tombstoned.Serialize()) + .Add("missed_blocks_counter", MissedBlocksCounter.Serialize()); + } + + public override bool Equals(object? obj) + { + return Equals(obj as ValidatorSigningInfo); + } + + public bool Equals(ValidatorSigningInfo? other) + { + return !(other is null) && + Address.Equals(other.Address) && + StartHeight.Equals(other.StartHeight) && + IndexOffset.Equals(other.IndexOffset) && + JailedUntil.Equals(other.JailedUntil) && + Tombstoned.Equals(other.Tombstoned) && + MissedBlocksCounter.Equals(other.MissedBlocksCounter); + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(Address.ToByteArray()); + } + } +} diff --git a/Lib9c/Action/DPoS/Unjail.cs b/Lib9c/Action/DPoS/Unjail.cs new file mode 100644 index 0000000000..ae01626f6c --- /dev/null +++ b/Lib9c/Action/DPoS/Unjail.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Immutable; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Action.DPoS.Control; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Util; + +namespace Nekoyume.Action.DPoS +{ + /// + /// A system action for DPoS that given . + /// + [ActionType(ActionTypeValue)] + public sealed class Unjail : ActionBase + { + private const string ActionTypeValue = "unjail"; + + /// + /// Creates a new instance of action. + /// + /// The of the validator + /// to unjail. + public Unjail(Address validator) + { + Validator = validator; + } + + public Unjail() + { + // Used only for deserialization. See also class Libplanet.Action.Sys.Registry. + } + + /// + /// The of the validator to . + /// + public Address Validator { get; set; } + + public FungibleAssetValue Amount { get; set; } + + /// + public override IValue PlainValue => Bencodex.Types.Dictionary.Empty + .Add("type_id", new Text(ActionTypeValue)) + .Add("validator", Validator.Serialize()); + + /// + public override void LoadPlainValue(IValue plainValue) + { + var dict = (Bencodex.Types.Dictionary)plainValue; + Validator = dict["validator"].ToAddress(); + } + + /// + public override IWorld Execute(IActionContext context) + { + context.UseGas(1); + IActionContext ctx = context; + var validatorAddress = Model.Validator.DeriveAddress(ctx.Signer); + if (!Validator.Equals(validatorAddress)) + { + throw new InvalidOperationException("Signer is not the validator."); + } + + var states = ctx.PreviousState; + states = ValidatorCtrl.Unjail( + states, + validatorAddress: Validator); + + return states; + } + } +} diff --git a/Lib9c/Action/DPoS/Util/DPoSModule.cs b/Lib9c/Action/DPoS/Util/DPoSModule.cs index 37b1574720..3ec155ef75 100644 --- a/Lib9c/Action/DPoS/Util/DPoSModule.cs +++ b/Lib9c/Action/DPoS/Util/DPoSModule.cs @@ -4,7 +4,7 @@ using Libplanet.Crypto; using Nekoyume.Action.DPoS.Misc; -namespace Nekoyume.Action.DPoS.Util +namespace Nekoyume.Action.DPoS { public static class DPoSModule { @@ -19,5 +19,12 @@ public static IWorld SetDPoSState(this IWorld world, Address address, IValue val account = account.SetState(address, value); return world.SetAccount(ReservedAddress.DPoSAccountAddress, account); } + + public static IWorld RemoveDPoSState(this IWorld world, Address address) + { + var account = world.GetAccount(ReservedAddress.DPoSAccountAddress); + account = account.RemoveState(address); + return world.SetAccount(ReservedAddress.DPoSAccountAddress, account); + } } } From 5b3d0c71b2bc77b5968e1282df1ea8852840b768 Mon Sep 17 00:00:00 2001 From: s2quake Date: Tue, 9 Apr 2024 14:06:44 +0900 Subject: [PATCH 2/2] test: Add test code for slashing. --- .../Action/DPoS/Control/EvidenceCtrlTest.cs | 155 +++ .../Action/DPoS/Control/SlashCtrlTest.cs | 914 ++++++++++++++++++ .../Action/DPoS/Control/ValidatorCtrlTest.cs | 152 +++ .../Control/ValidatorDelegationSetCtrlTest.cs | 2 +- .../Control/ValidatorSigningInfoCtrlTest.cs | 218 +++++ .Lib9c.Tests/Action/DPoS/PoSTest.cs | 141 +++ 6 files changed, 1581 insertions(+), 1 deletion(-) create mode 100644 .Lib9c.Tests/Action/DPoS/Control/EvidenceCtrlTest.cs create mode 100644 .Lib9c.Tests/Action/DPoS/Control/SlashCtrlTest.cs create mode 100644 .Lib9c.Tests/Action/DPoS/Control/ValidatorSigningInfoCtrlTest.cs diff --git a/.Lib9c.Tests/Action/DPoS/Control/EvidenceCtrlTest.cs b/.Lib9c.Tests/Action/DPoS/Control/EvidenceCtrlTest.cs new file mode 100644 index 0000000000..2521d5bebe --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Control/EvidenceCtrlTest.cs @@ -0,0 +1,155 @@ +namespace Lib9c.Tests.Action.DPoS.Control +{ + using System; + using System.Linq; + using System.Numerics; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume.Action.DPoS.Control; + using Nekoyume.Action.DPoS.Exception; + using Nekoyume.Action.DPoS.Misc; + using Nekoyume.Action.DPoS.Model; + using Nekoyume.Module; + using Xunit; + using Environment = Nekoyume.Action.DPoS.Control.Environment; + + public class EvidenceCtrlTest : PoSTest + { + private readonly PublicKey _operatorPublicKey; + private readonly Address _operatorAddress; + private readonly Address _delegatorAddress; + private readonly Address _validatorAddress; + private readonly FungibleAssetValue _governanceToken + = new FungibleAssetValue(Asset.GovernanceToken, 100, 0); + + private IWorld _states; + + public EvidenceCtrlTest() + { + _operatorPublicKey = new PrivateKey().PublicKey; + _operatorAddress = _operatorPublicKey.Address; + _delegatorAddress = CreateAddress(); + _validatorAddress = Validator.DeriveAddress(_operatorAddress); + _states = InitializeStates(); + } + + [Fact] + public void Execute_Test() + { + var governanceToken = _governanceToken; + var states = _states; + var operatorPublicKey = _operatorPublicKey; + var validatorAddress = _validatorAddress; + + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: governanceToken); + states = Update( + states: states, + blockIndex: 1); + + var power = GetPower(states, validatorAddress); + var evidence = new Evidence() + { + Address = validatorAddress, + Height = 1, + Power = power.RawValue, + }; + + states = EvidenceCtrl.Execute( + world: states, + actionContext: new ActionContext() { PreviousState = states, BlockIndex = 2 }, + validatorAddress: validatorAddress, + evidence: evidence, + nativeTokens: NativeTokens); + + var validator = ValidatorCtrl.GetValidator(states, validatorAddress); + var signingInfo = ValidatorSigningInfoCtrl.GetSigningInfo(states, validatorAddress); + var actualPower = GetPower(states, validatorAddress); + + Assert.NotEqual(power, actualPower); + Assert.True(validator.Jailed); + //Assert.Equal(BondingStatus.Unbonded, validator.Status); + Assert.Equal(long.MaxValue, signingInfo.JailedUntil); + Assert.True(signingInfo.Tombstoned); + } + + [Fact] + public void Execute_MaxAge_Test() + { + var governanceToken = _governanceToken; + var states = _states; + var operatorPublicKey = _operatorPublicKey; + var validatorAddress = _validatorAddress; + var blockIndex = 1; + + states = Promote( + states: states, + blockIndex: blockIndex, + operatorPublicKey: operatorPublicKey, + governanceToken: governanceToken); + states = Update( + states: states, + blockIndex: blockIndex); + + var farFutureHeight = blockIndex + Environment.MaxAgeNumBlocks + 1; + var expectedPower = GetPower(states, validatorAddress); + var evidence = new Evidence() + { + Address = validatorAddress, + Height = blockIndex, + Power = expectedPower.RawValue, + }; + + states = EvidenceCtrl.Execute( + world: states, + actionContext: new ActionContext() { PreviousState = states, BlockIndex = farFutureHeight }, + validatorAddress: validatorAddress, + evidence: evidence, + nativeTokens: NativeTokens); + + var validator = ValidatorCtrl.GetValidator(states, validatorAddress); + var signingInfo = ValidatorSigningInfoCtrl.GetSigningInfo(states, validatorAddress); + var actualPower = GetPower(states, validatorAddress); + + Assert.Equal(expectedPower, actualPower); + Assert.False(validator.Jailed); + Assert.NotEqual(long.MaxValue, signingInfo.JailedUntil); + Assert.False(signingInfo.Tombstoned); + } + + [Fact] + public void Execute_NotPromotedValidator_FailTest() + { + var states = _states; + var validatorAddress = _validatorAddress; + var power = GetPower(states, validatorAddress); + var evidence = new Evidence() + { + Address = validatorAddress, + Height = 1, + Power = power.RawValue, + }; + + Assert.Throws(() => + { + states = EvidenceCtrl.Execute( + world: states, + actionContext: new ActionContext() { PreviousState = states, BlockIndex = 2 }, + validatorAddress: validatorAddress, + evidence: evidence, + nativeTokens: NativeTokens); + }); + } + + private static FungibleAssetValue GetPower(IWorldState worldState, Address validatorAddress) + { + return worldState.GetBalance( + address: validatorAddress, + currency: Asset.ConsensusToken); + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/Control/SlashCtrlTest.cs b/.Lib9c.Tests/Action/DPoS/Control/SlashCtrlTest.cs new file mode 100644 index 0000000000..725379b85a --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Control/SlashCtrlTest.cs @@ -0,0 +1,914 @@ +namespace Lib9c.Tests.Action.DPoS.Control +{ + using System; + using System.Linq; + using System.Numerics; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume.Action.DPoS.Control; + using Nekoyume.Action.DPoS.Exception; + using Nekoyume.Action.DPoS.Misc; + using Nekoyume.Action.DPoS.Model; + using Nekoyume.Module; + using Xunit; + + public class SlashCtrlTest : PoSTest + { + public static readonly object[][] TestData = new object[][] + { + new object[] { false, new BigInteger(20) }, + new object[] { true, new BigInteger(20) }, + }; + + private const int ValidatorCount = 2; + private const int DelegatorCount = 2; + + private readonly PublicKey[] _operatorPublicKeys; + private readonly Address[] _operatorAddresses; + private readonly Address[] _delegatorAddresses; + private readonly Address[] _validatorAddresses; + private readonly FungibleAssetValue _defaultNCG + = new FungibleAssetValue(Asset.GovernanceToken, 1, 0); + + private readonly BigInteger _slashFactor = new BigInteger(20); + private IWorld _states; + + public SlashCtrlTest() + { + _operatorPublicKeys = Enumerable.Range(0, ValidatorCount) + .Select(_ => new PrivateKey().PublicKey) + .ToArray(); + _operatorAddresses = _operatorPublicKeys.Select(item => item.Address).ToArray(); + _delegatorAddresses = Enumerable.Range(0, DelegatorCount) + .Select(_ => CreateAddress()) + .ToArray(); + _validatorAddresses = _operatorAddresses + .Select(item => Validator.DeriveAddress(item)) + .ToArray(); + _states = InitializeStates(); + } + + [Theory] + [MemberData(nameof(TestData))] + public void Slash_Test(bool jailed, BigInteger slashFactor) + { + var validatorNCG = _defaultNCG; + var consensusToken = Asset.ConsensusFromGovernance(validatorNCG); + + var operatorPublicKey = _operatorPublicKeys[0]; + var validatorAddress = _validatorAddresses[0]; + var states = _states; + + // Promote validator with 1 NCG + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: validatorNCG); + states = Update( + states: states, + blockIndex: 1); + + var bondedPoolNCG = states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken); + if (jailed) + { + states = Jail( + states: states, + validatorAddress: validatorAddress); + } + + // Expect to slash 5 from validator's power and send 0.05 NCG to community pool + states = SlashCtrl.Slash( + world: states, + actionContext: new ActionContext { PreviousState = states, BlockIndex = 3, }, + validatorAddress: validatorAddress, + infractionHeight: 1, + power: consensusToken.RawValue, + slashFactor: slashFactor, + nativeTokens: NativeTokens); + + // Expect 95 ConsensusToken in validator + var expectedConsensusToken = SlashAsset(consensusToken, slashFactor); + // Expect 100 Share in validator + var expectedShare = GetShare(states, validatorAddress, expectedConsensusToken); + // Expect 0.95 NCG in bonded pool + var expectedBondedPoolNCG = bondedPoolNCG - (validatorNCG - SlashAsset(validatorNCG, slashFactor)); + // Expect 0.05 NCG in community pool + var expectedCommunityPoolNCG = validatorNCG - expectedBondedPoolNCG; + + var actualShare = GetShare(states, validatorAddress); + var actualConsensusToken = GetPower(states, validatorAddress); + var actualBondedPoolNCG = states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken); + var actualCommunityPoolNCG = states.GetBalance(ReservedAddress.CommunityPool, Asset.GovernanceToken); + + Assert.Equal(expectedShare, actualShare); + Assert.Equal(expectedConsensusToken, actualConsensusToken); + Assert.Equal(expectedBondedPoolNCG, actualBondedPoolNCG); + Assert.Equal(expectedCommunityPoolNCG, actualCommunityPoolNCG); + + _states = states; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Slash_WithDelegation_Test(bool jailed) + { + var validatorNCG = _defaultNCG; + var delegatorNCG = _defaultNCG; + var consensusToken = Asset.ConsensusFromGovernance(validatorNCG + delegatorNCG); + var slashFactor = _slashFactor; + + var operatorPublicKey = _operatorPublicKeys[0]; + var validatorAddress = _validatorAddresses[0]; + var delegatorAddress = _delegatorAddresses[0]; + var states = _states; + + // Promote validator with 1 NCG + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: validatorNCG); + // Delegate 1 NCG by delegator + states = Delegate( + states: states, + blockIndex: 1, + delegatorAddress: delegatorAddress, + validatorAddress: validatorAddress, + governanceToken: validatorNCG); + states = Update( + states: states, + blockIndex: 1); + + var bondedPoolNCG = states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken); + if (jailed) + { + states = Jail( + states: states, + validatorAddress: validatorAddress); + } + + // Expect to slash 10 from validator's power and send 0.1 NCG to community pool + states = SlashCtrl.Slash( + world: states, + actionContext: new ActionContext { PreviousState = states, BlockIndex = 2, }, + validatorAddress: validatorAddress, + infractionHeight: 1, + power: consensusToken.RawValue, + slashFactor: slashFactor, + nativeTokens: NativeTokens); + + // Expect 200 Share in validator + var expectedShare = FungibleAssetValue.FromRawValue(Asset.Share, consensusToken.RawValue); + // Expect 190 ConsensusToken in validator + var expectedConsensusToken = SlashAsset(consensusToken, slashFactor); + // Expect 1.9 NCG in bonded pool + var expectedBondedPoolNCG = SlashAsset(validatorNCG + delegatorNCG, slashFactor); + // Expect 0.1 NCG in community pool + var expectedCommunityPoolNCG = bondedPoolNCG - expectedBondedPoolNCG; + + var actualShare = GetShare(states, validatorAddress); + var actualConsensusToken = GetPower(states, validatorAddress); + var actualBondedPoolNCG = states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken); + var actualCommunityPoolNCG = states.GetBalance(ReservedAddress.CommunityPool, Asset.GovernanceToken); + + Assert.Equal(expectedShare, actualShare); + Assert.Equal(expectedConsensusToken, actualConsensusToken); + Assert.Equal(expectedBondedPoolNCG, actualBondedPoolNCG); + Assert.Equal(expectedCommunityPoolNCG, actualCommunityPoolNCG); + + _states = states; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Slash_WithUndelegation_Test(bool jailed) + { + var validatorNCG = _defaultNCG; + var delegatorNCG = _defaultNCG; + var slashFactor = _slashFactor; + var delegatorConsensusPower = Asset.ConsensusFromGovernance(delegatorNCG); + var undelegationShare = FungibleAssetValue.FromRawValue( + currency: Asset.Share, + rawValue: delegatorConsensusPower.RawValue); + + var operatorPublicKey = _operatorPublicKeys[0]; + var validatorAddress = _validatorAddresses[0]; + var delegatorAddress = _delegatorAddresses[0]; + var states = _states; + + // Promote validator with 1 NCG + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: validatorNCG); + states = Update(states, blockIndex: 1); + + // Delegate 1 NCG by delegator + states = Delegate( + states: states, + blockIndex: 2, + delegatorAddress: delegatorAddress, + validatorAddress: validatorAddress, + governanceToken: delegatorNCG); + states = Update(states, blockIndex: 2); + + var consensusTokenBeforeInfraction = GetPower(states, validatorAddress); + + // Undelegate 100 Share by delegator + states = Undelegate( + states: states, + blockIndex: 3, + delegatorAddress: delegatorAddress, + validatorAddress: validatorAddress, + share: undelegationShare + ); + states = Update(states, blockIndex: 3); + + var bondedPoolNCG = states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken); + var unbondedPoolNCG = states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken); + var consensusToken = GetPower(states, validatorAddress); + var share = GetShare(states, validatorAddress); + + if (jailed) + { + states = Jail( + states: states, + validatorAddress: validatorAddress); + } + + // Expect to slash 10 from validator's power and send 0.1 NCG to community pool + states = SlashCtrl.Slash( + world: states, + actionContext: new ActionContext { PreviousState = states, BlockIndex = 4, }, + validatorAddress: validatorAddress, + infractionHeight: 2, + power: consensusTokenBeforeInfraction.RawValue, + slashFactor: slashFactor, + nativeTokens: NativeTokens); + + // Expect 100 Share in validator + var expectedShare = share; + // Expect 95 ConsensusToken in validator + var expectedConsensusToken = SlashAsset(consensusToken, slashFactor); + // Expect 0.95 NCG in bonded pool + var expectedBondedPoolNCG = SlashAsset(validatorNCG, slashFactor); + // Expect 0.95 NCG in unbonded pool + var expectedUnbondedPoolNCG = SlashAsset(validatorNCG, slashFactor); + // Expect 0.1 NCG in community pool + var expectedCommunityPoolNCG = bondedPoolNCG + unbondedPoolNCG - expectedBondedPoolNCG - expectedUnbondedPoolNCG; + + var undelegation = UndelegateCtrl.GetUndelegation(states, delegatorAddress, validatorAddress); + var undelegationEntry = UndelegateCtrl.GetUndelegationEntry(states, undelegation.UndelegationEntryAddresses[0]); + + Assert.NotNull(undelegation); + Assert.NotNull(undelegationEntry); + Assert.Equal(expectedConsensusToken, undelegationEntry.UnbondingConsensusToken); + + var actualShare = GetShare(states, validatorAddress); + var actualConsensusToken = GetPower(states, validatorAddress); + var actualBondedPoolNCG = states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken); + var actualUnbondedPoolNCG = states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken); + var actualCommunityPoolNCG = states.GetBalance(ReservedAddress.CommunityPool, Asset.GovernanceToken); + + Assert.Equal(expectedShare, actualShare); + Assert.Equal(expectedConsensusToken, actualConsensusToken); + Assert.Equal(expectedBondedPoolNCG, actualBondedPoolNCG); + Assert.Equal(expectedUnbondedPoolNCG, actualUnbondedPoolNCG); + Assert.Equal(expectedCommunityPoolNCG, actualCommunityPoolNCG); + + _states = states; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Slash_OnlyValidator_AfterUndelegating_Test(bool jailed) + { + var validatorNCG = _defaultNCG; + var delegatorNCG = _defaultNCG; + var slashFactor = _slashFactor; + var delegatorConsensusPower = Asset.ConsensusFromGovernance(delegatorNCG); + var undelegationShare = FungibleAssetValue.FromRawValue( + currency: Asset.Share, + rawValue: delegatorConsensusPower.RawValue); + + var operatorPublicKey = _operatorPublicKeys[0]; + var validatorAddress = _validatorAddresses[0]; + var delegatorAddress = _delegatorAddresses[0]; + var states = _states; + + // Promote validator with 1 NCG + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: validatorNCG); + states = Update(states, blockIndex: 1); + + // Delegate 1 NCG by delegator + states = Delegate( + states: states, + blockIndex: 2, + delegatorAddress: delegatorAddress, + validatorAddress: validatorAddress, + governanceToken: delegatorNCG); + states = Update(states, blockIndex: 2); + + // Undelegate 100 Share by delegator + states = Undelegate( + states: states, + blockIndex: 3, + delegatorAddress: delegatorAddress, + validatorAddress: validatorAddress, + share: undelegationShare); + states = Update(states, blockIndex: 3); + + var consensusTokenBeforeInfraction = GetPower(states, validatorAddress); + var bondedPoolNCG = states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken); + var unbondedPoolNCG = states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken); + var consensusToken = GetPower(states, validatorAddress); + var share = GetShare(states, validatorAddress); + + if (jailed) + { + states = Jail( + states: states, + validatorAddress: validatorAddress); + } + + // Expect to slash only 5 from the validator's power, excluding the delegator, at height 4. + states = SlashCtrl.Slash( + world: states, + actionContext: new ActionContext { PreviousState = states, BlockIndex = 4, }, + validatorAddress: validatorAddress, + infractionHeight: 4, + power: consensusTokenBeforeInfraction.RawValue, + slashFactor: slashFactor, + nativeTokens: NativeTokens); + + // Expect 100 Share in validator + var expectedShare = share; + // Expect 95 ConsensusToken in validator + var expectedConsensusToken = SlashAsset(consensusToken, slashFactor); + // Expect 0.95 NCG in bonded pool + var expectedBondedPoolNCG = SlashAsset(validatorNCG, slashFactor); + // Expect 1.0 NCG in unbonded pool + var expectedUnbondedPoolNCG = delegatorNCG; + // Expect 0.05 NCG in community pool + var expectedCommunityPoolNCG = bondedPoolNCG + unbondedPoolNCG - expectedBondedPoolNCG - expectedUnbondedPoolNCG; + + var undelegation = UndelegateCtrl.GetUndelegation(states, delegatorAddress, validatorAddress); + var undelegationEntry = UndelegateCtrl.GetUndelegationEntry(states, undelegation.UndelegationEntryAddresses[0]); + + Assert.NotNull(undelegation); + Assert.NotNull(undelegationEntry); + Assert.Equal( + expected: Asset.ConsensusFromGovernance(delegatorNCG), + actual: undelegationEntry.UnbondingConsensusToken); + + var actualShare = GetShare(states, validatorAddress); + var actualConsensusToken = GetPower(states, validatorAddress); + var actualBondedPoolNCG = states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken); + var actualUnbondedPoolNCG = states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken); + var actualCommunityPoolNCG = states.GetBalance(ReservedAddress.CommunityPool, Asset.GovernanceToken); + + Assert.Equal(expectedShare, actualShare); + Assert.Equal(expectedConsensusToken, actualConsensusToken); + Assert.Equal(expectedBondedPoolNCG, actualBondedPoolNCG); + Assert.Equal(expectedUnbondedPoolNCG, actualUnbondedPoolNCG); + Assert.Equal(expectedCommunityPoolNCG, actualCommunityPoolNCG); + + _states = states; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Slash_WithRedelegation_Test(bool jailed) + { + var slashFactor = _slashFactor; + + var srcOperatorPublicKey = _operatorPublicKeys[0]; + var dstOperatorPublicKey = _operatorPublicKeys[1]; + var srcValidatorAddress = _validatorAddresses[0]; + var dstValidatorAddress = _validatorAddresses[1]; + var delegatorAddress = _delegatorAddresses[0]; + var states = _states; + var srcValidatorNCG = _defaultNCG; + var dstValidatorNCG = _defaultNCG; + var delegatorNCG = _defaultNCG; + var delegatorConsensusPower = Asset.ConsensusFromGovernance(delegatorNCG); + var redelegationShare = FungibleAssetValue.FromRawValue( + currency: Asset.Share, + rawValue: delegatorConsensusPower.RawValue); + + // Delegate 100 NCG by src operator + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: srcOperatorPublicKey, + governanceToken: srcValidatorNCG); + // Delegate 100 NCG by dst operator + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: dstOperatorPublicKey, + governanceToken: dstValidatorNCG); + states = Update(states, blockIndex: 1); + + // Delgate 100 NCG by delegator to src validator + states = Delegate( + states: states, + blockIndex: 2, + delegatorAddress: delegatorAddress, + validatorAddress: srcValidatorAddress, + governanceToken: delegatorNCG); + states = Update(states, blockIndex: 2); + var consensusTokenBeforeInfraction = GetPower(states, srcValidatorAddress); + + // Redelegate 100 NCG from src validator to dst validator + states = Redelegate( + states: states, + blockIndex: 3, + delegatorAddress: delegatorAddress, + srcValidatorAddress: srcValidatorAddress, + dstValidatorAddress: dstValidatorAddress, + share: redelegationShare); + states = Update(states, blockIndex: 3); + + // 3 NCG in bonded pool + var bondedPoolNCG = states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken); + // 0 NCG in unbonded pool + var unbondedPoolNCG = states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken); + // 100 consensus token in src validator + var srcConsensusToken = GetPower(states, srcValidatorAddress); + // 200 consensus token in dst validator + var dstConsensusToken = GetPower(states, dstValidatorAddress); + + // 100 share in src validator + var srcShare = GetShare(states, srcValidatorAddress); + // 200 share in dst validator + var dstShare = GetShare(states, dstValidatorAddress); + + if (jailed) + { + states = Jail( + states: states, + validatorAddress: srcValidatorAddress); + } + + // Slash src validator at height 2 + states = SlashCtrl.Slash( + world: states, + actionContext: new ActionContext { PreviousState = states, BlockIndex = 4, }, + validatorAddress: srcValidatorAddress, + infractionHeight: 2, + power: consensusTokenBeforeInfraction.RawValue, + slashFactor: slashFactor, + nativeTokens: NativeTokens); + + // Expect 95 ConsensusToken in src validator + var expectedSrcConsensusToken = SlashAsset(srcConsensusToken, slashFactor); + // Expect 195 ConsensusToken in dst validator + var expectedDstConsensusToken = + Asset.ConsensusFromGovernance(dstValidatorNCG) + + SlashAsset(Asset.ConsensusFromGovernance(delegatorNCG), slashFactor); + // Expect 100 Share in src validator + var expectedSrcShare = srcShare; + // Expect 195 Shre in dst validator + var expectedDstShare = + ValidatorCtrl.ShareFromConsensusToken(states, dstValidatorAddress, expectedDstConsensusToken); + // Expect 2.9 NCG in bonded pool + var expectedBondedPoolNCG = SlashAsset(srcValidatorNCG + delegatorNCG, slashFactor) + dstValidatorNCG; + // Expect 0 NCG in unbonded pool + var expectedUnbondedPoolNCG = new FungibleAssetValue(Asset.GovernanceToken, 0, 0); + // Expect 0.1 NCG in community pool + var expectedCommunityPoolNCG = srcValidatorNCG + dstValidatorNCG + delegatorNCG - expectedBondedPoolNCG; + + var redelegation = RedelegateCtrl.GetRedelegation(states, delegatorAddress, srcValidatorAddress, dstValidatorAddress); + var redelegationEntry = RedelegateCtrl.GetRedelegationEntry(states, redelegation.RedelegationEntryAddresses[0]); + + Assert.NotNull(redelegation); + Assert.NotNull(redelegationEntry); + + var actualSrcConsensusToken = GetPower(states, srcValidatorAddress); + var actualDstConsensusToken = GetPower(states, dstValidatorAddress); + var actualSrcShare = GetShare(states, srcValidatorAddress); + var actualDstShare = GetShare(states, dstValidatorAddress); + var actualBondedPoolNCG = states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken); + var actualUnbondedPoolNCG = states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken); + var actualCommunityPoolNCG = states.GetBalance(ReservedAddress.CommunityPool, Asset.GovernanceToken); + + Assert.Equal(expectedSrcShare, actualSrcShare); + Assert.Equal(expectedDstShare, actualDstShare); + Assert.Equal(expectedSrcConsensusToken, actualSrcConsensusToken); + Assert.Equal(expectedDstConsensusToken, actualDstConsensusToken); + Assert.Equal(expectedBondedPoolNCG, actualBondedPoolNCG); + Assert.Equal(expectedUnbondedPoolNCG, actualUnbondedPoolNCG); + Assert.Equal(expectedCommunityPoolNCG, actualCommunityPoolNCG); + + _states = states; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Slash_OnlyValidator_AfterRedelegating_Test(bool jailed) + { + var governanceToken = _defaultNCG; + var slashFactor = _slashFactor; + + var srcOperatorPublicKey = _operatorPublicKeys[0]; + var dstOperatorPublicKey = _operatorPublicKeys[1]; + var srcValidatorAddress = _validatorAddresses[0]; + var dstValidatorAddress = _validatorAddresses[1]; + var delegatorAddress = _delegatorAddresses[0]; + var states = _states; + var srcValidatorNCG = governanceToken; + var dstValidatorNCG = governanceToken; + var delegatorNCG = governanceToken; + var delegatorConsensusPower = Asset.ConsensusFromGovernance(delegatorNCG); + + // Promote src validator with 1 NCG + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: srcOperatorPublicKey, + governanceToken: srcValidatorNCG); + // Promote dst validator with 1 NCG + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: dstOperatorPublicKey, + governanceToken: dstValidatorNCG); + states = Update(states, blockIndex: 1); + + // Delgate 1 NCG by delegator to src validator + states = Delegate( + states: states, + blockIndex: 2, + delegatorAddress: delegatorAddress, + validatorAddress: srcValidatorAddress, + governanceToken: delegatorNCG); + states = Update(states, blockIndex: 2); + + // Redelegate 100 share from src validator to dst validator + states = Redelegate( + states: states, + blockIndex: 3, + delegatorAddress: delegatorAddress, + srcValidatorAddress: srcValidatorAddress, + dstValidatorAddress: dstValidatorAddress, + share: FungibleAssetValue.FromRawValue(Asset.Share, delegatorConsensusPower.RawValue)); + states = Update(states, blockIndex: 3); + + var consensusTokenBeforeInfraction = GetPower(states, srcValidatorAddress); + // 3 NCG in bonded pool + var bondedPoolNCG = states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken); + // 0 NCG in unbonded pool + var unbondedPoolNCG = states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken); + // 100 consensus token in src validator + var srcConsensusToken = GetPower(states, srcValidatorAddress); + // 200 consensus token in dst validator + var dstConsensusToken = GetPower(states, dstValidatorAddress); + + // 100 share in src validator + var srcShare = GetShare(states, srcValidatorAddress); + // 200 share in dst validator + var dstShare = GetShare(states, dstValidatorAddress); + + if (jailed) + { + states = Jail( + states: states, + validatorAddress: srcValidatorAddress); + } + + // Slash only src validator's power at height 4 + states = SlashCtrl.Slash( + world: states, + actionContext: new ActionContext { PreviousState = states, BlockIndex = 4, }, + validatorAddress: srcValidatorAddress, + infractionHeight: 4, + power: consensusTokenBeforeInfraction.RawValue, + slashFactor: slashFactor, + nativeTokens: NativeTokens); + + // Expect 95 ConsensusToken in src validator + var expectedSrcConsensusToken = SlashAsset(srcConsensusToken, slashFactor); + // Expect 200 ConsensusToken in src validator + var expectedDstConsensusToken = + Asset.ConsensusFromGovernance(dstValidatorNCG) + + Asset.ConsensusFromGovernance(delegatorNCG); + // Expect 100 Share in src validator + var expectedSrcShare = srcShare; + // Expect 200 Share in dst validator + var expectedDstShare = FungibleAssetValue.FromRawValue(Asset.Share, expectedDstConsensusToken.RawValue); + // Expect 2.95 NCG in bonded pool + var expectedBondedPoolNCG = SlashAsset(srcValidatorNCG, slashFactor) + delegatorNCG + dstValidatorNCG; + // Expect 0 NCG in unbonded pool + var expectedUnbondedPoolNCG = new FungibleAssetValue(Asset.GovernanceToken, 0, 0); + // Expect 0.05 NCG in community pool + var expectedCommunityPoolNCG = srcValidatorNCG + dstValidatorNCG + delegatorNCG - expectedBondedPoolNCG; + + var redelegation = RedelegateCtrl.GetRedelegation(states, delegatorAddress, srcValidatorAddress, dstValidatorAddress); + var redelegationEntry = RedelegateCtrl.GetRedelegationEntry(states, redelegation.RedelegationEntryAddresses[0]); + + Assert.NotNull(redelegation); + Assert.NotNull(redelegationEntry); + + var actualSrcConsensusToken = GetPower(states, srcValidatorAddress); + var actualDstConsensusToken = GetPower(states, dstValidatorAddress); + var actualSrcShare = GetShare(states, srcValidatorAddress); + var actualDstShare = GetShare(states, dstValidatorAddress); + var actualBondedPoolNCG = states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken); + var actualUnbondedPoolNCG = states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken); + var actualCommunityPoolNCG = states.GetBalance(ReservedAddress.CommunityPool, Asset.GovernanceToken); + + Assert.Equal(expectedSrcShare, actualSrcShare); + Assert.Equal(expectedDstShare, actualDstShare); + Assert.Equal(expectedSrcConsensusToken, actualSrcConsensusToken); + Assert.Equal(expectedDstConsensusToken, actualDstConsensusToken); + Assert.Equal(expectedBondedPoolNCG, actualBondedPoolNCG); + Assert.Equal(expectedUnbondedPoolNCG, actualUnbondedPoolNCG); + Assert.Equal(expectedCommunityPoolNCG, actualCommunityPoolNCG); + + _states = states; + } + + [Fact] + public void Slash_InvalidValidatorAddress_FailTest() + { + var states = _states; + var validatorAddress = CreateAddress(); + + Assert.Throws(() => + { + SlashCtrl.Slash( + world: states, + actionContext: new ActionContext { PreviousState = states, }, + validatorAddress: validatorAddress, + infractionHeight: 2, + power: 100, + slashFactor: 20, + nativeTokens: NativeTokens); + }); + } + + [Fact] + public void Slash_NegativeSlashFactor_FailTest() + { + var states = _states; + var operatorPublicKey = _operatorPublicKeys[0]; + var validatorAddress = _validatorAddresses[0]; + + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: new FungibleAssetValue(Asset.GovernanceToken, 100, 0)); + + Assert.Throws(() => + { + SlashCtrl.Slash( + world: states, + actionContext: new ActionContext { PreviousState = states, }, + validatorAddress: validatorAddress, + infractionHeight: 2, + power: 100, + slashFactor: -1, + nativeTokens: NativeTokens); + }); + } + + [Fact] + public void Slash_FutureBlockHeight_FailTest() + { + var validatorNCG = _defaultNCG; + var consensusToken = Asset.ConsensusFromGovernance(validatorNCG); + var slashFactor = _slashFactor; + + var operatorPublicKey = _operatorPublicKeys[0]; + var validatorAddress = _validatorAddresses[0]; + var states = _states; + + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: validatorNCG); + states = Update( + states: states, + blockIndex: 1); + + Assert.Throws(() => + { + SlashCtrl.Slash( + world: states, + actionContext: new ActionContext { PreviousState = states, BlockIndex = 2, }, + validatorAddress: validatorAddress, + infractionHeight: 3, + power: consensusToken.RawValue, + slashFactor: slashFactor, + nativeTokens: NativeTokens); + }); + } + + [Fact] + public void Unjail_Test() + { + var validatorNCG = _defaultNCG; + var operatorPublicKey = _operatorPublicKeys[0]; + var validatorAddress = _validatorAddresses[0]; + var states = _states; + + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: validatorNCG); + states = Update( + states: states, + blockIndex: 1); + states = Jail( + states: states, + validatorAddress: validatorAddress); + + states = SlashCtrl.Unjail( + world: states, + actionContext: new ActionContext { PreviousState = states, BlockIndex = 2, }, + validatorAddress: validatorAddress); + + var actualValidator = ValidatorCtrl.GetValidator(states, validatorAddress); + Assert.False(actualValidator!.Jailed); + + _states = states; + } + + [Fact] + public void Unjail_NotPromotedValidator_FailTest() + { + var states = _states; + var validatorAddress = _validatorAddresses[0]; + + Assert.Throws(() => + { + SlashCtrl.Unjail( + world: states, + actionContext: new ActionContext { PreviousState = states, BlockIndex = 2, }, + validatorAddress: validatorAddress); + }); + } + + [Fact] + public void Unjail_NotJailedValidator_FailTest() + { + var validatorNCG = _defaultNCG; + var operatorPublicKey = _operatorPublicKeys[0]; + var validatorAddress = _validatorAddresses[0]; + var states = _states; + + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: validatorNCG); + + Assert.Throws(() => + { + SlashCtrl.Unjail( + world: states, + actionContext: new ActionContext { PreviousState = states, BlockIndex = 2, }, + validatorAddress: validatorAddress); + }); + } + + [Fact] + public void Unjail_Tombstoned_FailTest() + { + var validatorNCG = _defaultNCG; + var operatorPublicKey = _operatorPublicKeys[0]; + var validatorAddress = _validatorAddresses[0]; + var states = _states; + + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: validatorNCG); + states = Tombstone( + states: states, + validatorAddress: validatorAddress); + + Assert.Throws(() => + { + SlashCtrl.Unjail( + world: states, + actionContext: new ActionContext { PreviousState = states, BlockIndex = 2, }, + validatorAddress: validatorAddress); + }); + } + + [Fact] + public void Unjail_StillJailed_FailTest() + { + var validatorNCG = _defaultNCG; + var operatorPublicKey = _operatorPublicKeys[0]; + var validatorAddress = _validatorAddresses[0]; + var states = _states; + + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: validatorNCG); + states = Update( + states: states, + blockIndex: 1); + states = JailUntil( + states: states, + validatorAddress: validatorAddress, + blockHeight: long.MaxValue); + + Assert.Throws(() => + { + SlashCtrl.Unjail( + world: states, + actionContext: new ActionContext { PreviousState = states, BlockIndex = 1, }, + validatorAddress: validatorAddress); + }); + } + + [Fact] + public void Unjail_PowerIsLessThanMinimum_FailTest() + { + var validatorNCG = _defaultNCG; + var operatorPublicKey = _operatorPublicKeys[0]; + var validatorAddress = _validatorAddresses[0]; + var states = _states; + + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: validatorNCG); + states = Update( + states: states, + blockIndex: 1); + states = Slash( + states: states, + blockIndex: 2, + validatorAddress: validatorAddress, + infractionHeight: 1, + power: Asset.ConsensusFromGovernance(validatorNCG).RawValue, + slashFactor: 2); + states = Jail( + states: states, + validatorAddress: validatorAddress); + + Assert.Throws(() => + { + SlashCtrl.Unjail( + world: states, + actionContext: new ActionContext { PreviousState = states, BlockIndex = 2, }, + validatorAddress: validatorAddress); + }); + } + + private static FungibleAssetValue SlashAsset(FungibleAssetValue value, BigInteger factor) + { + var (amount, _) = value.DivRem(factor); + return value - amount; + } + + private static FungibleAssetValue GetPower(IWorldState worldState, Address validatorAddress) + { + return worldState.GetBalance( + address: validatorAddress, + currency: Asset.ConsensusToken); + } + + private static FungibleAssetValue GetShare(IWorldState worldState, Address validatorAddress) + { + var validator = ValidatorCtrl.GetValidator(worldState, validatorAddress)!; + return validator.DelegatorShares; + } + + private static FungibleAssetValue GetShare( + IWorldState worldState, + Address validatorAddress, + FungibleAssetValue consensusToken) + { + var share = ValidatorCtrl.ShareFromConsensusToken( + worldState, + validatorAddress, + consensusToken); + return share ?? new FungibleAssetValue(Asset.Share); + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/Control/ValidatorCtrlTest.cs b/.Lib9c.Tests/Action/DPoS/Control/ValidatorCtrlTest.cs index 98a1bb80ea..b2b2d09649 100644 --- a/.Lib9c.Tests/Action/DPoS/Control/ValidatorCtrlTest.cs +++ b/.Lib9c.Tests/Action/DPoS/Control/ValidatorCtrlTest.cs @@ -17,6 +17,9 @@ public class ValidatorCtrlTest : PoSTest private readonly Address _operatorAddress; private readonly Address _validatorAddress; private readonly ImmutableHashSet _nativeTokens; + private readonly FungibleAssetValue _governanceToken + = new FungibleAssetValue(Asset.GovernanceToken, 100, 0); + private IWorld _states; public ValidatorCtrlTest() @@ -120,5 +123,154 @@ public void BalanceTest(int mintAmount, int selfDelegateAmount) ShareFromGovernance(selfDelegateAmount), ValidatorCtrl.GetValidator(_states, _validatorAddress)!.DelegatorShares); } + + [Fact] + public void JailTest() + { + var governanceToken = _governanceToken; + var states = _states; + var operatorPublicKey = _operatorPublicKey; + var validatorAddress = _validatorAddress; + + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: governanceToken); + + // Test before jailing + var validator1 = ValidatorCtrl.GetValidator(states, validatorAddress)!; + var powerIndex1 = ValidatorPowerIndexCtrl.GetValidatorPowerIndex(states)!; + Assert.False(validator1.Jailed); + Assert.Contains( + powerIndex1.ValidatorAddresses, + address => address.Equals(validatorAddress)); + + // Jail + states = ValidatorCtrl.Jail( + states, + validatorAddress: validatorAddress); + + // Test after jailing + var validator2 = ValidatorCtrl.GetValidator(states, validatorAddress)!; + var powerIndex2 = ValidatorPowerIndexCtrl.GetValidatorPowerIndex(states)!; + + Assert.True(validator2.Jailed); + Assert.DoesNotContain( + powerIndex2.ValidatorAddresses, + address => address.Equals(validatorAddress)); + } + + [Fact] + public void Jail_NotPromotedValidator_FailTest() + { + Assert.Throws(() => + { + ValidatorCtrl.Jail( + world: _states, + validatorAddress: _validatorAddress); + }); + } + + [Fact] + public void Jail_JailedValidator_FailTest() + { + var governanceToken = _governanceToken; + var states = _states; + var operatorPublicKey = _operatorPublicKey; + var validatorAddress = _validatorAddress; + + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: governanceToken); + + // Jail + states = ValidatorCtrl.Jail( + states, + validatorAddress: validatorAddress); + + Assert.Throws(() => + { + ValidatorCtrl.Jail( + world: states, + validatorAddress: validatorAddress); + }); + } + + [Fact] + public void UnjailTest() + { + var governanceToken = _governanceToken; + var states = _states; + var operatorPublicKey = _operatorPublicKey; + var validatorAddress = _validatorAddress; + + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: governanceToken); + + states = ValidatorCtrl.Jail( + states, + validatorAddress: _validatorAddress); + + // Test before unjailing + var validator1 = ValidatorCtrl.GetValidator(states, validatorAddress)!; + var powerIndex1 = ValidatorPowerIndexCtrl.GetValidatorPowerIndex(states)!; + + Assert.True(validator1.Jailed); + Assert.DoesNotContain( + powerIndex1.ValidatorAddresses, + address => address.Equals(_validatorAddress)); + + // Unjail + states = ValidatorCtrl.Unjail( + states, + validatorAddress: validatorAddress); + + // Test after unjailing + var validator2 = ValidatorCtrl.GetValidator(states, validatorAddress)!; + var powerIndex2 = ValidatorPowerIndexCtrl.GetValidatorPowerIndex(states)!; + Assert.False(validator2.Jailed); + Assert.Contains( + powerIndex2.ValidatorAddresses, + address => address.Equals(validatorAddress)); + } + + [Fact] + public void Unjail_NotPromotedValidator_FailTest() + { + Assert.Throws(() => + { + ValidatorCtrl.Unjail( + world: _states, + validatorAddress: _validatorAddress); + }); + } + + [Fact] + public void Unjail_NotJailedValidator_FailTest() + { + var governanceToken = _governanceToken; + var states = _states; + var operatorPublicKey = _operatorPublicKey; + var validatorAddress = _validatorAddress; + + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: governanceToken); + + Assert.Throws(() => + { + ValidatorCtrl.Unjail( + world: states, + validatorAddress: validatorAddress); + }); + } } } diff --git a/.Lib9c.Tests/Action/DPoS/Control/ValidatorDelegationSetCtrlTest.cs b/.Lib9c.Tests/Action/DPoS/Control/ValidatorDelegationSetCtrlTest.cs index a83b206391..c799f7b515 100644 --- a/.Lib9c.Tests/Action/DPoS/Control/ValidatorDelegationSetCtrlTest.cs +++ b/.Lib9c.Tests/Action/DPoS/Control/ValidatorDelegationSetCtrlTest.cs @@ -42,7 +42,7 @@ public ValidatorDelegationSetCtrlTest() } [Fact] - public void PromoteTest() + public void Promote_Test() { var validatorDelegationSet = ValidatorDelegationSetCtrl.GetValidatorDelegationSet( _states, diff --git a/.Lib9c.Tests/Action/DPoS/Control/ValidatorSigningInfoCtrlTest.cs b/.Lib9c.Tests/Action/DPoS/Control/ValidatorSigningInfoCtrlTest.cs new file mode 100644 index 0000000000..00d4378d9d --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Control/ValidatorSigningInfoCtrlTest.cs @@ -0,0 +1,218 @@ +namespace Lib9c.Tests.Action.DPoS.Control +{ + using System; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume.Action.DPoS.Control; + using Nekoyume.Action.DPoS.Exception; + using Nekoyume.Action.DPoS.Misc; + using Nekoyume.Action.DPoS.Model; + using Xunit; + + public class ValidatorSigningInfoCtrlTest : PoSTest + { + private readonly PublicKey _operatorPublicKey; + private readonly Address _operatorAddress; + private readonly Address _delegatorAddress; + private readonly Address _validatorAddress; + private readonly FungibleAssetValue _governanceToken + = new FungibleAssetValue(Asset.GovernanceToken, 100, 0); + + private IWorld _states; + + public ValidatorSigningInfoCtrlTest() + { + _operatorPublicKey = new PrivateKey().PublicKey; + _operatorAddress = _operatorPublicKey.Address; + _delegatorAddress = CreateAddress(); + _validatorAddress = Validator.DeriveAddress(_operatorAddress); + _states = InitializeStates(); + } + + [Fact] + public void SetSigningInfo_Test() + { + var signingInfo = new ValidatorSigningInfo + { + Address = _validatorAddress, + }; + + _states = ValidatorSigningInfoCtrl.SetSigningInfo( + world: _states, + signingInfo: signingInfo); + } + + [Fact] + public void GetSigningInfo_Test() + { + var signingInfo1 = ValidatorSigningInfoCtrl.GetSigningInfo(_states, _validatorAddress); + Assert.Null(signingInfo1); + + var signingInfo2 = new ValidatorSigningInfo + { + Address = _validatorAddress, + }; + _states = ValidatorSigningInfoCtrl.SetSigningInfo( + world: _states, + signingInfo: signingInfo2); + + var signingInfo3 = ValidatorSigningInfoCtrl.GetSigningInfo(_states, _validatorAddress); + Assert.NotNull(signingInfo3); + Assert.Equal(_validatorAddress, signingInfo3.Address); + Assert.Equal(signingInfo2, signingInfo3); + } + + [Fact] + public void Tombstone_Test() + { + var governanceToken = _governanceToken; + var states = _states; + var operatorPublicKey = _operatorPublicKey; + var validatorAddress = _validatorAddress; + + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: governanceToken); + + states = ValidatorSigningInfoCtrl.Tombstone(states, validatorAddress); + + Assert.True(ValidatorSigningInfoCtrl.IsTombstoned(states, validatorAddress)); + } + + [Fact] + public void Tombstone_NotPromotedValidator_FailTest() + { + var states = _states; + var validatorAddress = _validatorAddress; + + Assert.Throws(() => + { + ValidatorSigningInfoCtrl.Tombstone(states, validatorAddress); + }); + } + + [Fact] + public void Tombstone_TombstonedValidator_FailTest() + { + var governanceToken = _governanceToken; + var states = _states; + var operatorPublicKey = _operatorPublicKey; + var validatorAddress = _validatorAddress; + + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: governanceToken); + + states = ValidatorSigningInfoCtrl.Tombstone(states, validatorAddress); + + Assert.Throws(() => + { + ValidatorSigningInfoCtrl.Tombstone(states, validatorAddress); + }); + } + + [Fact] + public void JailUtil_Test() + { + var governanceToken = _governanceToken; + var states = _states; + var operatorPublicKey = _operatorPublicKey; + var validatorAddress = _validatorAddress; + + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: governanceToken); + + states = ValidatorSigningInfoCtrl.JailUntil( + world: states, + validatorAddress: validatorAddress, + blockHeight: 2); + + var signingInfo = ValidatorSigningInfoCtrl.GetSigningInfo(states, validatorAddress)!; + + Assert.NotNull(signingInfo); + Assert.Equal(2, signingInfo.JailedUntil); + } + + [Fact] + public void JailUtil_NotPromotedValidator_FailTest() + { + var states = _states; + var validatorAddress = _validatorAddress; + + Assert.Throws(() => + { + ValidatorSigningInfoCtrl.JailUntil(states, validatorAddress, 2); + }); + } + + [Fact] + public void JailUtil_NegativeBlockHeight_FailTest() + { + var governanceToken = _governanceToken; + var states = _states; + var operatorPublicKey = _operatorPublicKey; + var validatorAddress = _validatorAddress; + + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: governanceToken); + + Assert.Throws(() => + { + ValidatorSigningInfoCtrl.JailUntil(states, validatorAddress, -1); + }); + } + + [Fact] + public void JailUtil_MultipleInvocation_Test() + { + var governanceToken = _governanceToken; + var states = _states; + var operatorPublicKey = _operatorPublicKey; + var validatorAddress = _validatorAddress; + + states = Promote( + states: states, + blockIndex: 1, + operatorPublicKey: operatorPublicKey, + governanceToken: governanceToken); + + states = ValidatorSigningInfoCtrl.JailUntil( + world: states, + validatorAddress: validatorAddress, + blockHeight: 2); + + // Set block height to greater than current + states = ValidatorSigningInfoCtrl.JailUntil( + world: states, + validatorAddress: validatorAddress, + blockHeight: 3); + + var signingInfo1 = ValidatorSigningInfoCtrl.GetSigningInfo(states, validatorAddress)!; + + Assert.NotNull(signingInfo1); + Assert.Equal(3, signingInfo1.JailedUntil); + + // Set block height to lower than current + states = ValidatorSigningInfoCtrl.JailUntil( + world: states, + validatorAddress: validatorAddress, + blockHeight: 1); + + var signingInfo2 = ValidatorSigningInfoCtrl.GetSigningInfo(states, validatorAddress)!; + + Assert.NotNull(signingInfo2); + Assert.Equal(1, signingInfo2.JailedUntil); + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/PoSTest.cs b/.Lib9c.Tests/Action/DPoS/PoSTest.cs index 2cc7dd6ca7..2b810215e2 100644 --- a/.Lib9c.Tests/Action/DPoS/PoSTest.cs +++ b/.Lib9c.Tests/Action/DPoS/PoSTest.cs @@ -1,13 +1,19 @@ namespace Lib9c.Tests.Action.DPoS { + using System.Collections.Immutable; using System.Numerics; using Libplanet.Action.State; using Libplanet.Crypto; using Libplanet.Types.Assets; + using Nekoyume.Action.DPoS.Control; using Nekoyume.Action.DPoS.Misc; + using Nekoyume.Module; public class PoSTest { + protected static readonly ImmutableHashSet NativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + protected static IWorld InitializeStates() { return new World(new MockWorldState()); @@ -24,5 +30,140 @@ protected static FungibleAssetValue ShareFromGovernance(FungibleAssetValue gover protected static FungibleAssetValue ShareFromGovernance(BigInteger amount) => ShareFromGovernance(Asset.GovernanceToken * amount); + + protected static IWorld Promote(IWorld states, long blockIndex, PublicKey operatorPublicKey, FungibleAssetValue governanceToken) + { + var operatorAddress = operatorPublicKey.Address; + states = states.MintAsset( + context: new ActionContext { PreviousState = states, BlockIndex = blockIndex }, + recipient: operatorAddress, + value: governanceToken); + states = ValidatorCtrl.Create( + states, + new ActionContext { PreviousState = states, BlockIndex = blockIndex }, + operatorAddress, + operatorPublicKey, + governanceToken, + NativeTokens); + return states; + } + + protected static IWorld Delegate( + IWorld states, + long blockIndex, + Address delegatorAddress, + Address validatorAddress, + FungibleAssetValue governanceToken) + { + states = states.MintAsset( + context: new ActionContext { PreviousState = states, BlockIndex = blockIndex }, + recipient: delegatorAddress, + value: governanceToken); + states = DelegateCtrl.Execute( + states: states, + ctx: new ActionContext { PreviousState = states, BlockIndex = blockIndex }, + delegatorAddress: delegatorAddress, + validatorAddress: validatorAddress, + governanceToken: governanceToken, + nativeTokens: NativeTokens); + return states; + } + + protected static IWorld Undelegate( + IWorld states, + long blockIndex, + Address delegatorAddress, + Address validatorAddress, + FungibleAssetValue share) + { + states = UndelegateCtrl.Execute( + states: states, + ctx: new ActionContext { PreviousState = states, BlockIndex = blockIndex }, + delegatorAddress: delegatorAddress, + validatorAddress: validatorAddress, + share: share, + nativeTokens: NativeTokens); + return states; + } + + protected static IWorld Redelegate( + IWorld states, + long blockIndex, + Address delegatorAddress, + Address srcValidatorAddress, + Address dstValidatorAddress, + FungibleAssetValue share) + { + states = RedelegateCtrl.Execute( + states: states, + ctx: new ActionContext { PreviousState = states, BlockIndex = blockIndex }, + delegatorAddress: delegatorAddress, + srcValidatorAddress: srcValidatorAddress, + dstValidatorAddress: dstValidatorAddress, + redelegatingShare: share, + nativeTokens: NativeTokens); + return states; + } + + protected static IWorld Update( + IWorld states, + long blockIndex) + { + states = ValidatorSetCtrl.Update( + states: states, + ctx: new ActionContext { PreviousState = states, BlockIndex = blockIndex, }); + return states; + } + + protected static IWorld Jail( + IWorld states, + Address validatorAddress) + { + states = ValidatorCtrl.Jail( + world: states, + validatorAddress: validatorAddress); + return states; + } + + protected static IWorld JailUntil( + IWorld states, + Address validatorAddress, + long blockHeight) + { + states = ValidatorCtrl.JailUntil( + world: states, + validatorAddress: validatorAddress, + blockHeight: blockHeight); + return states; + } + + protected static IWorld Slash( + IWorld states, + long blockIndex, + Address validatorAddress, + long infractionHeight, + BigInteger power, + BigInteger slashFactor) + { + states = SlashCtrl.Slash( + world: states, + actionContext: new ActionContext { PreviousState = states, BlockIndex = blockIndex, }, + validatorAddress: validatorAddress, + infractionHeight: infractionHeight, + power: power, + slashFactor: slashFactor, + nativeTokens: NativeTokens); + return states; + } + + protected static IWorld Tombstone( + IWorld states, + Address validatorAddress) + { + states = ValidatorCtrl.Tombstone( + world: states, + validatorAddress: validatorAddress); + return states; + } } }