diff --git a/Lib9c/Action/DPoS/Control/DelegateCtrl.cs b/Lib9c/Action/DPoS/Control/DelegateCtrl.cs index 10ece08084..ca77e4bc76 100644 --- a/Lib9c/Action/DPoS/Control/DelegateCtrl.cs +++ b/Lib9c/Action/DPoS/Control/DelegateCtrl.cs @@ -24,6 +24,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..ffb2b47d39 --- /dev/null +++ b/Lib9c/Action/DPoS/Control/SlashCtrl.cs @@ -0,0 +1,500 @@ +#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); + + if (validator.Status == BondingStatus.Bonded) + { + world = BurnBondedTokens(world, actionContext, amount: tokensToBurn); + } + else if (validator.Status == BondingStatus.Unbonding || validator.Status == BondingStatus.Unbonded) + { + world = BurnNotBondedTokens(world, actionContext, amount: tokensToBurn); + } + else + { + 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 8bf071f46e..96ecc145f9 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)) { @@ -258,5 +273,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); + } } }