diff --git a/.Lib9c.Miner.Tests/Lib9c.Proposer.Tests.csproj b/.Lib9c.Miner.Tests/Lib9c.Proposer.Tests.csproj index 059f555c09..e5b1dba72c 100644 --- a/.Lib9c.Miner.Tests/Lib9c.Proposer.Tests.csproj +++ b/.Lib9c.Miner.Tests/Lib9c.Proposer.Tests.csproj @@ -22,6 +22,7 @@ + diff --git a/.Lib9c.Miner.Tests/ProposerTest.cs b/.Lib9c.Miner.Tests/ProposerTest.cs new file mode 100644 index 0000000000..aae4ba2857 --- /dev/null +++ b/.Lib9c.Miner.Tests/ProposerTest.cs @@ -0,0 +1,145 @@ +using System.Collections.Immutable; +using Lib9c.Renderers; +using Libplanet.Action; +using Libplanet.Blockchain.Policies; +using Libplanet.Blockchain; +using Libplanet.Crypto; +using Libplanet.Store; +using Libplanet.Store.Trie; +using Libplanet.Types.Assets; +using Libplanet.Types.Blocks; +using Libplanet.Types.Consensus; +using Libplanet.Types.Tx; +using Nekoyume.Action; +using Nekoyume.Action.Loader; +using Nekoyume.Blockchain.Policy; +using Nekoyume.Model.State; +using System.Numerics; + +namespace Lib9c.Proposer.Tests +{ + public class ProposerTest + { + private readonly PrivateKey _admin; + private readonly PrivateKey _proposer; + private readonly BlockChain _blockChain; + + public ProposerTest() + { + _admin = new PrivateKey(); + _proposer = new PrivateKey(); + var ncg = Currency.Uncapped("ncg", 2, null); + var policy = new DebugPolicy(); + var actionTypeLoader = new NCActionLoader(); + IStagePolicy stagePolicy = new VolatileStagePolicy(); + var mint = new PrepareRewardAssets + { + RewardPoolAddress = _proposer.Address, + Assets = new List + { + 1 * Currencies.Mead, + }, + }; + + var validatorSet = new ValidatorSet( + new List { new(_proposer.PublicKey, 10_000_000_000_000_000_000) }); + + var initializeStates = new InitializeStates( + validatorSet, + new RankingState0(), + new ShopState(), + new Dictionary(), + new GameConfigState(), + new RedeemCodeState(new Dictionary()), + new ActivatedAccountsState(), + new GoldCurrencyState(ncg), + new GoldDistribution[] { }, + new PendingActivationState[] { }); + + List actions = new List + { + initializeStates, + }; + + var genesis = BlockChain.ProposeGenesisBlock( + privateKey: _proposer, + transactions: ImmutableList.Empty + .Add(Transaction.Create( + 0, _proposer, null, actions.ToPlainValues())), + timestamp: DateTimeOffset.MinValue); + + var store = new MemoryStore(); + var stateStore = new TrieStateStore(new MemoryKeyValueStore()); + + _blockChain = BlockChain.Create( + policy, + stagePolicy, + store, + stateStore, + genesis, + new ActionEvaluator( + policy.PolicyActionsRegistry, + stateStore, + new NCActionLoader() + ), + new[] { new BlockRenderer(), } + ); + } + + [Fact] + public void ProposeBlock() + { + Block block = _blockChain.ProposeBlock(_proposer); + _blockChain.Append( + block, + GenerateBlockCommit( + block, + _proposer, + 10_000_000_000_000_000_000)); + } + + [Fact] + public void AssertInvalidProposer() + { + Block block = _blockChain.ProposeBlock(_proposer); + Assert.Throws(() => _blockChain.Append( + block, + GenerateBlockCommit( + block, + new PrivateKey(), + 10_000_000_000_000_000_000))); + } + + [Fact] + public void AssertInvalidPower() + { + Block block = _blockChain.ProposeBlock(_proposer); + Assert.Throws(() => _blockChain.Append( + block, + GenerateBlockCommit( + block, + _proposer, + 10_000_000_000_000_000))); + } + + private BlockCommit? GenerateBlockCommit( + Block block, PrivateKey privateKey, BigInteger power) + { + return block.Index != 0 + ? new BlockCommit( + block.Index, + 0, + block.Hash, + ImmutableArray.Create( + new VoteMetadata( + block.Index, + 0, + block.Hash, + DateTimeOffset.UtcNow, + privateKey.PublicKey, + power, + VoteFlag.PreCommit).Sign(privateKey))) + : null; + } + } +} diff --git a/.Lib9c.Tests/Action/Guild/Migration/MigratePlanetariumValidatorTest.cs b/.Lib9c.Tests/Action/Guild/Migration/MigratePlanetariumValidatorTest.cs new file mode 100644 index 0000000000..cd15015166 --- /dev/null +++ b/.Lib9c.Tests/Action/Guild/Migration/MigratePlanetariumValidatorTest.cs @@ -0,0 +1,116 @@ +namespace Lib9c.Tests.Action.Guild.Migration +{ + using System; + using System.Collections.Generic; + using System.Numerics; + using Lib9c.Tests.Util; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Libplanet.Types.Consensus; + using Nekoyume.Action.Guild; + using Nekoyume.Action.Guild.Migration; + using Nekoyume.Action.Guild.Migration.LegacyModels; + using Nekoyume.Action.ValidatorDelegation; + using Nekoyume.Model.Guild; + using Nekoyume.Model.Stake; + using Nekoyume.TypedAddress; + using Nekoyume.ValidatorDelegation; + using Xunit; + + // TODO: Remove this test class after the migration is completed. + public class MigratePlanetariumValidatorTest : GuildTestBase + { + [Fact] + public void Execute() + { + var guildAddress = AddressUtil.CreateGuildAddress(); + var validatorKey = new PrivateKey().PublicKey; + var validatorAddress = validatorKey.Address; + var power = 10_000_000_000_000_000_000; + var guildGold = Currencies.GuildGold; + var delegated = FungibleAssetValue.FromRawValue(guildGold, power); + + var world = EnsureLegacyPlanetariumValidator( + World, guildAddress, validatorKey, power); + + var guildRepository = new GuildRepository(world, new ActionContext { }); + var validatorRepository = new ValidatorRepository(world, new ActionContext { }); + var guildDelegatee = guildRepository.GetGuildDelegatee(validatorAddress); + var validatorDelegatee = validatorRepository.GetValidatorDelegatee(validatorAddress); + + Assert.False(validatorDelegatee.IsActive); + Assert.Equal(ValidatorDelegatee.InactiveDelegationPoolAddress, guildDelegatee.DelegationPoolAddress); + Assert.Equal(ValidatorDelegatee.InactiveDelegationPoolAddress, validatorDelegatee.DelegationPoolAddress); + Assert.Equal(delegated, world.GetBalance(ValidatorDelegatee.InactiveDelegationPoolAddress, Currencies.GuildGold)); + Assert.Equal(guildGold * 0, world.GetBalance(ValidatorDelegatee.ActiveDelegationPoolAddress, Currencies.GuildGold)); + + var action = new MigratePlanetariumValidator(); + var actionContext = new ActionContext + { + PreviousState = world, + Signer = new PrivateKey().Address, + }; + world = action.Execute(actionContext); + + guildRepository = new GuildRepository(world, new ActionContext { }); + validatorRepository = new ValidatorRepository(world, new ActionContext { }); + guildDelegatee = guildRepository.GetGuildDelegatee(validatorAddress); + validatorDelegatee = validatorRepository.GetValidatorDelegatee(validatorAddress); + + Assert.True(validatorRepository.GetValidatorDelegatee(validatorAddress).IsActive); + Assert.Equal(ValidatorDelegatee.ActiveDelegationPoolAddress, guildDelegatee.DelegationPoolAddress); + Assert.Equal(ValidatorDelegatee.ActiveDelegationPoolAddress, validatorDelegatee.DelegationPoolAddress); + Assert.Equal(delegated, world.GetBalance(ValidatorDelegatee.ActiveDelegationPoolAddress, Currencies.GuildGold)); + Assert.Equal(guildGold * 0, world.GetBalance(ValidatorDelegatee.InactiveDelegationPoolAddress, Currencies.GuildGold)); + + Assert.Throws(() => + { + var actionContext = new ActionContext + { + PreviousState = world, + Signer = new PrivateKey().Address, + }; + + world = action.Execute(actionContext); + }); + } + + private static IWorld EnsureLegacyPlanetariumValidator( + IWorld world, GuildAddress guildAddress, PublicKey validatorKey, BigInteger power) + { + world = world.SetDelegationMigrationHeight(0L); + + var toDelegate = FungibleAssetValue.FromRawValue(Currencies.GuildGold, power); + world = world + .MintAsset( + new ActionContext { }, + StakeState.DeriveAddress(validatorKey.Address), + toDelegate) + .MintAsset( + new ActionContext { }, + validatorKey.Address, + Currencies.Mead * 1); + + world = new PromoteValidator(validatorKey, toDelegate).Execute(new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + }); + + world = new MakeGuild(validatorKey.Address).Execute(new ActionContext + { + PreviousState = world, + Signer = GuildConfig.PlanetariumGuildOwner, + }); + + world = world.SetValidatorSet(new ValidatorSet( + new List + { + new Validator(validatorKey, power), + })); + + return world; + } + } +} diff --git a/.Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs b/.Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs index 3173039718..27699ba63a 100644 --- a/.Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs +++ b/.Lib9c.Tests/Action/ValidatorDelegation/SlashValidatorTest.cs @@ -210,4 +210,50 @@ public void Execute_ToJailedValidator_ThenNothingHappens() Assert.Equal(expectedJailed, actualJailed); } + + [Fact] + public void Execute_ByAbstain_ToJailedValidator_ThenNothingHappens() + { + // Given + var world = World; + var validatorKey = new PrivateKey(); + var height = 1L; + var actionContext = new ActionContext(); + world = EnsureToMintAsset(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsurePromotedValidator(world, validatorKey, DelegationCurrency * 10, height++); + world = EnsureJailedValidator(world, validatorKey, ref height); + + var expectedRepository = new ValidatorRepository(world, actionContext); + var expectedDelegatee = expectedRepository.GetValidatorDelegatee(validatorKey.Address); + var expectedTotalDelegated = expectedDelegatee.TotalDelegated; + + // When + for (var i = 0L; i <= AbstainHistory.MaxAbstainAllowance; i++) + { + var vote = CreateNullVote(validatorKey, height - 1); + var lastCommit = new BlockCommit( + height: height - 1, + round: 0, + blockHash: vote.BlockHash, + ImmutableArray.Create(vote)); + var slashValidator = new SlashValidator(); + actionContext = new ActionContext + { + PreviousState = world, + Signer = validatorKey.Address, + BlockIndex = height++, + LastCommit = lastCommit, + }; + world = slashValidator.Execute(actionContext); + } + + // Then + var actualRepisitory = new ValidatorRepository(world, actionContext); + var actualDelegatee = actualRepisitory.GetValidatorDelegatee(validatorKey.Address); + var actualTotalDelegated = actualDelegatee.TotalDelegated; + + Assert.True(actualDelegatee.Jailed); + Assert.False(actualDelegatee.Tombstoned); + Assert.Equal(expectedTotalDelegated, actualTotalDelegated); + } } diff --git a/Directory.Build.props b/Directory.Build.props index af6e2c7447..f18e8496d7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 5.4.0 + 5.4.1 diff --git a/Lib9c.Policy/Policy/DebugPolicy.cs b/Lib9c.Policy/Policy/DebugPolicy.cs index e10765da2f..8a1f5d1fbd 100644 --- a/Lib9c.Policy/Policy/DebugPolicy.cs +++ b/Lib9c.Policy/Policy/DebugPolicy.cs @@ -5,6 +5,7 @@ using Libplanet.Types.Blocks; using Libplanet.Types.Tx; using Nekoyume.Action; +using Nekoyume.Action.ValidatorDelegation; namespace Nekoyume.Blockchain.Policy { @@ -14,7 +15,25 @@ public DebugPolicy() { } - public IPolicyActionsRegistry PolicyActionsRegistry { get; } = new PolicyActionsRegistry(); + public IPolicyActionsRegistry PolicyActionsRegistry { get; } = + new PolicyActionsRegistry( + beginBlockActions: new IAction[] { + new SlashValidator(), + new AllocateGuildReward(), + new AllocateReward(), + }.ToImmutableArray(), + endBlockActions: new IAction[] { + new UpdateValidators(), + new RecordProposer(), + new RewardGold(), + new ReleaseValidatorUnbondings(), + }.ToImmutableArray(), + beginTxActions: new IAction[] { + new Mortgage(), + }.ToImmutableArray(), + endTxActions: new IAction[] { + new Reward(), new Refund(), + }.ToImmutableArray()); public TxPolicyViolationException ValidateNextBlockTx( BlockChain blockChain, Transaction transaction) diff --git a/Lib9c.Proposer/Proposer.cs b/Lib9c.Proposer/Proposer.cs index 65abac0838..0810c19713 100644 --- a/Lib9c.Proposer/Proposer.cs +++ b/Lib9c.Proposer/Proposer.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Threading; using Libplanet.Action; +using Libplanet.Action.State; using Libplanet.Blockchain; using Libplanet.Crypto; using Libplanet.Types.Blocks; @@ -33,6 +34,16 @@ public class Proposer block = _chain.ProposeBlock( _privateKey, lastCommit: lastCommit); + + if (!(_chain.GetNextWorldState() is IWorldState worldState)) + { + throw new InvalidOperationException( + "Failed to get next world state. Appending is not completed."); + } + + var proposerPower = worldState.GetValidatorSet().GetValidatorsPower( + new List { _privateKey.PublicKey }); + BlockCommit? commit = block.Index > 0 ? new BlockCommit( block.Index, @@ -45,7 +56,7 @@ public class Proposer block.Hash, DateTimeOffset.UtcNow, _privateKey.PublicKey, - null, + proposerPower, VoteFlag.PreCommit).Sign(_privateKey))) : null; _chain.Append(block, commit); diff --git a/Lib9c/Action/Guild/Migration/LegacyModels/MigrationModule.cs b/Lib9c/Action/Guild/Migration/LegacyModels/MigrationModule.cs index 4d2724c6f5..493282e363 100644 --- a/Lib9c/Action/Guild/Migration/LegacyModels/MigrationModule.cs +++ b/Lib9c/Action/Guild/Migration/LegacyModels/MigrationModule.cs @@ -1,4 +1,3 @@ -using System; using Bencodex.Types; using Libplanet.Action.State; using Libplanet.Crypto; @@ -23,11 +22,6 @@ public static readonly Address DelegationMigrationHeight public static IWorld SetDelegationMigrationHeight(this IWorld world, long height) { - if (world.GetDelegationMigrationHeight() is long) - { - throw new InvalidOperationException("Cannot overwrite delegation migration index."); - } - return world .MutateAccount( Addresses.Migration, diff --git a/Lib9c/Action/Guild/Migration/MigrateDelegation.cs b/Lib9c/Action/Guild/Migration/MigrateDelegation.cs index 65dbec9dde..14e77621ac 100644 --- a/Lib9c/Action/Guild/Migration/MigrateDelegation.cs +++ b/Lib9c/Action/Guild/Migration/MigrateDelegation.cs @@ -119,23 +119,28 @@ public override IWorld Execute(IActionContext context) return repository.World; } - catch (FailedLoadStateException) + catch (Exception e) { - var pledgeAddress = ((Address)Target).GetPledgeAddress(); - - // Patron contract structure: - // [0] = PatronAddress - // [1] = IsApproved - // [2] = Mead amount to refill. - if (!world.TryGetLegacyState(pledgeAddress, out List list) || list.Count < 3 || - list[0] is not Binary || list[0].ToAddress() != MeadConfig.PatronAddress || - list[1] is not Bencodex.Types.Boolean approved || !approved) + if (e is FailedLoadStateException || e is NullReferenceException) { - throw new GuildMigrationFailedException("Unexpected pledge structure."); + var pledgeAddress = ((Address)Target).GetPledgeAddress(); + + // Patron contract structure: + // [0] = PatronAddress + // [1] = IsApproved + // [2] = Mead amount to refill. + if (!world.TryGetLegacyState(pledgeAddress, out List list) || list.Count < 3 || + list[0] is not Binary || list[0].ToAddress() != MeadConfig.PatronAddress || + list[1] is not Bencodex.Types.Boolean approved || !approved) + { + throw new GuildMigrationFailedException("Unexpected pledge structure."); + } + + repository.JoinGuild(planetariumGuildAddress, Target); + return repository.World; } - repository.JoinGuild(planetariumGuildAddress, Target); - return repository.World; + throw; } } } diff --git a/Lib9c/Action/Guild/Migration/MigrateDelegationHeight.cs b/Lib9c/Action/Guild/Migration/MigrateDelegationHeight.cs index 48d1ad2b9d..dfd5aaa531 100644 --- a/Lib9c/Action/Guild/Migration/MigrateDelegationHeight.cs +++ b/Lib9c/Action/Guild/Migration/MigrateDelegationHeight.cs @@ -3,6 +3,7 @@ using Libplanet.Action; using Libplanet.Action.State; using Nekoyume.Action.Guild.Migration.LegacyModels; +using Nekoyume.Model.State; namespace Nekoyume.Action.Guild.Migration { @@ -53,6 +54,16 @@ public override IWorld Execute(IActionContext context) var world = context.PreviousState; + if (!TryGetAdminState(context, out AdminState adminState)) + { + throw new InvalidOperationException("Couldn't find admin state"); + } + + if (context.Signer != adminState.AdminAddress) + { + throw new PermissionDeniedException(adminState, context.Signer); + } + return world.SetDelegationMigrationHeight(Height); } } diff --git a/Lib9c/Action/Guild/Migration/MigratePlanetariumValidator.cs b/Lib9c/Action/Guild/Migration/MigratePlanetariumValidator.cs new file mode 100644 index 0000000000..0a91936bf2 --- /dev/null +++ b/Lib9c/Action/Guild/Migration/MigratePlanetariumValidator.cs @@ -0,0 +1,83 @@ +using System; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Nekoyume.Model.Guild; +using Nekoyume.Module.Guild; +using Nekoyume.ValidatorDelegation; + +namespace Nekoyume.Action.Guild.Migration +{ + // TODO: [GuildMigration] Remove this class when the migration is done. + /// + /// An action to migrate the planetarium guild. + /// + [ActionType(TypeIdentifier)] + public class MigratePlanetariumValidator : ActionBase + { + public const string TypeIdentifier = "migrate_planetarium_validator"; + + public MigratePlanetariumValidator() + { + } + + public override IValue PlainValue => Dictionary.Empty + .Add("type_id", TypeIdentifier) + .Add("values", Null.Value); + + public override void LoadPlainValue(IValue plainValue) + { + if (plainValue is not Dictionary root || + !root.TryGetValue((Text)"values", out var rawValues) || + rawValues is not Null) + { + throw new InvalidCastException(); + } + } + + public override IWorld Execute(IActionContext context) + { + GasTracer.UseGas(1); + + var world = context.PreviousState; + var guildRepository = new GuildRepository(world, context); + var guildAddress = guildRepository.GetJoinedGuild(GuildConfig.PlanetariumGuildOwner); + if (guildAddress is not { } planetariumGuildAddress) + { + throw new InvalidOperationException("The planetarium guild not exists"); + } + + var planetariumGuild = guildRepository.GetGuild( + planetariumGuildAddress); + + var validatorRepository = new ValidatorRepository(guildRepository); + var validatorDelegatee = validatorRepository.GetValidatorDelegatee( + planetariumGuild.ValidatorAddress); + + var validatorSet = world.GetValidatorSet(); + + if (!validatorSet.ContainsPublicKey(validatorDelegatee.PublicKey)) + { + throw new InvalidOperationException( + "The planetarium validator is not in the validator set."); + } + + if (validatorDelegatee.IsActive) + { + throw new InvalidOperationException( + "The planetarium validator is already active."); + } + + validatorDelegatee.Activate(); + validatorRepository.SetValidatorDelegatee(validatorDelegatee); + + guildRepository.UpdateWorld(validatorRepository.World); + var guildDelegatee = guildRepository.GetGuildDelegatee( + planetariumGuild.ValidatorAddress); + guildDelegatee.Activate(); + guildRepository.SetGuildDelgatee(guildDelegatee); + + return guildRepository.World; + } + } +} diff --git a/Lib9c/Action/ValidatorDelegation/SlashValidator.cs b/Lib9c/Action/ValidatorDelegation/SlashValidator.cs index 5d9fc74550..e97424efc0 100644 --- a/Lib9c/Action/ValidatorDelegation/SlashValidator.cs +++ b/Lib9c/Action/ValidatorDelegation/SlashValidator.cs @@ -52,6 +52,11 @@ public override IWorld Execute(IActionContext context) foreach (var abstain in abstainsToSlash) { var validatorDelegatee = repository.GetValidatorDelegatee(abstain.Address); + if (validatorDelegatee.Jailed) + { + continue; + } + validatorDelegatee.Slash(LivenessSlashFactor, context.BlockIndex, context.BlockIndex); validatorDelegatee.Jail(context.BlockIndex + AbstainJailTime); diff --git a/Lib9c/Delegation/Delegatee.cs b/Lib9c/Delegation/Delegatee.cs index e18ae6d3e8..52db964657 100644 --- a/Lib9c/Delegation/Delegatee.cs +++ b/Lib9c/Delegation/Delegatee.cs @@ -295,7 +295,7 @@ record = record.AddLumpSumRewards(rewards); Repository.SetLumpSumRewardsRecord(record); } - public void Slash(BigInteger slashFactor, long infractionHeight, long height) + public virtual void Slash(BigInteger slashFactor, long infractionHeight, long height) { FungibleAssetValue slashed = TotalDelegated.DivRem(slashFactor, out var rem); if (rem.Sign > 0) diff --git a/Lib9c/Model/Guild/GuildDelegatee.cs b/Lib9c/Model/Guild/GuildDelegatee.cs index 079fe52377..1ffedb2514 100644 --- a/Lib9c/Model/Guild/GuildDelegatee.cs +++ b/Lib9c/Model/Guild/GuildDelegatee.cs @@ -1,6 +1,7 @@ #nullable enable using System; using System.Collections.Generic; +using System.Numerics; using Libplanet.Crypto; using Libplanet.Types.Assets; using Nekoyume.Delegation; @@ -40,6 +41,23 @@ public GuildDelegatee( { } + public override void Slash(BigInteger slashFactor, long infractionHeight, long height) + { + FungibleAssetValue slashed = TotalDelegated.DivRem(slashFactor, out var rem); + if (rem.Sign > 0) + { + slashed += FungibleAssetValue.FromRawValue(rem.Currency, 1); + } + + if (slashed > Metadata.TotalDelegatedFAV) + { + slashed = Metadata.TotalDelegatedFAV; + } + + Metadata.RemoveDelegatedFAV(slashed); + Repository.SetDelegateeMetadata(Metadata); + } + public void Activate() { Metadata.DelegationPoolAddress = ValidatorDelegatee.ActiveDelegationPoolAddress; diff --git a/Lib9c/Module/ValidatorDelegation/ValidatorDelegateeModule.cs b/Lib9c/Module/ValidatorDelegation/ValidatorDelegateeModule.cs index 13f792cbef..7581ed37f6 100644 --- a/Lib9c/Module/ValidatorDelegation/ValidatorDelegateeModule.cs +++ b/Lib9c/Module/ValidatorDelegation/ValidatorDelegateeModule.cs @@ -35,7 +35,7 @@ public static ValidatorDelegatee CreateValidatorDelegatee( if (repository.TryGetValidatorDelegatee(publicKey.Address, out _)) { - throw new InvalidOperationException("The signer already has a validator delegatee."); + throw new InvalidOperationException("The public key already has a validator delegatee."); } var validatorDelegatee = new ValidatorDelegatee( diff --git a/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs b/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs index c3fa0d537b..7ebe20bea4 100644 --- a/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs +++ b/Lib9c/ValidatorDelegation/ValidatorDelegatee.cs @@ -199,6 +199,11 @@ public void SetCommissionPercentage(BigInteger percentage, long height) public void Activate() { + if (IsActive) + { + throw new InvalidOperationException("The validator is already active."); + } + ValidatorRepository repository = (ValidatorRepository)Repository; IsActive = true; Metadata.DelegationPoolAddress = ActiveDelegationPoolAddress; @@ -214,6 +219,11 @@ public void Activate() public void Deactivate() { + if (!IsActive) + { + throw new InvalidOperationException("The validator is already inactive."); + } + ValidatorRepository repository = (ValidatorRepository)Repository; IsActive = false; Metadata.DelegationPoolAddress = InactiveDelegationPoolAddress;