diff --git a/.Lib9c.Benchmarks/Program.cs b/.Lib9c.Benchmarks/Program.cs index 6b4126dd01..64dda8dbcb 100644 --- a/.Lib9c.Benchmarks/Program.cs +++ b/.Lib9c.Benchmarks/Program.cs @@ -81,7 +81,11 @@ static void Main(string[] args) IKeyValueStore stateKeyValueStore = new RocksDBKeyValueStore(Path.Combine(storePath, "states")); var stateStore = new TrieStateStore(stateKeyValueStore); var actionEvaluator = new ActionEvaluator( - _ => policy.BlockAction, + new PolicyActionsRegistry( + _ => policy.BeginBlockActions, + _ => policy.EndBlockActions, + _ => policy.BeginTxActions, + _ => policy.EndTxActions), stateStore, new NCActionLoader()); var chain = new BlockChain( diff --git a/.Lib9c.Plugin/PluginActionEvaluator.cs b/.Lib9c.Plugin/PluginActionEvaluator.cs index 7c76b9602e..cf3483f585 100644 --- a/.Lib9c.Plugin/PluginActionEvaluator.cs +++ b/.Lib9c.Plugin/PluginActionEvaluator.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.Security.Cryptography; using Lib9c.Plugin.Shared; using Libplanet.Action; @@ -5,6 +6,7 @@ using Libplanet.Extensions.ActionEvaluatorCommonComponents; using Libplanet.Store; using Nekoyume.Action; +using Nekoyume.Action.DPoS.Sys; using Nekoyume.Action.Loader; @@ -18,7 +20,11 @@ public PluginActionEvaluator(IPluginKeyValueStore keyValueStore) { var stateStore = new TrieStateStore(new WrappedKeyValueStore(keyValueStore)); _actionEvaluator = new ActionEvaluator( - _ => new RewardGold(), + new PolicyActionsRegistry( + _ => new IAction[] { }.ToImmutableArray(), + _ => new IAction[] { new RewardGold() }.ToImmutableArray(), + _ => new IAction[] { }.ToImmutableArray(), + _ => new IAction[] { }.ToImmutableArray()), stateStore, new NCActionLoader()); } diff --git a/.Lib9c.Tests/Action/ActionContext.cs b/.Lib9c.Tests/Action/ActionContext.cs index 9c157693b4..48fd6a72c0 100644 --- a/.Lib9c.Tests/Action/ActionContext.cs +++ b/.Lib9c.Tests/Action/ActionContext.cs @@ -5,6 +5,7 @@ namespace Lib9c.Tests.Action using Libplanet.Action.State; using Libplanet.Common; using Libplanet.Crypto; + using Libplanet.Types.Assets; using Libplanet.Types.Blocks; using Libplanet.Types.Tx; @@ -28,13 +29,17 @@ public class ActionContext : IActionContext public int BlockProtocolVersion { get; set; } + public BlockCommit LastCommit { get; set; } + public IWorld PreviousState { get; set; } public int RandomSeed { get; set; } public HashDigest? PreviousStateRootHash { get; set; } - public bool BlockAction { get; } + public bool IsBlockAction { get; } + + public FungibleAssetValue? MaxGasPrice { get; set; } public void UseGas(long gas) { diff --git a/.Lib9c.Tests/Action/DPoS/BlockTest.cs b/.Lib9c.Tests/Action/DPoS/BlockTest.cs new file mode 100644 index 0000000000..edc3dc65ed --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/BlockTest.cs @@ -0,0 +1,67 @@ +namespace Lib9c.Tests.Action.DPoS +{ + using System; + using System.Collections.Immutable; + using Libplanet.Action; + using Libplanet.Action.Loader; + using Libplanet.Blockchain; + using Libplanet.Blockchain.Policies; + using Libplanet.Crypto; + using Libplanet.Store; + using Libplanet.Store.Trie; + using Libplanet.Types.Tx; + using Nekoyume.Action; + using Nekoyume.Action.DPoS.Sys; + using Serilog; + using Xunit.Abstractions; + + public class BlockTest + { + private IBlockPolicy _policy; + private BlockChain _blockChain; + + public BlockTest(ITestOutputHelper outputHelper) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.TestOutput(outputHelper) + .CreateLogger(); + + var genesisProposer = new PrivateKey(); + _policy = new BlockPolicy( + beginBlockActions: new IAction[] { new AllocateReward() }.ToImmutableArray(), + endBlockActions: new IAction[] + { + new UpdateValidators(), + new RecordProposer(), + }.ToImmutableArray(), + beginTxActions: new IAction[] { new Mortgage() }.ToImmutableArray(), + endTxActions: new IAction[] { new Refund(), new Reward() }.ToImmutableArray()); + + var store = new MemoryStore(); + var stateStore = new TrieStateStore(new MemoryKeyValueStore()); + var actionEvaluator = new ActionEvaluator( + new PolicyActionsRegistry( + _ => _policy.BeginBlockActions, + _ => _policy.EndBlockActions, + _ => _policy.BeginTxActions, + _ => _policy.EndTxActions), + stateStore: stateStore, + actionTypeLoader: new SingleActionLoader(typeof(ActionBase))); + var genesisBlock = BlockChain.ProposeGenesisBlock( + actionEvaluator, + transactions: ImmutableList.Empty, + privateKey: genesisProposer, + timestamp: DateTimeOffset.UtcNow); + var blockChainStates = new BlockChainStates(store, stateStore); + _blockChain = new BlockChain( + policy: _policy, + stagePolicy: new VolatileStagePolicy(), + store: new MemoryStore(), + stateStore: stateStore, + genesisBlock: genesisBlock, + blockChainStates: blockChainStates, + actionEvaluator: actionEvaluator); + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/Control/DelegateCtrlTest.cs b/.Lib9c.Tests/Action/DPoS/Control/DelegateCtrlTest.cs new file mode 100644 index 0000000000..e490bd912a --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Control/DelegateCtrlTest.cs @@ -0,0 +1,196 @@ +namespace Lib9c.Tests.Action.DPoS.Control +{ + using System.Collections.Immutable; + 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 DelegateCtrlTest : PoSTest + { + private readonly PublicKey _operatorPublicKey; + private readonly Address _operatorAddress; + private readonly Address _delegatorAddress; + private readonly Address _validatorAddress; + private ImmutableHashSet _nativeTokens; + private IWorld _states; + + public DelegateCtrlTest() + { + _operatorPublicKey = new PrivateKey().PublicKey; + _operatorAddress = _operatorPublicKey.Address; + _delegatorAddress = CreateAddress(); + _validatorAddress = Validator.DeriveAddress(_operatorAddress); + _nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + _states = InitializeStates(); + } + + [Fact] + public void InvalidCurrencyTest() + { + Initialize(500, 500, 100); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + Asset.ConsensusFromGovernance(50)); + Assert.Throws( + () => _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.ConsensusFromGovernance(30), + _nativeTokens)); + } + + [Fact] + public void InvalidValidatorTest() + { + Initialize(500, 500, 100); + Assert.Throws( + () => _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + CreateAddress(), + Asset.GovernanceToken * 10, + _nativeTokens)); + } + + [Fact] + public void InvalidShareTest() + { + Initialize(500, 500, 100); + _states = _states.BurnAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _validatorAddress, + Asset.ConsensusFromGovernance(100)); + Assert.Throws( + () => _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.GovernanceToken * 10, + _nativeTokens)); + } + + [Theory] + [InlineData(500, 500, 100, 10)] + [InlineData(500, 500, 100, 20)] + public void BalanceTest( + int operatorMintAmount, + int delegatorMintAmount, + int selfDelegateAmount, + int delegateAmount) + { + Initialize(operatorMintAmount, delegatorMintAmount, selfDelegateAmount); + _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.GovernanceToken * delegateAmount, + _nativeTokens); + Assert.Equal( + Asset.GovernanceToken * 0, + _states.GetBalance(_validatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.ConsensusFromGovernance(0), + _states.GetBalance(_operatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.ConsensusFromGovernance(0), + _states.GetBalance(_delegatorAddress, Asset.ConsensusToken)); + Assert.Equal( + ShareFromGovernance(0), + _states.GetBalance(_operatorAddress, Asset.Share)); + Assert.Equal( + ShareFromGovernance(0), + _states.GetBalance(_delegatorAddress, Asset.Share)); + Assert.Equal( + Asset.ConsensusFromGovernance(selfDelegateAmount + delegateAmount), + _states.GetBalance(_validatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.GovernanceToken * (operatorMintAmount - selfDelegateAmount), + _states.GetBalance(_operatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (selfDelegateAmount + delegateAmount), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + var balanceA = _states.GetBalance( + Delegation.DeriveAddress(_operatorAddress, _validatorAddress), + Asset.Share); + var balanceB = _states.GetBalance( + Delegation.DeriveAddress(_delegatorAddress, _validatorAddress), + Asset.Share); + Assert.Equal( + ValidatorCtrl.GetValidator(_states, _validatorAddress)!.DelegatorShares, + balanceA + balanceB); + } + + private void Initialize( + int operatorMintAmount, int delegatorMintAmount, int selfDelegateAmount) + { + _states = InitializeStates(); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + Asset.GovernanceToken * operatorMintAmount); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + Asset.GovernanceToken * delegatorMintAmount); + _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + _operatorPublicKey, + Asset.GovernanceToken * selfDelegateAmount, + _nativeTokens); + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/Control/DistributeTest.cs b/.Lib9c.Tests/Action/DPoS/Control/DistributeTest.cs new file mode 100644 index 0000000000..2cf8c5359b --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Control/DistributeTest.cs @@ -0,0 +1,265 @@ +namespace Lib9c.Tests.Action.DPoS.Control +{ + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Libplanet.Types.Blocks; + using Libplanet.Types.Consensus; + using Nekoyume.Action.DPoS.Control; + using Nekoyume.Action.DPoS.Misc; + using Nekoyume.Action.DPoS.Model; + using Nekoyume.Module; + using Xunit; + using Validator = Nekoyume.Action.DPoS.Model.Validator; + + public class DistributeTest : PoSTest + { + private readonly ImmutableHashSet _nativeTokens; + private IWorld _states; + + public DistributeTest() + : base() + { + _nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + _states = InitializeStates(); + OperatorPrivateKeys = new List(); + OperatorPublicKeys = new List(); + OperatorAddresses = new List
(); + ValidatorAddresses = new List
(); + DelegatorAddress = CreateAddress(); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + DelegatorAddress, + Asset.GovernanceToken * 100000); + for (int i = 0; i < 200; i++) + { + PrivateKey operatorPrivateKey = new PrivateKey(); + PublicKey operatorPublicKey = operatorPrivateKey.PublicKey; + Address operatorAddress = operatorPublicKey.Address; + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + operatorAddress, + Asset.GovernanceToken * 1000); + + OperatorPrivateKeys.Add(operatorPrivateKey); + OperatorPublicKeys.Add(operatorPublicKey); + OperatorAddresses.Add(operatorAddress); + _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + operatorAddress, + operatorPublicKey, + Asset.GovernanceToken * 1, + _nativeTokens); + ValidatorAddresses.Add(Validator.DeriveAddress(operatorAddress)); + } + } + + private List OperatorPrivateKeys { get; set; } + + private List OperatorPublicKeys { get; set; } + + private List
OperatorAddresses { get; set; } + + private List
ValidatorAddresses { get; set; } + + private Address DelegatorAddress { get; set; } + + [Fact] + public void ValidatorSetTest() + { + for (int i = 0; i < 200; i++) + { + _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + DelegatorAddress, + ValidatorAddresses[i], + Asset.GovernanceToken * (i + 1), + _nativeTokens); + } + + Address validatorAddressA = ValidatorAddresses[3]; + Address validatorAddressB = ValidatorAddresses[5]; + + _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + DelegatorAddress, + validatorAddressA, + Asset.GovernanceToken * 200, + _nativeTokens); + + _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + DelegatorAddress, + validatorAddressB, + Asset.GovernanceToken * 300, + _nativeTokens); + + _states = ValidatorSetCtrl.Update( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }); + + Nekoyume.Action.DPoS.Model.ValidatorSet validatorSet; + (_states, validatorSet) = ValidatorSetCtrl.FetchBondedValidatorSet(_states); + var blockHash = BlockHash.FromString( + "0000000000000000000000000000000000000000000000000000000000000001"); + + List votes = validatorSet.Set.Select( + validator => + { + bool joined = + validator.OperatorPublicKey.Equals(OperatorPrivateKeys[3].PublicKey) || + validator.OperatorPublicKey.Equals(OperatorPrivateKeys[5].PublicKey); + return new VoteMetadata( + default, + default, + blockHash, + default, + validator.OperatorPublicKey, + validator.ConsensusToken.RawValue, + joined ? VoteFlag.PreCommit : VoteFlag.Null).Sign( + joined ? OperatorPrivateKeys.First( + pk => pk.PublicKey.Equals(validator.OperatorPublicKey)) : null); + }) + .ToList(); + FungibleAssetValue blockReward = Asset.ConsensusFromGovernance(50); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ReservedAddress.RewardPool, + blockReward); + _states = AllocateRewardCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _nativeTokens, + votes, + new ProposerInfo(0, OperatorAddresses[3])); + + var (baseProposerReward, _) + = (blockReward * AllocateRewardCtrl.BaseProposerRewardNumerator) + .DivRem(AllocateRewardCtrl.BaseProposerRewardDenominator); + var (bonusProposerReward, _) + = (blockReward * (205 + 307) + * AllocateRewardCtrl.BonusProposerRewardNumerator) + .DivRem((100 + (101 + 200) * 50 - 101 - 102 + 204 + 306) + * AllocateRewardCtrl.BonusProposerRewardDenominator); + FungibleAssetValue proposerReward = baseProposerReward + bonusProposerReward; + FungibleAssetValue validatorRewardSum = blockReward - proposerReward; + + var (validatorRewardA, _) + = (validatorRewardSum * 205) + .DivRem(100 + (101 + 200) * 50 - 101 - 102 + 204 + 306); + var (commissionA, _) + = (validatorRewardA * Validator.CommissionNumerator) + .DivRem(Validator.CommissionDenominator); + var (validatorRewardB, _) + = (validatorRewardSum * 307) + .DivRem(100 + (101 + 200) * 50 - 101 - 102 + 204 + 306); + var (commissionB, _) + = (validatorRewardB * Validator.CommissionNumerator) + .DivRem(Validator.CommissionDenominator); + + Assert.Equal( + Asset.ConsensusFromGovernance(0), + _states.GetBalance(ReservedAddress.RewardPool, Asset.ConsensusToken)); + + Assert.Equal( + Asset.GovernanceToken * (100 + (101 + 200) * 50 - 101 - 102 + 204 + 306), + _states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken)); + + Assert.Equal( + Asset.ConsensusFromGovernance(205), + _states.GetBalance(validatorAddressA, Asset.ConsensusToken)); + + Assert.Equal( + Asset.ConsensusFromGovernance(307), + _states.GetBalance(validatorAddressB, Asset.ConsensusToken)); + + Assert.Equal( + proposerReward + commissionA, + _states.GetBalance( + AllocateRewardCtrl.RewardAddress(OperatorAddresses[3]), Asset.ConsensusToken)); + + Assert.Equal( + commissionB, + _states.GetBalance( + AllocateRewardCtrl.RewardAddress(OperatorAddresses[5]), Asset.ConsensusToken)); + + Address delegationAddressA + = Delegation.DeriveAddress(DelegatorAddress, validatorAddressA); + + Assert.Equal( + Asset.ConsensusFromGovernance(0), + _states.GetBalance( + AllocateRewardCtrl.RewardAddress(DelegatorAddress), Asset.ConsensusToken)); + + var (delegatorToken, _) + = (_states.GetBalance( + ValidatorRewards.DeriveAddress(validatorAddressA, Asset.ConsensusToken), + Asset.ConsensusToken) + * _states.GetBalance( + Delegation.DeriveAddress(DelegatorAddress, validatorAddressA), + Asset.Share) + .RawValue) + .DivRem(ValidatorCtrl.GetValidator(_states, validatorAddressA)! + .DelegatorShares.RawValue); + + _states = DelegateCtrl.Distribute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 5, + }, + _nativeTokens, + delegationAddressA); + + Assert.Equal( + delegatorToken, + _states.GetBalance( + AllocateRewardCtrl.RewardAddress(DelegatorAddress), Asset.ConsensusToken)); + } + } +} 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/RedelegateCtrlTest.cs b/.Lib9c.Tests/Action/DPoS/Control/RedelegateCtrlTest.cs new file mode 100644 index 0000000000..58cadf3122 --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Control/RedelegateCtrlTest.cs @@ -0,0 +1,386 @@ +namespace Lib9c.Tests.Action.DPoS.Control +{ + using System.Collections.Immutable; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume.Action.DPoS; + using Nekoyume.Action.DPoS.Control; + using Nekoyume.Action.DPoS.Exception; + using Nekoyume.Action.DPoS.Misc; + using Nekoyume.Action.DPoS.Model; + using Nekoyume.Action.DPoS.Util; + using Nekoyume.Module; + using Xunit; + + public class RedelegateCtrlTest : PoSTest + { + private readonly PublicKey _srcOperatorPublicKey; + private readonly PublicKey _dstOperatorPublicKey; + private readonly Address _srcOperatorAddress; + private readonly Address _dstOperatorAddress; + private readonly Address _delegatorAddress; + private readonly Address _srcValidatorAddress; + private readonly Address _dstValidatorAddress; + private readonly Address _redelegationAddress; + private ImmutableHashSet _nativeTokens; + private IWorld _states; + + public RedelegateCtrlTest() + { + _srcOperatorPublicKey = new PrivateKey().PublicKey; + _dstOperatorPublicKey = new PrivateKey().PublicKey; + _srcOperatorAddress = _srcOperatorPublicKey.Address; + _dstOperatorAddress = _dstOperatorPublicKey.Address; + _delegatorAddress = CreateAddress(); + _srcValidatorAddress = Validator.DeriveAddress(_srcOperatorAddress); + _dstValidatorAddress = Validator.DeriveAddress(_dstOperatorAddress); + _redelegationAddress = Redelegation.DeriveAddress( + _delegatorAddress, _srcValidatorAddress, _dstValidatorAddress); + _nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + _states = InitializeStates(); + } + + [Fact] + public void InvalidCurrencyTest() + { + Initialize(500, 500, 100, 100); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + Asset.ConsensusFromGovernance(50)); + Assert.Throws( + () => _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + _dstValidatorAddress, + Asset.ConsensusFromGovernance(30), + _nativeTokens)); + Assert.Throws( + () => _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + _dstValidatorAddress, + Asset.GovernanceToken * 30, + _nativeTokens)); + } + + [Fact] + public void InvalidValidatorTest() + { + Initialize(500, 500, 100, 100); + Assert.Throws( + () => _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + CreateAddress(), + _dstValidatorAddress, + ShareFromGovernance(10), + _nativeTokens)); + Assert.Throws( + () => _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + CreateAddress(), + ShareFromGovernance(10), + _nativeTokens)); + } + + [Fact] + public void MaxEntriesTest() + { + Initialize(500, 500, 100, 100); + for (long i = 0; i < 10; i++) + { + _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = i, + }, + _delegatorAddress, + _srcValidatorAddress, + _dstValidatorAddress, + ShareFromGovernance(1), + _nativeTokens); + } + + Assert.Throws( + () => _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + _dstValidatorAddress, + ShareFromGovernance(1), + _nativeTokens)); + } + + [Fact] + public void ExceedRedelegateTest() + { + Initialize(500, 500, 100, 100); + Assert.Throws( + () => _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + _dstValidatorAddress, + ShareFromGovernance(101), + _nativeTokens)); + } + + [Theory] + [InlineData(500, 500, 100, 100, 100)] + [InlineData(500, 500, 100, 100, 50)] + public void CompleteRedelegationTest( + int operatorMintAmount, + int delegatorMintAmount, + int selfDelegateAmount, + int delegateAmount, + int redelegateAmount) + { + Initialize(operatorMintAmount, delegatorMintAmount, selfDelegateAmount, delegateAmount); + _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + _dstValidatorAddress, + Asset.Share * redelegateAmount, + _nativeTokens); + Assert.Single( + RedelegateCtrl.GetRedelegation(_states, _redelegationAddress)! + .RedelegationEntryAddresses); + _states = RedelegateCtrl.Complete( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1000, + }, + _redelegationAddress); + Assert.Single( + RedelegateCtrl.GetRedelegation(_states, _redelegationAddress)! + .RedelegationEntryAddresses); + _states = RedelegateCtrl.Complete( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 50400 * 5, + }, + _redelegationAddress); + Assert.Empty( + RedelegateCtrl.GetRedelegation(_states, _redelegationAddress)! + .RedelegationEntryAddresses); + } + + [Theory] + [InlineData(500, 500, 100, 100, 100)] + [InlineData(500, 500, 100, 100, 50)] + public void BalanceTest( + int operatorMintAmount, + int delegatorMintAmount, + int selfDelegateAmount, + int delegateAmount, + int redelegateAmount) + { + Initialize(operatorMintAmount, delegatorMintAmount, selfDelegateAmount, delegateAmount); + _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + _dstValidatorAddress, + Asset.Share * redelegateAmount, + _nativeTokens); + Assert.Equal( + Asset.GovernanceToken * 0, + _states.GetBalance(_srcValidatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * 0, + _states.GetBalance(_dstValidatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.ConsensusFromGovernance(0), + _states.GetBalance(_srcOperatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.ConsensusFromGovernance(0), + _states.GetBalance(_dstOperatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.ConsensusFromGovernance(0), + _states.GetBalance(_delegatorAddress, Asset.ConsensusToken)); + Assert.Equal( + ShareFromGovernance(0), + _states.GetBalance(_srcOperatorAddress, Asset.Share)); + Assert.Equal( + ShareFromGovernance(0), + _states.GetBalance(_dstOperatorAddress, Asset.Share)); + Assert.Equal( + ShareFromGovernance(0), + _states.GetBalance(_delegatorAddress, Asset.Share)); + Assert.Equal( + Asset.GovernanceToken * (operatorMintAmount - selfDelegateAmount), + _states.GetBalance(_srcOperatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (operatorMintAmount - selfDelegateAmount), + _states.GetBalance(_dstOperatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (2 * selfDelegateAmount + delegateAmount), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + var balanceA = _states.GetBalance( + Delegation.DeriveAddress( + _srcOperatorAddress, + _srcValidatorAddress), + Asset.Share); + var balanceB = _states.GetBalance( + Delegation.DeriveAddress( + _delegatorAddress, + _srcValidatorAddress), + Asset.Share); + Assert.Equal( + ValidatorCtrl.GetValidator(_states, _srcValidatorAddress)!.DelegatorShares, + balanceA + balanceB); + balanceA = _states.GetBalance( + Delegation.DeriveAddress( + _dstOperatorAddress, + _dstValidatorAddress), + Asset.Share); + balanceB = _states.GetBalance( + Delegation.DeriveAddress( + _delegatorAddress, + _dstValidatorAddress), + Asset.Share); + Assert.Equal( + ValidatorCtrl.GetValidator(_states, _dstValidatorAddress)!.DelegatorShares, + balanceA + balanceB); + RedelegationEntry entry = new RedelegationEntry( + _states.GetDPoSState( + RedelegateCtrl.GetRedelegation(_states, _redelegationAddress)! + .RedelegationEntryAddresses[0])!); + Assert.Equal( + Asset.ConsensusFromGovernance(selfDelegateAmount + delegateAmount) + - entry.UnbondingConsensusToken, + _states.GetBalance(_srcValidatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.ConsensusFromGovernance(selfDelegateAmount) + + entry.UnbondingConsensusToken, + _states.GetBalance(_dstValidatorAddress, Asset.ConsensusToken)); + } + + private void Initialize( + int operatorMintAmount, + int delegatorMintAmount, + int selfDelegateAmount, + int delegateAmount) + { + _states = InitializeStates(); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _srcOperatorAddress, + Asset.GovernanceToken * operatorMintAmount); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _dstOperatorAddress, + Asset.GovernanceToken * operatorMintAmount); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + Asset.GovernanceToken * delegatorMintAmount); + _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _srcOperatorAddress, + _srcOperatorPublicKey, + Asset.GovernanceToken * selfDelegateAmount, + _nativeTokens); + _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _dstOperatorAddress, + _dstOperatorPublicKey, + Asset.GovernanceToken * selfDelegateAmount, + _nativeTokens); + _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + Asset.GovernanceToken * delegateAmount, + _nativeTokens); + } + } +} 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/UndelegateCtrlTest.cs b/.Lib9c.Tests/Action/DPoS/Control/UndelegateCtrlTest.cs new file mode 100644 index 0000000000..6c7eec7426 --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Control/UndelegateCtrlTest.cs @@ -0,0 +1,398 @@ +namespace Lib9c.Tests.Action.DPoS.Control +{ + using System.Collections.Immutable; + 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 UndelegateCtrlTest : PoSTest + { + private readonly PublicKey _operatorPublicKey; + private readonly Address _operatorAddress; + private readonly Address _delegatorAddress; + private readonly Address _validatorAddress; + private readonly Address _undelegationAddress; + private ImmutableHashSet _nativeTokens; + private IWorld _states; + + public UndelegateCtrlTest() + { + _operatorPublicKey = new PrivateKey().PublicKey; + _operatorAddress = _operatorPublicKey.Address; + _delegatorAddress = CreateAddress(); + _validatorAddress = Validator.DeriveAddress(_operatorAddress); + _undelegationAddress = Undelegation.DeriveAddress(_delegatorAddress, _validatorAddress); + _nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + _states = InitializeStates(); + } + + [Fact] + public void InvalidCurrencyTest() + { + Initialize(500, 500, 100, 100); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + Asset.ConsensusFromGovernance(50)); + Assert.Throws( + () => _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.ConsensusFromGovernance(30), + _nativeTokens)); + Assert.Throws( + () => _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.GovernanceToken * 30, + _nativeTokens)); + } + + [Fact] + public void InvalidValidatorTest() + { + Initialize(500, 500, 100, 100); + Assert.Throws( + () => _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + CreateAddress(), + ShareFromGovernance(10), + _nativeTokens)); + } + + [Fact] + public void MaxEntriesTest() + { + Initialize(500, 500, 100, 100); + for (long i = 0; i < 10; i++) + { + _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = i, + }, + _delegatorAddress, + _validatorAddress, + ShareFromGovernance(1), + _nativeTokens); + } + + Assert.Throws( + () => _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + ShareFromGovernance(1), + _nativeTokens)); + } + + [Fact] + public void ExceedUndelegateTest() + { + Initialize(500, 500, 100, 100); + Assert.Throws( + () => _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + ShareFromGovernance(101), + _nativeTokens)); + } + + [Theory] + [InlineData(500, 500, 100, 100, 100)] + [InlineData(500, 500, 100, 100, 50)] + public void CompleteUnbondingTest( + int operatorMintAmount, + int delegatorMintAmount, + int selfDelegateAmount, + int delegateAmount, + int undelegateAmount) + { + Initialize(operatorMintAmount, delegatorMintAmount, selfDelegateAmount, delegateAmount); + _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + ShareFromGovernance(undelegateAmount), + _nativeTokens); + Assert.Single( + UndelegateCtrl.GetUndelegation(_states, _undelegationAddress)! + .UndelegationEntryAddresses); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (selfDelegateAmount + delegateAmount), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + _states = UndelegateCtrl.Complete( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1000, + }, + _undelegationAddress); + Assert.Single(UndelegateCtrl.GetUndelegation(_states, _undelegationAddress)! + .UndelegationEntryAddresses); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (selfDelegateAmount + delegateAmount), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + _states = UndelegateCtrl.Complete( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 50400 * 5, + }, + _undelegationAddress); + Assert.Empty(UndelegateCtrl.GetUndelegation(_states, _undelegationAddress)! + .UndelegationEntryAddresses); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount + undelegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (selfDelegateAmount + delegateAmount - undelegateAmount), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + } + + [Theory] + [InlineData(500, 500, 100, 100, 100, 30)] + [InlineData(500, 500, 100, 100, 50, 30)] + public void CancelUndelegateTest( + int operatorMintAmount, + int delegatorMintAmount, + int selfDelegateAmount, + int delegateAmount, + int undelegateAmount, + int cancelAmount) + { + Initialize(operatorMintAmount, delegatorMintAmount, selfDelegateAmount, delegateAmount); + _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + ShareFromGovernance(undelegateAmount), + _nativeTokens); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.ConsensusFromGovernance(selfDelegateAmount + delegateAmount - undelegateAmount), + _states.GetBalance(_validatorAddress, Asset.ConsensusToken)); + _states = UndelegateCtrl.Cancel( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 2, + }, + _undelegationAddress, + Asset.ConsensusFromGovernance(cancelAmount), + _nativeTokens); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.ConsensusFromGovernance( + selfDelegateAmount + delegateAmount - undelegateAmount + cancelAmount), + _states.GetBalance(_validatorAddress, Asset.ConsensusToken)); + _states = UndelegateCtrl.Complete( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1000, + }, + _undelegationAddress); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.ConsensusFromGovernance( + selfDelegateAmount + delegateAmount - undelegateAmount + cancelAmount), + _states.GetBalance(_validatorAddress, Asset.ConsensusToken)); + _states = UndelegateCtrl.Complete( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 50400 * 5, + }, + _undelegationAddress); + Assert.Equal( + Asset.GovernanceToken + * (delegatorMintAmount - delegateAmount + undelegateAmount - cancelAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.ConsensusFromGovernance( + selfDelegateAmount + delegateAmount - undelegateAmount + cancelAmount), + _states.GetBalance(_validatorAddress, Asset.ConsensusToken)); + } + + [Theory] + [InlineData(500, 500, 100, 100, 100)] + [InlineData(500, 500, 100, 100, 50)] + public void BalanceTest( + int operatorMintAmount, + int delegatorMintAmount, + int selfDelegateAmount, + int delegateAmount, + int undelegateAmount) + { + Initialize(operatorMintAmount, delegatorMintAmount, selfDelegateAmount, delegateAmount); + _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + ShareFromGovernance(undelegateAmount), + _nativeTokens); + Assert.Equal( + Asset.GovernanceToken * 0, + _states.GetBalance(_validatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.ConsensusFromGovernance(0), + _states.GetBalance(_operatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.ConsensusFromGovernance(0), + _states.GetBalance(_delegatorAddress, Asset.ConsensusToken)); + Assert.Equal( + ShareFromGovernance(0), + _states.GetBalance(_operatorAddress, Asset.Share)); + Assert.Equal( + ShareFromGovernance(0), + _states.GetBalance(_delegatorAddress, Asset.Share)); + Assert.Equal( + Asset.GovernanceToken * (operatorMintAmount - selfDelegateAmount), + _states.GetBalance(_operatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken * (selfDelegateAmount + delegateAmount), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + var balanceA = _states.GetBalance( + Delegation.DeriveAddress( + _operatorAddress, + _validatorAddress), + Asset.Share); + var balanceB = _states.GetBalance( + Delegation.DeriveAddress( + _delegatorAddress, + _validatorAddress), + Asset.Share); + Assert.Equal( + ValidatorCtrl.GetValidator(_states, _validatorAddress)!.DelegatorShares, + balanceA + balanceB); + Assert.Equal( + Asset.ConsensusFromGovernance(selfDelegateAmount + delegateAmount - undelegateAmount), + _states.GetBalance(_validatorAddress, Asset.ConsensusToken)); + } + + private void Initialize( + int operatorMintAmount, + int delegatorMintAmount, + int selfDelegateAmount, + int delegateAmount) + { + _states = InitializeStates(); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + Asset.GovernanceToken * operatorMintAmount); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + Asset.GovernanceToken * delegatorMintAmount); + _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + _operatorPublicKey, + Asset.GovernanceToken * selfDelegateAmount, + _nativeTokens); + _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.GovernanceToken * delegateAmount, + _nativeTokens); + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/Control/ValidatorCtrlTest.cs b/.Lib9c.Tests/Action/DPoS/Control/ValidatorCtrlTest.cs new file mode 100644 index 0000000000..b2b2d09649 --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Control/ValidatorCtrlTest.cs @@ -0,0 +1,276 @@ +namespace Lib9c.Tests.Action.DPoS.Control +{ + using System.Collections.Immutable; + 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 ValidatorCtrlTest : PoSTest + { + private readonly PublicKey _operatorPublicKey; + 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() + : base() + { + _operatorPublicKey = new PrivateKey().PublicKey; + _operatorAddress = _operatorPublicKey.Address; + _validatorAddress = Validator.DeriveAddress(_operatorAddress); + _nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + _states = InitializeStates(); + } + + [Fact] + public void InvalidCurrencyTest() + { + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + Asset.ConsensusFromGovernance(50)); + Assert.Throws( + () => _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + _operatorPublicKey, + Asset.ConsensusFromGovernance(30), + _nativeTokens)); + } + + [Theory] + [InlineData(500, 0)] + [InlineData(500, 1000)] + public void InvalidSelfDelegateTest(int mintAmount, int selfDelegateAmount) + { + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + Asset.GovernanceToken * mintAmount); + Assert.Throws( + () => _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + _operatorPublicKey, + Asset.GovernanceToken * selfDelegateAmount, + _nativeTokens)); + } + + [Theory] + [InlineData(500, 10)] + [InlineData(500, 100)] + public void BalanceTest(int mintAmount, int selfDelegateAmount) + { + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + Asset.GovernanceToken * mintAmount); + _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + _operatorPublicKey, + Asset.GovernanceToken * selfDelegateAmount, + _nativeTokens); + Assert.Equal( + Asset.ConsensusFromGovernance(selfDelegateAmount), + _states.GetBalance(_validatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.GovernanceToken * (mintAmount - selfDelegateAmount), + _states.GetBalance(_operatorAddress, Asset.GovernanceToken)); + Assert.Equal( + ShareFromGovernance(selfDelegateAmount), + _states.GetBalance( + Delegation.DeriveAddress(_operatorAddress, _validatorAddress), Asset.Share)); + Assert.Equal( + 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 new file mode 100644 index 0000000000..c799f7b515 --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Control/ValidatorDelegationSetCtrlTest.cs @@ -0,0 +1,62 @@ +namespace Lib9c.Tests.Action.DPoS.Control +{ + using System.Collections.Immutable; + 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.Model; + using Nekoyume.Module; + using Xunit; + + public class ValidatorDelegationSetCtrlTest : PoSTest + { + private readonly PublicKey _operatorPublicKey; + private readonly Address _operatorAddress; + private readonly Address _validatorAddress; + private readonly ImmutableHashSet _nativeTokens; + private IWorld _states; + + public ValidatorDelegationSetCtrlTest() + : base() + { + _operatorPublicKey = new PrivateKey().PublicKey; + _operatorAddress = _operatorPublicKey.Address; + _validatorAddress = Validator.DeriveAddress(_operatorAddress); + _nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + _states = InitializeStates(); + _states = _states.MintAsset( + context: new ActionContext { PreviousState = _states, }, + recipient: _operatorAddress, + value: Asset.GovernanceToken * 100000); + _states = ValidatorCtrl.Create( + states: _states, + ctx: new ActionContext { PreviousState = _states, }, + operatorAddress: _operatorAddress, + operatorPublicKey: _operatorPublicKey, + Asset.GovernanceToken * 10, + nativeTokens: _nativeTokens + ); + } + + [Fact] + public void Promote_Test() + { + var validatorDelegationSet = ValidatorDelegationSetCtrl.GetValidatorDelegationSet( + _states, + _validatorAddress + ); + Assert.NotNull(validatorDelegationSet); + Assert.Single(validatorDelegationSet.Set); + var delegation = DelegateCtrl.GetDelegation( + states: _states, + delegationAddress: validatorDelegationSet.Set[0] + ); + Assert.NotNull(delegation); + Assert.Equal(_validatorAddress, delegation.ValidatorAddress); + Assert.Equal(_operatorAddress, delegation.DelegatorAddress); + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/Control/ValidatorPowerIndexCtrlTest.cs b/.Lib9c.Tests/Action/DPoS/Control/ValidatorPowerIndexCtrlTest.cs new file mode 100644 index 0000000000..0ebafe576b --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Control/ValidatorPowerIndexCtrlTest.cs @@ -0,0 +1,187 @@ +namespace Lib9c.Tests.Action.DPoS.Control +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + 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.Model; + using Nekoyume.Module; + using Xunit; + + public class ValidatorPowerIndexCtrlTest : PoSTest + { + private readonly ImmutableHashSet _nativeTokens; + private IWorld _states; + + public ValidatorPowerIndexCtrlTest() + { + List operatorPublicKeys = new List() + { + new PrivateKey().PublicKey, + new PrivateKey().PublicKey, + new PrivateKey().PublicKey, + new PrivateKey().PublicKey, + new PrivateKey().PublicKey, + }; + + List
operatorAddresses = operatorPublicKeys.Select( + pubKey => pubKey.Address).ToList(); + + _nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + + _states = InitializeStates(); + ValidatorAddresses = new List
(); + + var pairs = operatorAddresses.Zip(operatorPublicKeys, (addr, key) => (addr, key)); + foreach (var (addr, key) in pairs) + { + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + addr, + Asset.GovernanceToken * 100); + _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + addr, + key, + Asset.GovernanceToken * 10, + _nativeTokens); + ValidatorAddresses.Add(Validator.DeriveAddress(addr)); + } + } + + private List
ValidatorAddresses { get; set; } + + [Fact] + public void SortingTestDifferentToken() + { + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[0], + Asset.ConsensusFromGovernance(10)); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[1], + Asset.ConsensusFromGovernance(30)); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[2], + Asset.ConsensusFromGovernance(50)); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[3], + Asset.ConsensusFromGovernance(40)); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[4], + Asset.ConsensusFromGovernance(20)); + _states = ValidatorPowerIndexCtrl.Update(_states, ValidatorAddresses); + ValidatorPowerIndex validatorPowerIndex; + (_states, validatorPowerIndex) + = ValidatorPowerIndexCtrl.FetchValidatorPowerIndex(_states); + List index = validatorPowerIndex.Index.ToList(); + Assert.Equal(5, index.Count); + Assert.Equal(ValidatorAddresses[2], index[0].ValidatorAddress); + Assert.Equal(Asset.ConsensusFromGovernance(60), index[0].ConsensusToken); + Assert.Equal(ValidatorAddresses[3], index[1].ValidatorAddress); + Assert.Equal(Asset.ConsensusFromGovernance(50), index[1].ConsensusToken); + Assert.Equal(ValidatorAddresses[1], index[2].ValidatorAddress); + Assert.Equal(Asset.ConsensusFromGovernance(40), index[2].ConsensusToken); + Assert.Equal(ValidatorAddresses[4], index[3].ValidatorAddress); + Assert.Equal(Asset.ConsensusFromGovernance(30), index[3].ConsensusToken); + Assert.Equal(ValidatorAddresses[0], index[4].ValidatorAddress); + Assert.Equal(Asset.ConsensusFromGovernance(20), index[4].ConsensusToken); + } + + [Fact] + public void SortingTestSameToken() + { + (_states, _) = ValidatorPowerIndexCtrl.FetchValidatorPowerIndex(_states); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[0], + Asset.ConsensusFromGovernance(10)); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[1], + Asset.ConsensusFromGovernance(10)); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[2], + Asset.ConsensusFromGovernance(10)); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[3], + Asset.ConsensusFromGovernance(10)); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[4], + Asset.ConsensusFromGovernance(10)); + _states = ValidatorPowerIndexCtrl.Update(_states, ValidatorAddresses); + ValidatorPowerIndex validatorPowerIndex; + (_states, validatorPowerIndex) + = ValidatorPowerIndexCtrl.FetchValidatorPowerIndex(_states); + List index = validatorPowerIndex.Index.ToList(); + Assert.Equal(5, index.Count); + for (int i = 0; i < index.Count - 1; i++) + { + Assert.True(((IComparable
)index[i].ValidatorAddress) + .CompareTo(index[i + 1].ValidatorAddress) > 0); + } + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/Control/ValidatorSetCtrlTest.cs b/.Lib9c.Tests/Action/DPoS/Control/ValidatorSetCtrlTest.cs new file mode 100644 index 0000000000..fec51a4a7c --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Control/ValidatorSetCtrlTest.cs @@ -0,0 +1,221 @@ +namespace Lib9c.Tests.Action.DPoS.Control +{ + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Linq; + 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.Model; + using Nekoyume.Module; + using Xunit; + + public class ValidatorSetCtrlTest : PoSTest + { + private ImmutableHashSet _nativeTokens; + private IWorld _states; + + public ValidatorSetCtrlTest() + : base() + { + _nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + _states = InitializeStates(); + OperatorAddresses = new List
(); + ValidatorAddresses = new List
(); + DelegatorAddress = CreateAddress(); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + }, + DelegatorAddress, + Asset.GovernanceToken * 100000); + for (int i = 0; i < 200; i++) + { + PublicKey operatorPublicKey = new PrivateKey().PublicKey; + Address operatorAddress = operatorPublicKey.Address; + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + }, + operatorAddress, + Asset.GovernanceToken * 1000); + OperatorAddresses.Add(operatorAddress); + _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + operatorAddress, + operatorPublicKey, + Asset.GovernanceToken * 1, + _nativeTokens); + ValidatorAddresses.Add(Validator.DeriveAddress(operatorAddress)); + } + } + + private List
OperatorAddresses { get; set; } + + private List
ValidatorAddresses { get; set; } + + private Address DelegatorAddress { get; set; } + + [Fact] + public void ValidatorSetTest() + { + for (int i = 0; i < 200; i++) + { + _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + DelegatorAddress, + ValidatorAddresses[i], + Asset.GovernanceToken * (i + 1), + _nativeTokens); + } + + Address validatorAddressA = ValidatorAddresses[3]; + Address validatorAddressB = ValidatorAddresses[5]; + Address delegationAddressB = Delegation.DeriveAddress( + DelegatorAddress, validatorAddressB); + + _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + DelegatorAddress, + validatorAddressA, + Asset.GovernanceToken * 200, + _nativeTokens); + + _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + DelegatorAddress, + validatorAddressB, + Asset.GovernanceToken * 300, + _nativeTokens); + + _states = ValidatorSetCtrl.Update( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }); + + ValidatorSet bondedSet; + (_states, bondedSet) = ValidatorSetCtrl.FetchBondedValidatorSet(_states); + Assert.Equal( + validatorAddressB, bondedSet.Set.ToList()[0].ValidatorAddress); + Assert.Equal( + validatorAddressA, bondedSet.Set.ToList()[1].ValidatorAddress); + Assert.Equal( + ShareFromGovernance(5 + 1 + 300), + _states.GetBalance(delegationAddressB, Asset.Share)); + Assert.Equal( + Asset.ConsensusFromGovernance(1 + 5 + 1 + 300), + _states.GetBalance(ValidatorAddresses[5], Asset.ConsensusToken)); + Assert.Equal( + Asset.GovernanceToken + * (100 + (101 + 200) * 50 - 101 - 102 + 204 + 306), + _states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken + * (100 + (1 + 100) * 50 - 4 - 6 + 101 + 102), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken + * (100000 - (1 + 200) * 100 - 200 - 300), + _states.GetBalance(DelegatorAddress, Asset.GovernanceToken)); + + _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 2, + }, + DelegatorAddress, + validatorAddressB, + _states.GetBalance(delegationAddressB, Asset.Share), + _nativeTokens); + + Assert.Equal( + ShareFromGovernance(0), + _states.GetBalance(delegationAddressB, Asset.Share)); + Assert.Equal( + Asset.ConsensusFromGovernance(1), + _states.GetBalance(validatorAddressB, Asset.ConsensusToken)); + Assert.Equal( + Asset.GovernanceToken + * (100 + (101 + 200) * 50 - 101 - 102 + 204 + 306 - 306), + _states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken + * (100 + (1 + 100) * 50 - 4 - 6 + 101 + 102 + 306), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken + * (100000 - (1 + 200) * 100 - 200 - 300), + _states.GetBalance(DelegatorAddress, Asset.GovernanceToken)); + + _states = ValidatorSetCtrl.Update( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }); + (_states, bondedSet) = ValidatorSetCtrl.FetchBondedValidatorSet(_states); + Assert.Equal( + validatorAddressA, bondedSet.Set.ToList()[0].ValidatorAddress); + Assert.Equal( + Asset.GovernanceToken + * (100 + (101 + 200) * 50 - 101 - 102 + 204 + 306 - 306 + 102), + _states.GetBalance(ReservedAddress.BondedPool, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken + * (100 + (1 + 100) * 50 - 4 - 6 + 101 + 102 + 306 - 102), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken + * (100000 - (1 + 200) * 100 - 200 - 300), + _states.GetBalance(DelegatorAddress, Asset.GovernanceToken)); + + _states = ValidatorSetCtrl.Update( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 50400 * 5, + }); + + Assert.Equal( + Asset.GovernanceToken + * (100 + (1 + 100) * 50 - 4 - 6 + 101 + 102 + 306 - 102 - 306), + _states.GetBalance(ReservedAddress.UnbondedPool, Asset.GovernanceToken)); + Assert.Equal( + Asset.GovernanceToken + * (100000 - (1 + 200) * 100 - 200 - 300 + 306), + _states.GetBalance(DelegatorAddress, Asset.GovernanceToken)); + } + } +} 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/Model/DelegationTest.cs b/.Lib9c.Tests/Action/DPoS/Model/DelegationTest.cs new file mode 100644 index 0000000000..46726644a2 --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Model/DelegationTest.cs @@ -0,0 +1,23 @@ +namespace Lib9c.Tests.Action.DPoS.Model +{ + using Nekoyume.Action.DPoS.Model; + using Xunit; + + public class DelegationTest : PoSTest + { + private readonly Delegation _delegation; + + public DelegationTest() + { + _delegation = new Delegation(CreateAddress(), CreateAddress()); + } + + [Fact] + public void MarshallingTest() + { + Delegation newDelegation + = new Delegation(_delegation.Serialize()); + Assert.Equal(_delegation, newDelegation); + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/Model/RedelegationEntryTest.cs b/.Lib9c.Tests/Action/DPoS/Model/RedelegationEntryTest.cs new file mode 100644 index 0000000000..ba9655a817 --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Model/RedelegationEntryTest.cs @@ -0,0 +1,48 @@ +namespace Lib9c.Tests.Action.DPoS.Model +{ + using Nekoyume.Action.DPoS.Exception; + using Nekoyume.Action.DPoS.Misc; + using Nekoyume.Action.DPoS.Model; + using Xunit; + + public class RedelegationEntryTest : PoSTest + { + private readonly RedelegationEntry _redelegationEntry; + + public RedelegationEntryTest() + { + _redelegationEntry = new RedelegationEntry( + CreateAddress(), + ShareFromGovernance(1), + Asset.ConsensusFromGovernance(1), + ShareFromGovernance(1), + 1, + 1); + } + + [Fact] + public void InvalidUnbondingConsensusToken() + { + Assert.Throws( + () => _redelegationEntry.RedelegatingShare = Asset.GovernanceToken * 1); + Assert.Throws( + () => _redelegationEntry.RedelegatingShare = Asset.ConsensusFromGovernance(1)); + Assert.Throws( + () => _redelegationEntry.UnbondingConsensusToken = Asset.GovernanceToken * 1); + Assert.Throws( + () => _redelegationEntry.UnbondingConsensusToken = ShareFromGovernance(1)); + Assert.Throws( + () => _redelegationEntry.IssuedShare = Asset.GovernanceToken * 1); + Assert.Throws( + () => _redelegationEntry.IssuedShare = Asset.ConsensusFromGovernance(1)); + } + + [Fact] + public void MarshallingTest() + { + RedelegationEntry newRedelegationEntry + = new RedelegationEntry(_redelegationEntry.Serialize()); + Assert.Equal(_redelegationEntry, newRedelegationEntry); + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/Model/RedelegationTest.cs b/.Lib9c.Tests/Action/DPoS/Model/RedelegationTest.cs new file mode 100644 index 0000000000..893a60adaf --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Model/RedelegationTest.cs @@ -0,0 +1,24 @@ +namespace Lib9c.Tests.Action.DPoS.Model +{ + using Nekoyume.Action.DPoS.Model; + using Xunit; + + public class RedelegationTest : PoSTest + { + private readonly Redelegation _redelegation; + + public RedelegationTest() + { + _redelegation = new Redelegation( + CreateAddress(), CreateAddress(), CreateAddress()); + } + + [Fact] + public void MarshallingTest() + { + Redelegation newRedelegationInfo + = new Redelegation(_redelegation.Serialize()); + Assert.Equal(_redelegation, newRedelegationInfo); + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/Model/UndelegationEntryTest.cs b/.Lib9c.Tests/Action/DPoS/Model/UndelegationEntryTest.cs new file mode 100644 index 0000000000..eaf4bc74c9 --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Model/UndelegationEntryTest.cs @@ -0,0 +1,35 @@ +namespace Lib9c.Tests.Action.DPoS.Model +{ + using Nekoyume.Action.DPoS.Exception; + using Nekoyume.Action.DPoS.Misc; + using Nekoyume.Action.DPoS.Model; + using Xunit; + + public class UndelegationEntryTest : PoSTest + { + private readonly UndelegationEntry _undelegationEntry; + + public UndelegationEntryTest() + { + _undelegationEntry = new UndelegationEntry( + CreateAddress(), Asset.ConsensusFromGovernance(1), 1, 1); + } + + [Fact] + public void InvalidUnbondingConsensusToken() + { + Assert.Throws( + () => _undelegationEntry.UnbondingConsensusToken = Asset.GovernanceToken * 1); + Assert.Throws( + () => _undelegationEntry.UnbondingConsensusToken = ShareFromGovernance(1)); + } + + [Fact] + public void MarshallingTest() + { + UndelegationEntry newUndelegationEntry + = new UndelegationEntry(_undelegationEntry.Serialize()); + Assert.Equal(_undelegationEntry, newUndelegationEntry); + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/Model/UndelegationTest.cs b/.Lib9c.Tests/Action/DPoS/Model/UndelegationTest.cs new file mode 100644 index 0000000000..a12e323b46 --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Model/UndelegationTest.cs @@ -0,0 +1,23 @@ +namespace Lib9c.Tests.Action.DPoS.Model +{ + using Nekoyume.Action.DPoS.Model; + using Xunit; + + public class UndelegationTest : PoSTest + { + private readonly Undelegation _undelegation; + + public UndelegationTest() + { + _undelegation = new Undelegation(CreateAddress(), CreateAddress()); + } + + [Fact] + public void MarshallingTest() + { + Undelegation newUndelegationInfo + = new Undelegation(_undelegation.Serialize()); + Assert.Equal(_undelegation, newUndelegationInfo); + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/Model/ValidatorPowerIndexTest.cs b/.Lib9c.Tests/Action/DPoS/Model/ValidatorPowerIndexTest.cs new file mode 100644 index 0000000000..4a7d2e8b88 --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Model/ValidatorPowerIndexTest.cs @@ -0,0 +1,23 @@ +namespace Lib9c.Tests.Action.DPoS.Model +{ + using Nekoyume.Action.DPoS.Model; + using Xunit; + + public class ValidatorPowerIndexTest : PoSTest + { + private readonly ValidatorPowerIndex _validatorPowerIndex; + + public ValidatorPowerIndexTest() + { + _validatorPowerIndex = new ValidatorPowerIndex(); + } + + [Fact] + public void MarshallingTest() + { + ValidatorPowerIndex newValidatorPowerIndex = new ValidatorPowerIndex( + _validatorPowerIndex.Serialize()); + Assert.Equal(_validatorPowerIndex.Index, newValidatorPowerIndex.Index); + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/Model/ValidatorPowerTest.cs b/.Lib9c.Tests/Action/DPoS/Model/ValidatorPowerTest.cs new file mode 100644 index 0000000000..3853c2fd61 --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Model/ValidatorPowerTest.cs @@ -0,0 +1,38 @@ +namespace Lib9c.Tests.Action.DPoS.Model +{ + using Libplanet.Crypto; + using Nekoyume.Action.DPoS.Exception; + using Nekoyume.Action.DPoS.Misc; + using Nekoyume.Action.DPoS.Model; + using Xunit; + + public class ValidatorPowerTest : PoSTest + { + private readonly ValidatorPower _validatorPower; + + public ValidatorPowerTest() + { + _validatorPower = new ValidatorPower( + CreateAddress(), + new PrivateKey().PublicKey, + Asset.ConsensusFromGovernance(10)); + } + + [Fact] + public void InvalidUnbondingConsensusToken() + { + Assert.Throws( + () => _validatorPower.ConsensusToken = Asset.GovernanceToken * 1); + Assert.Throws( + () => _validatorPower.ConsensusToken = ShareFromGovernance(1)); + } + + [Fact] + public void MarshallingTest() + { + ValidatorPower newValidatorPower = new ValidatorPower( + _validatorPower.Serialize()); + Assert.Equal(_validatorPower, newValidatorPower); + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/Model/ValidatorSetTest.cs b/.Lib9c.Tests/Action/DPoS/Model/ValidatorSetTest.cs new file mode 100644 index 0000000000..0de98c074c --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Model/ValidatorSetTest.cs @@ -0,0 +1,25 @@ +namespace Lib9c.Tests.Action.DPoS.Model +{ + using Nekoyume.Action.DPoS.Model; + using Xunit; + + public class ValidatorSetTest : PoSTest + { + private readonly ValidatorSet _validatorSet; + + public ValidatorSetTest() + { + _validatorSet = new ValidatorSet(); + } + + [Fact] + public void MarshallingTest() + { + ValidatorSet newValidatorSet = new ValidatorSet( + _validatorSet.Serialize()); + Assert.Equal( + _validatorSet.Set, + newValidatorSet.Set); + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/Model/ValidatorTest.cs b/.Lib9c.Tests/Action/DPoS/Model/ValidatorTest.cs new file mode 100644 index 0000000000..570107613d --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Model/ValidatorTest.cs @@ -0,0 +1,34 @@ +namespace Lib9c.Tests.Action.DPoS.Model +{ + using Libplanet.Crypto; + using Nekoyume.Action.DPoS.Exception; + using Nekoyume.Action.DPoS.Misc; + using Nekoyume.Action.DPoS.Model; + using Xunit; + + public class ValidatorTest : PoSTest + { + private readonly Validator _validator; + + public ValidatorTest() + { + _validator = new Validator(CreateAddress(), new PrivateKey().PublicKey); + } + + [Fact] + public void InvalidShareTypeTest() + { + Assert.Throws( + () => _validator.DelegatorShares = Asset.ConsensusFromGovernance(1)); + Assert.Throws( + () => _validator.DelegatorShares = Asset.GovernanceToken * 1); + } + + [Fact] + public void MarshallingTest() + { + Validator newValidator = new Validator(_validator.Serialize()); + Assert.Equal(_validator, newValidator); + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/PoSTest.cs b/.Lib9c.Tests/Action/DPoS/PoSTest.cs new file mode 100644 index 0000000000..2b810215e2 --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/PoSTest.cs @@ -0,0 +1,169 @@ +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()); + } + + protected static Address CreateAddress() + { + PrivateKey privateKey = new PrivateKey(); + return privateKey.Address; + } + + protected static FungibleAssetValue ShareFromGovernance(FungibleAssetValue governanceToken) + => FungibleAssetValue.FromRawValue(Asset.Share, governanceToken.RawValue); + + 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; + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/Sys/RecordProposerTest.cs b/.Lib9c.Tests/Action/DPoS/Sys/RecordProposerTest.cs new file mode 100644 index 0000000000..8e09315902 --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Sys/RecordProposerTest.cs @@ -0,0 +1,40 @@ +namespace Lib9c.Tests.Action.DPoS.Sys +{ + using Libplanet.Action.State; + using Libplanet.Crypto; + using Nekoyume.Action.DPoS; + using Nekoyume.Action.DPoS.Misc; + using Nekoyume.Action.DPoS.Model; + using Nekoyume.Action.DPoS.Sys; + using Xunit; + + public class RecordProposerTest : PoSTest + { + [Fact] + public void Execute() + { + var privateKey1 = new PrivateKey(); + var privateKey2 = new PrivateKey(); + var previousProposerInfo = new ProposerInfo(1, privateKey1.Address); + + // Prepare initial state. + IWorld initialState = new World(new MockWorldState()); + initialState = initialState.SetDPoSState( + ReservedAddress.ProposerInfo, + previousProposerInfo.Bencoded); + // Execute the action. + initialState = new RecordProposer().Execute( + new ActionContext + { + PreviousState = initialState, + BlockIndex = 2, + Miner = privateKey2.Address, + }); + + var proposerInfo = + new ProposerInfo(initialState.GetDPoSState(ReservedAddress.ProposerInfo)); + Assert.Equal(2, proposerInfo.BlockIndex); + Assert.True(privateKey2.Address.Equals(proposerInfo.Proposer)); + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/Sys/UpdateValidatorsTest.cs b/.Lib9c.Tests/Action/DPoS/Sys/UpdateValidatorsTest.cs new file mode 100644 index 0000000000..68e427dbd6 --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/Sys/UpdateValidatorsTest.cs @@ -0,0 +1,92 @@ +namespace Lib9c.Tests.Action.DPoS.Sys +{ + using System.Linq; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume.Action.DPoS; + using Nekoyume.Action.DPoS.Control; + using Nekoyume.Action.DPoS.Misc; + using Nekoyume.Action.DPoS.Model; + using Nekoyume.Action.DPoS.Sys; + using Nekoyume.Module; + using Xunit; + + public class UpdateValidatorsTest : PoSTest + { + [Fact] + public void Execute() + { + // Prepare initial state. + IWorld initialState = new World(new MockWorldState()); + const int count = 4; + var validatorKeys = Enumerable.Range(0, count).Select(_ => new PrivateKey().PublicKey).ToArray(); + initialState = validatorKeys.Aggregate( + initialState, + (current, key) => current.MintAsset( + new ActionContext(), + key.Address, + new FungibleAssetValue(Asset.GovernanceToken, 1, 0))); + foreach (var key in validatorKeys) + { + Assert.Equal(1, initialState.GetBalance(key.Address, Asset.GovernanceToken).MajorUnit); + Assert.Equal(0, initialState.GetBalance(key.Address, Asset.GovernanceToken).MinorUnit); + } + + // Stake 1 for each validator. + foreach (var key in validatorKeys) + { + initialState = new PromoteValidator( + key, + new FungibleAssetValue(Asset.GovernanceToken, 1, 0)).Execute( + new ActionContext + { + PreviousState = initialState, + Signer = key.Address, + }); + } + + Assert.Equal(0, ValidatorSetCtrl.FetchBondedValidatorSet(initialState).Item2.Count); + Assert.Equal(0, initialState.GetValidatorSet().TotalCount); + + // Execute the action. + initialState = new UpdateValidators().Execute( + new ActionContext + { + PreviousState = initialState, + LastCommit = null, + }); + + Assert.Equal(count, ValidatorSetCtrl.FetchBondedValidatorSet(initialState).Item2.Count); + Assert.Equal(count, initialState.GetValidatorSet().TotalCount); + Assert.Equal( + validatorKeys.ToHashSet(), + initialState.GetValidatorSet() + .Validators.Select(validator => validator.PublicKey) + .ToHashSet()); + + initialState = new Undelegate( + Validator.DeriveAddress(validatorKeys[0].Address), + new FungibleAssetValue(Asset.Share, 100, 0)).Execute( + new ActionContext + { + PreviousState = initialState, + Signer = validatorKeys[0].Address, + }); + + Assert.Equal(count, ValidatorSetCtrl.FetchBondedValidatorSet(initialState).Item2.Count); + Assert.Equal(count, initialState.GetValidatorSet().TotalCount); + + // Execute the action. + initialState = new UpdateValidators().Execute( + new ActionContext + { + PreviousState = initialState, + LastCommit = null, + }); + + Assert.Equal(count - 1, ValidatorSetCtrl.FetchBondedValidatorSet(initialState).Item2.Count); + Assert.Equal(count - 1, initialState.GetValidatorSet().TotalCount); + } + } +} diff --git a/.Lib9c.Tests/Action/DPoS/ValidatorPowerComparerTest.cs b/.Lib9c.Tests/Action/DPoS/ValidatorPowerComparerTest.cs new file mode 100644 index 0000000000..16f69f2d11 --- /dev/null +++ b/.Lib9c.Tests/Action/DPoS/ValidatorPowerComparerTest.cs @@ -0,0 +1,39 @@ +namespace Lib9c.Tests.Action.DPoS +{ + using System; + using Libplanet.Crypto; + using Nekoyume.Action.DPoS.Misc; + using Nekoyume.Action.DPoS.Model; + using Xunit; + + public class ValidatorPowerComparerTest : PoSTest + { + [Fact] + public void CompareDifferentTokenTest() + { + PublicKey publicKeyA = new PrivateKey().PublicKey; + PublicKey publicKeyB = new PrivateKey().PublicKey; + ValidatorPower validatorPowerA = new ValidatorPower( + publicKeyA.Address, publicKeyA, Asset.ConsensusFromGovernance(10)); + ValidatorPower validatorPowerB = new ValidatorPower( + publicKeyB.Address, publicKeyB, Asset.ConsensusFromGovernance(11)); + Assert.True(((IComparable)validatorPowerA) + .CompareTo(validatorPowerB) > 0); + } + + [Fact] + public void CompareSameTokenTest() + { + PublicKey publicKeyA = new PrivateKey().PublicKey; + PublicKey publicKeyB = new PrivateKey().PublicKey; + ValidatorPower validatorPowerA = new ValidatorPower( + publicKeyA.Address, publicKeyA, Asset.ConsensusFromGovernance(10)); + ValidatorPower validatorPowerB = new ValidatorPower( + publicKeyB.Address, publicKeyB, Asset.ConsensusFromGovernance(10)); + int sign = -((IComparable
)publicKeyA.Address) + .CompareTo(publicKeyB.Address); + Assert.True(((IComparable)validatorPowerA) + .CompareTo(validatorPowerB) == sign); + } + } +} diff --git a/.Lib9c.Tests/Action/RewardGoldTest.cs b/.Lib9c.Tests/Action/RewardGoldTest.cs index 08096bcc43..ad353c0afc 100644 --- a/.Lib9c.Tests/Action/RewardGoldTest.cs +++ b/.Lib9c.Tests/Action/RewardGoldTest.cs @@ -542,7 +542,11 @@ public async Task Genesis_StateRootHash(bool mainnet) pendingActivationStates: pendingActivationStates.ToArray() ); var tempActionEvaluator = new ActionEvaluator( - policyBlockActionGetter: _ => policy.BlockAction, + new PolicyActionsRegistry( + beginBlockActionsGetter: _ => policy.BeginBlockActions, + endBlockActionsGetter: _ => policy.EndBlockActions, + beginTxActionsGetter: _ => policy.BeginTxActions, + endTxActionsGetter: _ => policy.EndTxActions), stateStore: new TrieStateStore(new MemoryKeyValueStore()), actionTypeLoader: new NCActionLoader()); genesis = BlockChain.ProposeGenesisBlock( @@ -565,7 +569,11 @@ public async Task Genesis_StateRootHash(bool mainnet) stateStore: stateStore, genesisBlock: genesis, actionEvaluator: new ActionEvaluator( - policyBlockActionGetter: _ => policy.BlockAction, + new PolicyActionsRegistry( + beginBlockActionsGetter: _ => policy.BeginBlockActions, + endBlockActionsGetter: _ => policy.EndBlockActions, + beginTxActionsGetter: _ => policy.BeginTxActions, + endTxActionsGetter: _ => policy.EndTxActions), stateStore: stateStore, actionTypeLoader: new NCActionLoader() ), diff --git a/.Lib9c.Tests/Policy/BlockPolicyTest.cs b/.Lib9c.Tests/Policy/BlockPolicyTest.cs index f5591b66cb..9da5e921d3 100644 --- a/.Lib9c.Tests/Policy/BlockPolicyTest.cs +++ b/.Lib9c.Tests/Policy/BlockPolicyTest.cs @@ -65,7 +65,11 @@ public void ValidateNextBlockTx() stateStore, genesis, new ActionEvaluator( - policyBlockActionGetter: _ => policy.BlockAction, + new PolicyActionsRegistry( + beginBlockActionsGetter: _ => policy.BeginBlockActions, + endBlockActionsGetter: _ => policy.EndBlockActions, + beginTxActionsGetter: _ => policy.BeginTxActions, + endTxActionsGetter: _ => policy.EndTxActions), stateStore: stateStore, actionTypeLoader: new NCActionLoader() ), @@ -273,7 +277,11 @@ public void BlockCommitFromNonValidator() stateStore, genesis, new ActionEvaluator( - policyBlockActionGetter: _ => policy.BlockAction, + new PolicyActionsRegistry( + beginBlockActionsGetter: _ => policy.BeginBlockActions, + endBlockActionsGetter: _ => policy.EndBlockActions, + beginTxActionsGetter: _ => policy.BeginTxActions, + endTxActionsGetter: _ => policy.EndTxActions), stateStore: stateStore, actionTypeLoader: new NCActionLoader() ), @@ -327,7 +335,11 @@ public void MustNotIncludeBlockActionAtTransaction() stateStore, genesis, new ActionEvaluator( - policyBlockActionGetter: _ => policy.BlockAction, + new PolicyActionsRegistry( + beginBlockActionsGetter: _ => policy.BeginBlockActions, + endBlockActionsGetter: _ => policy.EndBlockActions, + beginTxActionsGetter: _ => policy.BeginTxActions, + endTxActionsGetter: _ => policy.EndTxActions), stateStore: stateStore, actionTypeLoader: actionLoader ), @@ -380,7 +392,11 @@ public void EarnMiningGoldWhenSuccessMining() stateStore, genesis, new ActionEvaluator( - policyBlockActionGetter: _ => policy.BlockAction, + new PolicyActionsRegistry( + beginBlockActionsGetter: _ => policy.BeginBlockActions, + endBlockActionsGetter: _ => policy.EndBlockActions, + beginTxActionsGetter: _ => policy.BeginTxActions, + endTxActionsGetter: _ => policy.EndTxActions), stateStore: stateStore, actionTypeLoader: new NCActionLoader() ), @@ -432,7 +448,11 @@ public void ValidateNextBlockWithManyTransactions() stateStore, genesis, new ActionEvaluator( - policyBlockActionGetter: _ => policy.BlockAction, + new PolicyActionsRegistry( + beginBlockActionsGetter: _ => policy.BeginBlockActions, + endBlockActionsGetter: _ => policy.EndBlockActions, + beginTxActionsGetter: _ => policy.BeginTxActions, + endTxActionsGetter: _ => policy.EndTxActions), stateStore: stateStore, actionTypeLoader: new NCActionLoader() ) @@ -533,7 +553,11 @@ public void ValidateNextBlockWithManyTransactionsPerSigner() stateStore, genesis, new ActionEvaluator( - policyBlockActionGetter: _ => policy.BlockAction, + new PolicyActionsRegistry( + beginBlockActionsGetter: _ => policy.BeginBlockActions, + endBlockActionsGetter: _ => policy.EndBlockActions, + beginTxActionsGetter: _ => policy.BeginTxActions, + endTxActionsGetter: _ => policy.EndTxActions), stateStore: stateStore, actionTypeLoader: new NCActionLoader() ) @@ -625,6 +649,7 @@ private BlockCommit GenerateBlockCommit(Block block, PrivateKey key) block.Hash, DateTimeOffset.UtcNow, privateKey.PublicKey, + BigInteger.One, VoteFlag.PreCommit).Sign(privateKey))) : null; } diff --git a/.Lib9c.Tests/TestHelper/BlockChainHelper.cs b/.Lib9c.Tests/TestHelper/BlockChainHelper.cs index 42a9b17e2f..1a2aa9851c 100644 --- a/.Lib9c.Tests/TestHelper/BlockChainHelper.cs +++ b/.Lib9c.Tests/TestHelper/BlockChainHelper.cs @@ -47,7 +47,11 @@ public static BlockChain MakeBlockChain( stateStore, genesis, new ActionEvaluator( - policyBlockActionGetter: _ => policy.BlockAction, + new PolicyActionsRegistry( + beginBlockActionsGetter: _ => policy.BeginBlockActions, + endBlockActionsGetter: _ => policy.EndBlockActions, + beginTxActionsGetter: _ => policy.BeginTxActions, + endTxActionsGetter: _ => policy.EndTxActions), stateStore: stateStore, actionTypeLoader: new NCActionLoader() ), diff --git a/.Lib9c.Tools/SubCommand/State.cs b/.Lib9c.Tools/SubCommand/State.cs index d418ebe4f4..4d0b9037b3 100644 --- a/.Lib9c.Tools/SubCommand/State.cs +++ b/.Lib9c.Tools/SubCommand/State.cs @@ -115,7 +115,11 @@ IStateStore stateStore var actionLoader = TypedActionLoader.Create( typeof(ActionBase).Assembly, typeof(ActionBase)); var actionEvaluator = new ActionEvaluator( - _ => policy.BlockAction, + new PolicyActionsRegistry( + _ => policy.BeginBlockActions, + _ => policy.EndBlockActions, + _ => policy.BeginTxActions, + _ => policy.EndTxActions), stateStore, actionLoader); HashDigest stateRootHash = block.Index < 1 diff --git a/.Libplanet b/.Libplanet index 7d95f209e8..cbfcc93459 160000 --- a/.Libplanet +++ b/.Libplanet @@ -1 +1 @@ -Subproject commit 7d95f209e8358f2f90568cfc4de9ff819c8eea40 +Subproject commit cbfcc93459890c554a91219b826bab5e06a1b1f7 diff --git a/.Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/ActionEvaluationSerializerTest.cs b/.Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/ActionEvaluationSerializerTest.cs index b19d739380..731be6f8f2 100644 --- a/.Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/ActionEvaluationSerializerTest.cs +++ b/.Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/ActionEvaluationSerializerTest.cs @@ -33,7 +33,7 @@ public void Serialization() blockProtocolVersion: 0, previousState: prevState, randomSeed: 123, - blockAction: true), + isBlockAction: true), outputState: outputState, exception: new UnexpectedlyTerminatedActionException( "", diff --git a/.Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContextMarshaller.cs b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContextMarshaller.cs index cb0bed67fb..1aeda029a6 100644 --- a/.Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContextMarshaller.cs +++ b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContextMarshaller.cs @@ -27,7 +27,7 @@ public static Dictionary Marshal(this ICommittedActionContext actionContext) .Add("block_protocol_version", actionContext.BlockProtocolVersion) .Add("previous_states", actionContext.PreviousState.ByteArray) .Add("random_seed", actionContext.RandomSeed) - .Add("block_action", actionContext.BlockAction); + .Add("block_action", actionContext.IsBlockAction); if (actionContext.TxId is { } txId) { @@ -50,7 +50,7 @@ txIdValue is Binary txIdBinaryValue blockProtocolVersion: (Integer)dictionary["block_protocol_version"], previousState: new HashDigest(dictionary["previous_states"]), randomSeed: (Integer)dictionary["random_seed"], - blockAction: (Boolean)dictionary["block_action"] + isBlockAction: (Boolean)dictionary["block_action"] ); } diff --git a/.Libplanet.Extensions.RemoteBlockChainStates/RemoteAccount.cs b/.Libplanet.Extensions.RemoteBlockChainStates/RemoteAccount.cs index 69e7860b71..d2a70fe495 100644 --- a/.Libplanet.Extensions.RemoteBlockChainStates/RemoteAccount.cs +++ b/.Libplanet.Extensions.RemoteBlockChainStates/RemoteAccount.cs @@ -62,6 +62,11 @@ public IAccount SetValidator(Validator validator) throw new NotSupportedException(); } + public IAccount SetValidatorSet(ValidatorSet validatorSet) + { + throw new NotSupportedException(); + } + public IAccount TransferAsset(IActionContext context, Address sender, Address recipient, FungibleAssetValue value, bool allowNegativeBalance = false) { throw new NotSupportedException(); diff --git a/Lib9c.DevExtensions/Utils.cs b/Lib9c.DevExtensions/Utils.cs index 5e2dcc1a7c..42c15e7522 100644 --- a/Lib9c.DevExtensions/Utils.cs +++ b/Lib9c.DevExtensions/Utils.cs @@ -85,7 +85,11 @@ Guid chainIdValue var blockChainStates = new BlockChainStates(store, stateStore); var actionLoader = new NCDevActionLoader(); ActionEvaluator actionEvaluator = new ActionEvaluator( - _ => policy.BlockAction, + new PolicyActionsRegistry( + _ => policy.BeginBlockActions, + _ => policy.EndBlockActions, + _ => policy.BeginTxActions, + _ => policy.EndTxActions), stateStore, actionLoader); diff --git a/Lib9c.MessagePack/Action/NCActionEvaluation.cs b/Lib9c.MessagePack/Action/NCActionEvaluation.cs index f977a3489d..9902c81cbb 100644 --- a/Lib9c.MessagePack/Action/NCActionEvaluation.cs +++ b/Lib9c.MessagePack/Action/NCActionEvaluation.cs @@ -9,6 +9,7 @@ using Libplanet.Common; using Libplanet.Types.Tx; using MessagePack; +using Nekoyume.Action.DPoS; namespace Nekoyume.Action { @@ -19,7 +20,7 @@ public struct NCActionEvaluation [Key(0)] [MessagePackFormatter(typeof(NCActionFormatter))] - public ActionBase? Action { get; set; } + public ActionBase Action { get; set; } [Key(1)] [MessagePackFormatter(typeof(AddressFormatter))] @@ -55,7 +56,7 @@ public struct NCActionEvaluation [SerializationConstructor] public NCActionEvaluation( - ActionBase? action, + ActionBase action, Address signer, long blockIndex, HashDigest outputStates, @@ -81,7 +82,7 @@ public ActionEvaluation ToActionEvaluation() { return new ActionEvaluation { - Action = Action is null ? new RewardGold() : Action, + Action = Action, Signer = Signer, BlockIndex = BlockIndex, OutputState = OutputState, diff --git a/Lib9c.Policy/Policy/BlockPolicySource.cs b/Lib9c.Policy/Policy/BlockPolicySource.cs index edd16ab940..552d0e6b17 100644 --- a/Lib9c.Policy/Policy/BlockPolicySource.cs +++ b/Lib9c.Policy/Policy/BlockPolicySource.cs @@ -16,6 +16,8 @@ using Libplanet.Crypto; using Libplanet.Types.Blocks; using Libplanet.Types.Tx; +using Nekoyume.Action.DPoS; +using Nekoyume.Action.DPoS.Sys; #if UNITY_EDITOR || UNITY_STANDALONE using UniRx; @@ -130,7 +132,14 @@ internal IBlockPolicy GetPolicy( // FIXME: Slight inconsistency due to pre-existing delegate. return new BlockPolicy( - new RewardGold(), + beginBlockActions: new IAction[] { new AllocateReward() }.ToImmutableArray(), + endBlockActions: new IAction[] + { + new UpdateValidators(), + new RecordProposer(), + }.ToImmutableArray(), + beginTxActions: new IAction[] { new Mortgage() }.ToImmutableArray(), + endTxActions: new IAction[] { new Refund(), new Reward() }.ToImmutableArray(), blockInterval: BlockInterval, validateNextBlockTx: validateNextBlockTx, validateNextBlock: validateNextBlock, diff --git a/Lib9c.Policy/Policy/DebugPolicy.cs b/Lib9c.Policy/Policy/DebugPolicy.cs index 54985c7aec..90d627cf24 100644 --- a/Lib9c.Policy/Policy/DebugPolicy.cs +++ b/Lib9c.Policy/Policy/DebugPolicy.cs @@ -1,9 +1,12 @@ +using System.Collections.Immutable; using Libplanet.Action; using Libplanet.Blockchain; using Libplanet.Blockchain.Policies; using Libplanet.Types.Blocks; using Libplanet.Types.Tx; using Nekoyume.Action; +using Nekoyume.Action.DPoS; +using Nekoyume.Action.DPoS.Sys; namespace Nekoyume.Blockchain.Policy { @@ -13,7 +16,17 @@ public DebugPolicy() { } - public IAction BlockAction { get; } = new RewardGold(); + public ImmutableArray BeginBlockActions { get; } = + new IAction[] { new AllocateReward() }.ToImmutableArray(); + + public ImmutableArray EndBlockActions { get; } = + new IAction[] { new UpdateValidators(), new RecordProposer() }.ToImmutableArray(); + + public ImmutableArray BeginTxActions { get; } = + new IAction[] { new Mortgage() }.ToImmutableArray(); + + public ImmutableArray EndTxActions { get; } = + new IAction[] { new Refund(), new Reward() }.ToImmutableArray(); public TxPolicyViolationException ValidateNextBlockTx( BlockChain blockChain, Transaction transaction) diff --git a/Lib9c.Policy/Policy/NCBlockPolicy.cs b/Lib9c.Policy/Policy/NCBlockPolicy.cs index aa47725cd3..9c763b275e 100644 --- a/Lib9c.Policy/Policy/NCBlockPolicy.cs +++ b/Lib9c.Policy/Policy/NCBlockPolicy.cs @@ -4,13 +4,15 @@ using Libplanet.Types.Blocks; using Libplanet.Types.Tx; using System; +using System.Collections.Immutable; namespace Nekoyume.Blockchain.Policy { public class NCBlockPolicy : BlockPolicy { public NCBlockPolicy( - IAction blockAction, + ImmutableArray beginBlockActions, + ImmutableArray endBlockActions, TimeSpan blockInterval, Func? validateNextBlockTx = null, @@ -21,7 +23,8 @@ public NCBlockPolicy( Func? getMaxTransactionsPerBlock = null, Func? getMaxTransactionsPerSignerPerBlock = null) : base( - blockAction: blockAction, + beginBlockActions: beginBlockActions, + endBlockActions: endBlockActions, blockInterval: blockInterval, validateNextBlockTx: validateNextBlockTx, validateNextBlock: validateNextBlock, diff --git a/Lib9c.Proposer/Proposer.cs b/Lib9c.Proposer/Proposer.cs index 4d0260572b..48dabb1d0a 100644 --- a/Lib9c.Proposer/Proposer.cs +++ b/Lib9c.Proposer/Proposer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Numerics; using System.Threading; using Libplanet.Action; using Libplanet.Blockchain; @@ -8,6 +9,7 @@ using Libplanet.Types.Blocks; using Libplanet.Types.Consensus; using Libplanet.Types.Tx; +using Nekoyume.Module; using Serilog; namespace Nekoyume.Blockchain @@ -33,20 +35,29 @@ public class Proposer block = _chain.ProposeBlock( _privateKey, lastCommit: lastCommit); - BlockCommit? commit = block.Index > 0 - ? new BlockCommit( + BlockCommit? commit = null; + if (block.Index > 0) + { + var power = _chain.GetWorldState() + .GetValidatorSet() + .GetValidator(_privateKey.PublicKey) + .Power; + commit = new BlockCommit( block.Index, 0, block.Hash, - ImmutableArray.Empty - .Add(new VoteMetadata( + ImmutableArray.Empty.Add( + new VoteMetadata( block.Index, 0, block.Hash, DateTimeOffset.UtcNow, _privateKey.PublicKey, - VoteFlag.PreCommit).Sign(_privateKey))) - : null; + power, + VoteFlag.PreCommit).Sign(_privateKey))); + } + + _chain.Append(block, commit); } catch (OperationCanceledException) diff --git a/Lib9c.Renderers/Renderers/ActionRenderer.cs b/Lib9c.Renderers/Renderers/ActionRenderer.cs index d46df38df8..bf03b68a1a 100644 --- a/Lib9c.Renderers/Renderers/ActionRenderer.cs +++ b/Lib9c.Renderers/Renderers/ActionRenderer.cs @@ -36,9 +36,7 @@ public ActionRenderer() public void RenderAction(IValue action, ICommittedActionContext context, HashDigest nextState) => ActionRenderSubject.OnNext(new ActionEvaluation { - Action = context.BlockAction - ? new RewardGold() - : (ActionBase)_actionLoader.LoadAction(context.BlockIndex, action), + Action = (ActionBase)_actionLoader.LoadAction(context.BlockIndex, action), Signer = context.Signer, BlockIndex = context.BlockIndex, TxId = context.TxId, @@ -52,9 +50,7 @@ public void RenderActionError(IValue action, ICommittedActionContext context, Ex Log.Error(exception, "{action} execution failed.", action); ActionRenderSubject.OnNext(new ActionEvaluation { - Action = context.BlockAction - ? new RewardGold() - : (ActionBase)_actionLoader.LoadAction(context.BlockIndex, action), + Action = (ActionBase)_actionLoader.LoadAction(context.BlockIndex, action), Signer = context.Signer, BlockIndex = context.BlockIndex, TxId = context.TxId, diff --git a/Lib9c.Utils/BlockHelper.cs b/Lib9c.Utils/BlockHelper.cs index a28fa9c51b..013ad575d5 100644 --- a/Lib9c.Utils/BlockHelper.cs +++ b/Lib9c.Utils/BlockHelper.cs @@ -15,6 +15,7 @@ using Libplanet.Types.Consensus; using Libplanet.Types.Tx; using Nekoyume.Action; +using Nekoyume.Action.DPoS; using Nekoyume.Action.Loader; using Nekoyume.Blockchain.Policy; using Nekoyume.Model.State; @@ -38,8 +39,8 @@ public static Block ProposeGenesisBlock( DateTimeOffset? timestamp = null, IEnumerable? actionBases = null, Currency? goldCurrency = null, - ISet
? assetMinters = null - ) + ISet
? assetMinters = null, + Dictionary? initialFavs = null) { if (!tableSheets.TryGetValue(nameof(GameConfigSheet), out var csv)) { @@ -74,7 +75,8 @@ public static Block ProposeGenesisBlock( pendingActivationStates: pendingActivationStates, authorizedMinersState: authorizedMinersState, creditsState: credits is null ? null : new CreditsState(credits), - assetMinters: assetMinters + assetMinters: assetMinters, + initialFavs: initialFavs ); List actions = new List { @@ -84,20 +86,24 @@ public static Block ProposeGenesisBlock( { new Initialize( states: ImmutableDictionary.Create(), - validatorSet: new ValidatorSet( - initialValidators.Select(validator => - new Validator(validator.Key, validator.Value)).ToList() - ) - ), + validatorSet: new ValidatorSet()), + new InitializeValidators(initialValidators) }; if (!(actionBases is null)) { actions.AddRange(actionBases); } - var blockAction = new BlockPolicySource().GetPolicy().BlockAction; + var beginBlockActions = new BlockPolicySource().GetPolicy().BeginBlockActions; + var endBlockActions = new BlockPolicySource().GetPolicy().EndBlockActions; + var beginTxActions = new BlockPolicySource().GetPolicy().BeginTxActions; + var endTxActions = new BlockPolicySource().GetPolicy().EndTxActions; var actionLoader = new NCActionLoader(); var actionEvaluator = new ActionEvaluator( - _ => blockAction, + new PolicyActionsRegistry( + _ => beginBlockActions, + _ => endBlockActions, + _ => beginTxActions, + _ => endTxActions), new TrieStateStore(new MemoryKeyValueStore()), actionLoader); return diff --git a/Lib9c.sln.DotSettings b/Lib9c.sln.DotSettings index f0ba53d29f..a8a315c16f 100644 --- a/Lib9c.sln.DotSettings +++ b/Lib9c.sln.DotSettings @@ -10,8 +10,12 @@ True True True + True True True True + True + True + True True True \ No newline at end of file diff --git a/Lib9c/Action/DPoS/CancelUndelegation.cs b/Lib9c/Action/DPoS/CancelUndelegation.cs new file mode 100644 index 0000000000..bebe20f1fc --- /dev/null +++ b/Lib9c/Action/DPoS/CancelUndelegation.cs @@ -0,0 +1,87 @@ +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.Model; +using Nekoyume.Action.DPoS.Sys; +using Nekoyume.Action.DPoS.Util; + +namespace Nekoyume.Action.DPoS +{ + /// + /// A system action for DPoS that cancel specified + /// of tokens to a given . + /// + [ActionType(ActionTypeValue)] + public sealed class CancelUndelegation : ActionBase + { + private const string ActionTypeValue = "cancel_undelegation"; + + /// + /// Creates a new instance of action. + /// + /// The of the validator + /// to delegate tokens. + /// The amount of the asset to be delegated. + public CancelUndelegation(Address validator, FungibleAssetValue amount) + { + Validator = validator; + Amount = amount; + } + + public CancelUndelegation() + { + // Used only for deserialization. See also class Libplanet.Action.Sys.Registry. + } + + /// + /// The of the validator + /// to cancel the and . + /// + public Address Validator { get; set; } + + /// + /// The amount of the asset to be delegated. + /// + public FungibleAssetValue Amount { get; set; } + + /// + public override IValue PlainValue => Bencodex.Types.Dictionary.Empty + .Add("type_id", new Text(ActionTypeValue)) + .Add("validator", Validator.Serialize()) + .Add("amount", Amount.Serialize()); + + /// + public override void LoadPlainValue(IValue plainValue) + { + var dict = (Bencodex.Types.Dictionary)plainValue; + Validator = dict["validator"].ToAddress(); + Amount = dict["amount"].ToFungibleAssetValue(); + } + + /// + public override IWorld Execute(IActionContext context) + { + context.UseGas(1); + IActionContext ctx = context; + var states = ctx.PreviousState; + var nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + + // if (ctx.Rehearsal) + // Rehearsal mode is not implemented + states = UndelegateCtrl.Cancel( + states, + ctx, + Undelegation.DeriveAddress(ctx.Signer, Validator), + Amount, + nativeTokens); + + return states; + } + } +} diff --git a/Lib9c/Action/DPoS/Control/AllocateRewardCtrl.cs b/Lib9c/Action/DPoS/Control/AllocateRewardCtrl.cs new file mode 100644 index 0000000000..7be94c8213 --- /dev/null +++ b/Lib9c/Action/DPoS/Control/AllocateRewardCtrl.cs @@ -0,0 +1,188 @@ +#nullable enable +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Numerics; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Libplanet.Types.Blocks; +using Libplanet.Types.Consensus; +using Nekoyume.Action.DPoS.Exception; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Model; +using Nekoyume.Action.DPoS.Util; +using Nekoyume.Module; +using Validator = Nekoyume.Action.DPoS.Model.Validator; +using ValidatorSet = Nekoyume.Action.DPoS.Model.ValidatorSet; + +namespace Nekoyume.Action.DPoS.Control +{ + public static class AllocateRewardCtrl + { + public static BigInteger BaseProposerRewardNumerator => 1; + + public static BigInteger BaseProposerRewardDenominator => 100; + + public static BigInteger BonusProposerRewardNumerator => 4; + + public static BigInteger BonusProposerRewardDenominator => 100; + + public static Address RewardAddress(Address holderAddress) + { + return AddressHelper.Derive(holderAddress, "RewardAddress"); + } + + internal static IWorld Execute( + IWorld states, + IActionContext ctx, + IImmutableSet? nativeTokens, + IEnumerable? votes, + ProposerInfo proposerInfo) + { + if (nativeTokens is null) + { + throw new NullNativeTokensException(); + } + + foreach (Currency nativeToken in nativeTokens) + { + if (votes is { } lastVotesEnumerable) + { + var lastVotes = lastVotesEnumerable.ToArray(); + states = DistributeProposerReward( + states, ctx, nativeToken, proposerInfo, lastVotes); + + // TODO: Check if this is correct? + states = DistributeValidatorReward( + states, ctx, nativeToken, lastVotes); + } + + FungibleAssetValue communityFund = states.GetBalance( + ReservedAddress.RewardPool, nativeToken); + + if (communityFund.Sign > 0) + { + states = states.TransferAsset( + ctx, + ReservedAddress.RewardPool, + ReservedAddress.CommunityPool, + communityFund); + } + } + + return states; + } + + internal static IWorld DistributeProposerReward( + IWorld states, + IActionContext ctx, + Currency nativeToken, + ProposerInfo proposerInfo, + Vote[] lastVotes) + { + FungibleAssetValue blockReward = states.GetBalance( + ReservedAddress.RewardPool, nativeToken); + + if (proposerInfo.BlockIndex != ctx.BlockIndex - 1) + { + return states; + } + + if (blockReward.Sign <= 0) + { + return states; + } + + BigInteger votePowerNumerator + = lastVotes.Aggregate( + BigInteger.Zero, + (total, next) + => total + (next.Flag == VoteFlag.PreCommit ? next.ValidatorPower : 0)); + + BigInteger votePowerDenominator + = lastVotes.Aggregate( + BigInteger.Zero, + (total, next) + => total + next.ValidatorPower); + + var (baseProposerReward, _) + = (blockReward * BaseProposerRewardNumerator).DivRem(BaseProposerRewardDenominator); + var (bonusProposerReward, _) + = (blockReward * votePowerNumerator * BonusProposerRewardNumerator) + .DivRem(votePowerDenominator * BonusProposerRewardDenominator); + FungibleAssetValue proposerReward = baseProposerReward + bonusProposerReward; + + states = states.TransferAsset( + ctx, + ReservedAddress.RewardPool, + RewardAddress(proposerInfo.Proposer), + proposerReward); + + return states; + } + + internal static IWorld DistributeValidatorReward( + IWorld states, + IActionContext ctx, + Currency nativeToken, + Vote[] lastVotes) + { + long blockHeight = ctx.BlockIndex; + FungibleAssetValue validatorRewardSum = states.GetBalance( + ReservedAddress.RewardPool, nativeToken); + + if (validatorRewardSum.Sign <= 0) + { + return states; + } + + BigInteger powerDenominator + = lastVotes.Aggregate( + BigInteger.Zero, + (total, next) + => total + next.ValidatorPower); + + foreach (Vote vote in lastVotes) + { + if (vote.Flag == VoteFlag.Null || vote.Flag == VoteFlag.Unknown) + { + continue; + } + + BigInteger powerNumerator = vote.ValidatorPower; + + var (validatorReward, _) + = (validatorRewardSum * powerNumerator) + .DivRem(powerDenominator); + var (commission, _) + = (validatorReward * Validator.CommissionNumerator) + .DivRem(Validator.CommissionDenominator); + + FungibleAssetValue delegationRewardSum = validatorReward - commission; + + states = states.TransferAsset( + ctx, + ReservedAddress.RewardPool, + RewardAddress(vote.ValidatorPublicKey.Address), + commission); + + states = states.TransferAsset( + ctx, + ReservedAddress.RewardPool, + ValidatorRewards.DeriveAddress(vote.ValidatorPublicKey.Address, nativeToken), + delegationRewardSum); + + states = ValidatorRewardsCtrl.Add( + states, + vote.ValidatorPublicKey.Address, + nativeToken, + blockHeight, + delegationRewardSum); + } + + return states; + } + } +} diff --git a/Lib9c/Action/DPoS/Control/Bond.cs b/Lib9c/Action/DPoS/Control/Bond.cs new file mode 100644 index 0000000000..a834fc46c8 --- /dev/null +++ b/Lib9c/Action/DPoS/Control/Bond.cs @@ -0,0 +1,157 @@ +using System.Collections.Immutable; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +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 Bond + { + internal static (IWorld, FungibleAssetValue) Execute( + IWorld states, + IActionContext ctx, + FungibleAssetValue consensusToken, + Address validatorAddress, + Address delegationAddress, + IImmutableSet nativeTokens) + { + // TODO: Failure condition + // 1. Validator does not exist + // 2. Exchange rate is invalid(validator has no tokens but there are outstanding shares) + // 3. Amount is less than the minimum amount + // 4. Delegator does not have sufficient consensus token (fail or apply maximum) + if (!consensusToken.Currency.Equals(Asset.ConsensusToken)) + { + throw new Exception.InvalidCurrencyException(Asset.ConsensusToken, consensusToken.Currency); + } + + if (!(ValidatorCtrl.GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + // If validator share is zero, exchange rate is 1 + // Else, exchange rate is validator share / token + if (!(ValidatorCtrl.ShareFromConsensusToken( + states, validator.Address, consensusToken) is { } issuedShare)) + { + throw new InvalidExchangeRateException(validator.Address); + } + + // Mint consensus token to validator + states = states.MintAsset(ctx, validator.Address, consensusToken); + + // Mint share to delegation + states = states.MintAsset(ctx, delegationAddress, issuedShare); + + // Track total shares minted from validator + validator.DelegatorShares += issuedShare; + states = states.SetDPoSState(validator.Address, validator.Serialize()); + states = ValidatorPowerIndexCtrl.Update(states, validator.Address); + + ValidatorDelegationSet validatorDelegationSet; + (states, validatorDelegationSet) = + ValidatorDelegationSetCtrl.FetchValidatorDelegationSet(states, validator.Address); + + foreach (Address addrs in validatorDelegationSet.Set) + { + states = DelegateCtrl.Distribute(states, ctx, nativeTokens, addrs); + } + + states = ValidatorDelegationSetCtrl.Add( + states: states, + validatorAddress: validatorAddress, + delegationAddress: delegationAddress + ); + + return (states, issuedShare); + } + + internal static (IWorld, FungibleAssetValue) Cancel( + IWorld states, + IActionContext ctx, + FungibleAssetValue share, + Address validatorAddress, + Address delegationAddress, + IImmutableSet nativeTokens) + { + // Currency check + if (!share.Currency.Equals(Asset.Share)) + { + throw new Exception.InvalidCurrencyException(Asset.Share, share.Currency); + } + + FungibleAssetValue delegationShareBalance = states.GetBalance( + delegationAddress, Asset.Share); + if (share > delegationShareBalance) + { + throw new InsufficientFungibleAssetValueException( + share, + delegationShareBalance, + $"Delegation {delegationAddress} has insufficient share"); + } + + if (!(ValidatorCtrl.GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + // Delegator share burn + states = states.BurnAsset(ctx, delegationAddress, share); + + // Jailing check + FungibleAssetValue delegationShare = states.GetBalance(delegationAddress, Asset.Share); + if (delegationAddress.Equals(validator.OperatorAddress) + && !validator.Jailed + && ValidatorCtrl.ConsensusTokenFromShare(states, validator.Address, delegationShare) + < Validator.MinSelfDelegation) + { + validator.Jailed = true; + } + + // Calculate consensus token amount + if (!(ValidatorCtrl.ConsensusTokenFromShare( + states, validator.Address, share) is { } unbondingConsensusToken)) + { + throw new InvalidExchangeRateException(validator.Address); + } + + if (share.Equals(validator.DelegatorShares)) + { + unbondingConsensusToken = states.GetBalance( + validator.Address, Asset.ConsensusToken); + } + + // Subtracting from DelegatorShare have to be calculated last + // since it will affect ConsensusTokenFromShare() + validator.DelegatorShares -= share; + states = states.BurnAsset(ctx, validator.Address, unbondingConsensusToken); + states = states.SetDPoSState(validator.Address, validator.Serialize()); + + states = ValidatorPowerIndexCtrl.Update(states, validator.Address); + + ValidatorDelegationSet validatorDelegationSet; + (states, validatorDelegationSet) = + ValidatorDelegationSetCtrl.FetchValidatorDelegationSet(states, validator.Address); + + foreach (Address addrs in validatorDelegationSet.Set) + { + states = DelegateCtrl.Distribute(states, ctx, nativeTokens, addrs); + } + + states = ValidatorDelegationSetCtrl.Remove( + states: states, + validatorAddress: validatorAddress, + delegationAddress: delegationAddress + ); + + return (states, unbondingConsensusToken); + } + } +} diff --git a/Lib9c/Action/DPoS/Control/DelegateCtrl.cs b/Lib9c/Action/DPoS/Control/DelegateCtrl.cs new file mode 100644 index 0000000000..c637965bcc --- /dev/null +++ b/Lib9c/Action/DPoS/Control/DelegateCtrl.cs @@ -0,0 +1,168 @@ +#nullable enable +using System.Collections.Immutable; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Action.DPoS.Exception; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Model; +using Nekoyume.Action.DPoS.Util; +using Nekoyume.Module; +using Serilog; + +namespace Nekoyume.Action.DPoS.Control +{ + internal static class DelegateCtrl + { + internal static Delegation? GetDelegation(IWorldState states, Address delegationAddress) + { + if (states.GetDPoSState(delegationAddress) is { } value) + { + return new Delegation(value); + } + + 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, + Address validatorAddress) + { + Address delegationAddress = Delegation.DeriveAddress( + delegatorAddress, validatorAddress); + Delegation delegation; + if (states.GetDPoSState(delegationAddress) is { } value) + { + delegation = new Delegation(value); + } + else + { + delegation = new Delegation(delegatorAddress, validatorAddress); + states = states.SetDPoSState(delegation.Address, delegation.Serialize()); + } + + return (states, delegation); + } + + internal static IWorld Execute( + IWorld states, + IActionContext ctx, + Address delegatorAddress, + Address validatorAddress, + FungibleAssetValue governanceToken, + IImmutableSet nativeTokens) + { + if (!governanceToken.Currency.Equals(Asset.GovernanceToken)) + { + throw new Exception.InvalidCurrencyException(Asset.GovernanceToken, governanceToken.Currency); + } + + FungibleAssetValue delegatorGovernanceTokenBalance = states.GetBalance( + delegatorAddress, Asset.GovernanceToken); + if (governanceToken > delegatorGovernanceTokenBalance) + { + throw new InsufficientFungibleAssetValueException( + governanceToken, + delegatorGovernanceTokenBalance, + $"Delegator {delegatorAddress} has insufficient governanceToken"); + } + + if (!(ValidatorCtrl.GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + Delegation? delegation; + (states, delegation) = FetchDelegation(states, delegatorAddress, validatorAddress); + + FungibleAssetValue consensusToken = Asset.ConsensusFromGovernance(governanceToken); + Address poolAddress = validator.Status == BondingStatus.Bonded + ? ReservedAddress.BondedPool + : ReservedAddress.UnbondedPool; + + states = states.TransferAsset( + ctx, delegatorAddress, poolAddress, governanceToken); + (states, _) = Bond.Execute( + states, + ctx, + consensusToken, + delegation.ValidatorAddress, + delegation.Address, + nativeTokens); + + states = states.SetDPoSState(delegation.Address, delegation.Serialize()); + + return states; + } + + internal static IWorld Distribute( + IWorld states, + IActionContext ctx, + IImmutableSet nativeTokens, + Address delegationAddress) + { + long blockHeight = ctx.BlockIndex; + if (!(GetDelegation(states, delegationAddress) is { } delegation)) + { + throw new NullDelegationException(delegationAddress); + } + + if (!(ValidatorCtrl.GetValidator(states, delegation.ValidatorAddress) is { } validator)) + { + throw new NullValidatorException(delegation.ValidatorAddress); + } + + foreach (Currency nativeToken in nativeTokens) + { + FungibleAssetValue delegationRewardSum = ValidatorRewardsCtrl.RewardSumBetween( + states, + delegation.ValidatorAddress, + nativeToken, + delegation.LatestDistributeHeight, + blockHeight); + + // Skip if there is no reward to distribute. + if (delegationRewardSum.RawValue == 0) + { + continue; + } + + if (!(ValidatorCtrl.TokenPortionByShare( + states, + delegation.ValidatorAddress, + delegationRewardSum, + states.GetBalance(delegationAddress, Asset.Share)) is { } reward)) + { + throw new InvalidExchangeRateException(validator.Address); + } + + if (reward.Sign > 0) + { + Address validatorRewardAddress + = ValidatorRewards.DeriveAddress(delegation.ValidatorAddress, nativeToken); + + states = states.TransferAsset( + ctx, + validatorRewardAddress, + AllocateRewardCtrl.RewardAddress(delegation.DelegatorAddress), + reward); + } + } + + delegation.LatestDistributeHeight = blockHeight; + + states = states.SetDPoSState(delegation.Address, delegation.Serialize()); + + return states; + } + } +} 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 new file mode 100644 index 0000000000..57080c8126 --- /dev/null +++ b/Lib9c/Action/DPoS/Control/RedelegateCtrl.cs @@ -0,0 +1,268 @@ +#nullable enable +using System; +using System.Collections.Generic; +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.Exception; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Model; +using Nekoyume.Module; + +namespace Nekoyume.Action.DPoS.Control +{ + internal static class RedelegateCtrl + { + internal static Redelegation? GetRedelegation( + IWorldState states, + Address redelegationAddress) + { + if (states.GetDPoSState(redelegationAddress) is { } value) + { + return new Redelegation(value); + } + + 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, + Address srcValidatorAddress, + Address dstValidatorAddress) + { + Address redelegationAddress = Redelegation.DeriveAddress( + delegatorAddress, srcValidatorAddress, dstValidatorAddress); + + Redelegation redelegation; + if (states.GetDPoSState(redelegationAddress) is { } value) + { + redelegation = new Redelegation(value); + } + else + { + redelegation = new Redelegation( + delegatorAddress, + srcValidatorAddress, + dstValidatorAddress); + states = states.SetDPoSState(redelegation.Address, redelegation.Serialize()); + } + + return (states, redelegation); + } + + internal static IWorld Execute( + IWorld states, + IActionContext ctx, + Address delegatorAddress, + Address srcValidatorAddress, + Address dstValidatorAddress, + FungibleAssetValue redelegatingShare, + IImmutableSet nativeTokens) + { + // TODO: Failure condition + // 1. Delegation does not exist + // 2. Source validator does not exist + // 3. Target validator does not exist + // 3. Delegation has less shares than worth of amount + // 4. Existing redelegation has maximum entries + // 5?. Delegation does not have sufficient token (fail or apply maximum) + long blockHeight = ctx.BlockIndex; + if (!redelegatingShare.Currency.Equals(Asset.Share)) + { + throw new Exception.InvalidCurrencyException(Asset.Share, redelegatingShare.Currency); + } + + if (ValidatorCtrl.GetValidator(states, srcValidatorAddress) is null) + { + throw new NullValidatorException(srcValidatorAddress); + } + + if (ValidatorCtrl.GetValidator(states, dstValidatorAddress) is null) + { + throw new NullValidatorException(dstValidatorAddress); + } + + Redelegation redelegation; + (states, redelegation) = FetchRedelegation( + states, + delegatorAddress, + srcValidatorAddress, + dstValidatorAddress); + + if (redelegation.RedelegationEntryAddresses.Count + >= Redelegation.MaximumRedelegationEntries) + { + throw new MaximumRedelegationEntriesException( + redelegation.Address, redelegation.RedelegationEntryAddresses.Count); + } + + // Add new destination delegation, if not exist + (states, _) = DelegateCtrl.FetchDelegation( + states, delegatorAddress, dstValidatorAddress); + FungibleAssetValue unbondingConsensusToken; + FungibleAssetValue issuedShare; + (states, unbondingConsensusToken) = Bond.Cancel( + states, + ctx, + redelegatingShare, + srcValidatorAddress, + redelegation.SrcDelegationAddress, + nativeTokens); + (states, issuedShare) = Bond.Execute( + states, + ctx, + unbondingConsensusToken, + dstValidatorAddress, + redelegation.DstDelegationAddress, + nativeTokens); + + if (!(ValidatorCtrl.GetValidator(states, srcValidatorAddress) is { } srcValidator)) + { + throw new NullValidatorException(srcValidatorAddress); + } + + if (!(ValidatorCtrl.GetValidator(states, dstValidatorAddress) is { } dstValidator)) + { + throw new NullValidatorException(dstValidatorAddress); + } + + states = (srcValidator.Status, dstValidator.Status) switch + { + (BondingStatus.Bonded, BondingStatus.Unbonding) => states.TransferAsset( + ctx, + ReservedAddress.BondedPool, + ReservedAddress.UnbondedPool, + Asset.GovernanceFromConsensus(unbondingConsensusToken)), + (BondingStatus.Bonded, BondingStatus.Unbonded) => states.TransferAsset( + ctx, + ReservedAddress.BondedPool, + ReservedAddress.UnbondedPool, + Asset.GovernanceFromConsensus(unbondingConsensusToken)), + (BondingStatus.Unbonding, BondingStatus.Bonded) => states.TransferAsset( + ctx, + ReservedAddress.UnbondedPool, + ReservedAddress.BondedPool, + Asset.GovernanceFromConsensus(unbondingConsensusToken)), + (BondingStatus.Unbonded, BondingStatus.Bonded) => states.TransferAsset( + ctx, + ReservedAddress.UnbondedPool, + ReservedAddress.BondedPool, + Asset.GovernanceFromConsensus(unbondingConsensusToken)), + _ => states, + }; + + RedelegationEntry redelegationEntry = new RedelegationEntry( + redelegation.Address, + redelegatingShare, + unbondingConsensusToken, + issuedShare, + redelegation.RedelegationEntryIndex, + blockHeight); + redelegation.RedelegationEntryAddresses.Add( + redelegationEntry.Index, redelegationEntry.Address); + redelegation.RedelegationEntryIndex += 1; + + states = states.SetDPoSState(redelegationEntry.Address, redelegationEntry.Serialize()); + states = states.SetDPoSState(redelegation.Address, redelegation.Serialize()); + + states = UnbondingSetCtrl.AddRedelegationAddressSet(states, redelegation.Address); + + return states; + } + + // This have to be called for each block, + // to update staking status and generate block with updated validators. + // Would it be better to declare this on out of this class? + internal static IWorld Complete( + IWorld states, + IActionContext ctx, + Address redelegationAddress) + { + long blockHeight = ctx.BlockIndex; + if (!(GetRedelegation(states, redelegationAddress) is { } redelegation)) + { + throw new NullRedelegationException(redelegationAddress); + } + + List completedIndices = new List(); +#pragma warning disable LAA1002 + foreach (KeyValuePair redelegationEntryAddressKV + in redelegation.RedelegationEntryAddresses) + { + IValue? serializedRedelegationEntry + = states.GetDPoSState(redelegationEntryAddressKV.Value); + if (serializedRedelegationEntry == null) + { + continue; + } + + RedelegationEntry redelegationEntry + = new RedelegationEntry(serializedRedelegationEntry); + + if (redelegationEntry.IsMatured(blockHeight)) + { + completedIndices.Add(redelegationEntry.Index); + } + } +#pragma warning restore LAA1002 + + foreach (long index in completedIndices) + { + redelegation.RedelegationEntryAddresses.Remove(index); + } + + states = states.SetDPoSState(redelegation.Address, redelegation.Serialize()); + + if (redelegation.RedelegationEntryAddresses.Count == 0) + { + states = UnbondingSetCtrl.RemoveRedelegationAddressSet( + states, redelegation.Address); + } + + return states; + } + } +} 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 new file mode 100644 index 0000000000..e372052f74 --- /dev/null +++ b/Lib9c/Action/DPoS/Control/UnbondingSetCtrl.cs @@ -0,0 +1,139 @@ +#nullable enable +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Model; + +namespace Nekoyume.Action.DPoS.Control +{ + internal static class UnbondingSetCtrl + { + internal static UnbondingSet? GetUnbondingSet(IWorldState states) + { + if (states.GetDPoSState(ReservedAddress.UnbondingSet) is { } value) + { + return new UnbondingSet(value); + } + + return null; + } + + internal static (IWorld, UnbondingSet) FetchUnbondingSet(IWorld states) + { + UnbondingSet unbondingSet; + if (states.GetDPoSState(ReservedAddress.UnbondingSet) is { } value) + { + unbondingSet = new UnbondingSet(value); + } + else + { + unbondingSet = new UnbondingSet(); + states = states.SetDPoSState(unbondingSet.Address, unbondingSet.Serialize()); + } + + return (states, unbondingSet); + } + + internal static IWorld CompleteValidatorSet(IWorld states, IActionContext ctx) + { + UnbondingSet unbondingSet; + (states, unbondingSet) = FetchUnbondingSet(states); + foreach (Address address in unbondingSet.ValidatorAddressSet) + { + states = ValidatorCtrl.Complete(states, ctx, address); + } + + return states; + } + + internal static IWorld CompleteUndelegationSet(IWorld states, IActionContext ctx) + { + UnbondingSet unbondingSet; + (states, unbondingSet) = FetchUnbondingSet(states); + foreach (Address address in unbondingSet.UndelegationAddressSet) + { + states = UndelegateCtrl.Complete(states, ctx, address); + } + + return states; + } + + internal static IWorld CompleteRedelegationSet(IWorld states, IActionContext ctx) + { + UnbondingSet unbondingSet; + (states, unbondingSet) = FetchUnbondingSet(states); + foreach (Address address in unbondingSet.RedelegationAddressSet) + { + states = RedelegateCtrl.Complete(states, ctx, address); + } + + return states; + } + + internal static IWorld Complete(IWorld states, IActionContext ctx) + { + states = CompleteValidatorSet(states, ctx); + states = CompleteUndelegationSet(states, ctx); + states = CompleteRedelegationSet(states, ctx); + + return states; + } + + internal static IWorld AddValidatorAddressSet(IWorld states, Address validatorAddress) + { + UnbondingSet unbondingSet; + (states, unbondingSet) = FetchUnbondingSet(states); + unbondingSet.ValidatorAddressSet.Add(validatorAddress); + states = states.SetDPoSState(unbondingSet.Address, unbondingSet.Serialize()); + return states; + } + + internal static IWorld AddUndelegationAddressSet(IWorld states, Address undelegationAddress) + { + UnbondingSet unbondingSet; + (states, unbondingSet) = FetchUnbondingSet(states); + unbondingSet.UndelegationAddressSet.Add(undelegationAddress); + states = states.SetDPoSState(unbondingSet.Address, unbondingSet.Serialize()); + return states; + } + + internal static IWorld AddRedelegationAddressSet(IWorld states, Address redelegationAddress) + { + UnbondingSet unbondingSet; + (states, unbondingSet) = FetchUnbondingSet(states); + unbondingSet.RedelegationAddressSet.Add(redelegationAddress); + states = states.SetDPoSState(unbondingSet.Address, unbondingSet.Serialize()); + return states; + } + + internal static IWorld RemoveValidatorAddressSet(IWorld states, Address validatorAddress) + { + UnbondingSet unbondingSet; + (states, unbondingSet) = FetchUnbondingSet(states); + unbondingSet.ValidatorAddressSet.Remove(validatorAddress); + states = states.SetDPoSState(unbondingSet.Address, unbondingSet.Serialize()); + return states; + } + + internal static IWorld RemoveUndelegationAddressSet( + IWorld states, Address undelegationAddress) + { + UnbondingSet unbondingSet; + (states, unbondingSet) = FetchUnbondingSet(states); + unbondingSet.UndelegationAddressSet.Remove(undelegationAddress); + states = states.SetDPoSState(unbondingSet.Address, unbondingSet.Serialize()); + return states; + } + + internal static IWorld RemoveRedelegationAddressSet( + IWorld states, Address redelegationAddress) + { + UnbondingSet unbondingSet; + (states, unbondingSet) = FetchUnbondingSet(states); + unbondingSet.RedelegationAddressSet.Remove(redelegationAddress); + states = states.SetDPoSState(unbondingSet.Address, unbondingSet.Serialize()); + return states; + } + } +} diff --git a/Lib9c/Action/DPoS/Control/UndelegateCtrl.cs b/Lib9c/Action/DPoS/Control/UndelegateCtrl.cs new file mode 100644 index 0000000000..a42c0b3450 --- /dev/null +++ b/Lib9c/Action/DPoS/Control/UndelegateCtrl.cs @@ -0,0 +1,377 @@ +#nullable enable +using System; +using System.Collections.Generic; +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.Exception; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Model; +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) + { + if (state.GetDPoSState(undelegationAddress) is { } value) + { + return new Undelegation(value); + } + + 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, + Address validatorAddress) + { + Address undelegationAddress = Undelegation.DeriveAddress( + delegatorAddress, validatorAddress); + Undelegation undelegation; + if (state.GetDPoSState(undelegationAddress) is { } value) + { + undelegation = new Undelegation(value); + } + else + { + undelegation = new Undelegation(delegatorAddress, validatorAddress); + state = state.SetDPoSState(undelegation.Address, undelegation.Serialize()); + } + + return (state, undelegation); + } + + internal static IWorld Execute( + IWorld states, + IActionContext ctx, + Address delegatorAddress, + Address validatorAddress, + FungibleAssetValue share, + IImmutableSet nativeTokens) + { + // TODO: Failure condition + // 1. Delegation does not exist + // 2. Validator does not exist + // 3. Delegation has less shares than worth of amount + // 4. Existing undelegation has maximum entries + + long blockHeight = ctx.BlockIndex; + + // Currency check + if (!share.Currency.Equals(Asset.Share)) + { + throw new Exception.InvalidCurrencyException(Asset.Share, share.Currency); + } + + Undelegation undelegation; + (states, undelegation) = FetchUndelegation(states, delegatorAddress, validatorAddress); + + if (undelegation.UndelegationEntryAddresses.Count + >= Undelegation.MaximumUndelegationEntries) + { + throw new MaximumUndelegationEntriesException( + undelegation.Address, undelegation.UndelegationEntryAddresses.Count); + } + + // Validator loading + if (!(ValidatorCtrl.GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + // Unbonding + FungibleAssetValue unbondingConsensusToken; + (states, unbondingConsensusToken) = Bond.Cancel( + states, + ctx, + share, + undelegation.ValidatorAddress, + undelegation.DelegationAddress, + nativeTokens); + + // Governance token pool transfer + if (validator.Status == BondingStatus.Bonded) + { + states = states.TransferAsset( + ctx, + ReservedAddress.BondedPool, + ReservedAddress.UnbondedPool, + Asset.GovernanceFromConsensus(unbondingConsensusToken)); + } + + // Entry register + UndelegationEntry undelegationEntry = new UndelegationEntry( + undelegation.Address, + unbondingConsensusToken, + undelegation.UndelegationEntryIndex, + blockHeight); + undelegation.UndelegationEntryAddresses.Add( + undelegationEntry.Index, undelegationEntry.Address); + undelegation.UndelegationEntryIndex += 1; + + // TODO: Global state indexing is also needed + states = states.SetDPoSState(undelegationEntry.Address, undelegationEntry.Serialize()); + + states = states.SetDPoSState(undelegation.Address, undelegation.Serialize()); + + states = UnbondingSetCtrl.AddUndelegationAddressSet(states, undelegation.Address); + + return states; + } + + internal static IWorld Cancel( + IWorld states, + IActionContext ctx, + Address undelegationAddress, + FungibleAssetValue cancelledConsensusToken, + IImmutableSet nativeTokens) + { + long blockHeight = ctx.BlockIndex; + + // Currency check + if (!cancelledConsensusToken.Currency.Equals(Asset.ConsensusToken)) + { + throw new Exception.InvalidCurrencyException( + Asset.ConsensusToken, cancelledConsensusToken.Currency); + } + + if (!(GetUndelegation(states, undelegationAddress) is { } undelegation)) + { + throw new NullUndelegationException(undelegationAddress); + } + + // Validator loading + if (!(ValidatorCtrl.GetValidator( + states, undelegation.ValidatorAddress) is { } validator)) + { + throw new NullValidatorException(undelegation.ValidatorAddress); + } + + // Copy of cancelling amount + FungibleAssetValue cancellingConsensusToken = + new FungibleAssetValue( + Asset.ConsensusToken, + cancelledConsensusToken.MajorUnit, + cancelledConsensusToken.MinorUnit); + + // Iterate all entries + List undelegationEntryIndices = new List(); +#pragma warning disable LAA1002 + foreach (KeyValuePair undelegationEntryAddressKV + in undelegation.UndelegationEntryAddresses) + { + // Load entry + IValue? serializedUndelegationEntry + = states.GetDPoSState(undelegationEntryAddressKV.Value); + + // Skip empty entry + if (serializedUndelegationEntry == null) + { + continue; + } + + UndelegationEntry undelegationEntry + = new UndelegationEntry(serializedUndelegationEntry); + + // Double check for unbonded entry + if (blockHeight >= undelegationEntry.CompletionBlockHeight) + { + throw new PostmatureUndelegationEntryException( + blockHeight, + undelegationEntry.CompletionBlockHeight, + undelegationEntry.Address); + } + + // Check if cancelledConsensusToken is less than total undelegation + if (cancellingConsensusToken.RawValue < 0) + { + throw new InsufficientFungibleAssetValueException( + cancelledConsensusToken, + cancelledConsensusToken + cancellingConsensusToken, + $"Undelegation {undelegationAddress} has insufficient consensusToken"); + } + + // Apply unbonding + if (cancellingConsensusToken < undelegationEntry.UnbondingConsensusToken) + { + undelegationEntry.UnbondingConsensusToken -= cancellingConsensusToken; + states = states.SetDPoSState( + undelegationEntry.Address, undelegationEntry.Serialize()); + break; + } + + // If cancelling amount is more than current entry, save and skip + else + { + cancellingConsensusToken -= undelegationEntry.UnbondingConsensusToken; + undelegationEntryIndices.Add(undelegationEntry.Index); + } + } +#pragma warning restore LAA1002 + + (states, _) = Bond.Execute( + states, + ctx, + cancelledConsensusToken, + undelegation.ValidatorAddress, + undelegation.DelegationAddress, + nativeTokens); + + if (validator.Status == BondingStatus.Bonded) + { + states = states.TransferAsset( + ctx, + ReservedAddress.UnbondedPool, + ReservedAddress.BondedPool, + Asset.GovernanceFromConsensus(cancelledConsensusToken)); + } + + undelegationEntryIndices.ForEach( + idx => undelegation.UndelegationEntryAddresses.Remove(idx)); + + states = states.SetDPoSState(undelegation.Address, undelegation.Serialize()); + + if (undelegation.UndelegationEntryAddresses.Count == 0) + { + states = UnbondingSetCtrl.RemoveUndelegationAddressSet( + states, undelegation.Address); + } + + return states; + } + + // This have to be called for each block, + // to update staking status and generate block with updated validators. + // Would it be better to declare this on out of this class? + internal static IWorld Complete( + IWorld states, + IActionContext ctx, + Address undelegationAddress) + { + long blockHeight = ctx.BlockIndex; + if (!(GetUndelegation(states, undelegationAddress) is { } undelegation)) + { + throw new NullUndelegationException(undelegationAddress); + } + + List completedIndices = new List(); + + // Iterate all entries +#pragma warning disable LAA1002 + foreach (KeyValuePair undelegationEntryAddressKV + in undelegation.UndelegationEntryAddresses) + { + // Load entry + IValue? serializedUndelegationEntry + = states.GetDPoSState(undelegationEntryAddressKV.Value); + + // Skip empty entry + if (serializedUndelegationEntry == null) + { + continue; + } + + UndelegationEntry undelegationEntry + = new UndelegationEntry(serializedUndelegationEntry); + + // Complete matured entries + if (undelegationEntry.IsMatured(blockHeight)) + { + // Pay back governance token to delegator + states = states.TransferAsset( + ctx, + ReservedAddress.UnbondedPool, + undelegation.DelegatorAddress, + Asset.GovernanceFromConsensus(undelegationEntry.UnbondingConsensusToken)); + + // Remove entry + completedIndices.Add(undelegationEntry.Index); + } + } +#pragma warning restore LAA1002 + + foreach (long index in completedIndices) + { + undelegation.UndelegationEntryAddresses.Remove(index); + } + + states = states.SetDPoSState(undelegation.Address, undelegation.Serialize()); + + if (undelegation.UndelegationEntryAddresses.Count == 0) + { + states = UnbondingSetCtrl.RemoveUndelegationAddressSet( + states, undelegation.Address); + } + + return states; + } + } +} diff --git a/Lib9c/Action/DPoS/Control/ValidatorCtrl.cs b/Lib9c/Action/DPoS/Control/ValidatorCtrl.cs new file mode 100644 index 0000000000..ea4290db82 --- /dev/null +++ b/Lib9c/Action/DPoS/Control/ValidatorCtrl.cs @@ -0,0 +1,351 @@ +#nullable enable +using System; +using System.Collections.Immutable; +using System.Linq; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Action.DPoS.Exception; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Model; +using Nekoyume.Module; + +namespace Nekoyume.Action.DPoS.Control +{ + internal static class ValidatorCtrl + { + internal static Validator? GetValidator(IWorldState states, Address validatorAddress) + { + if (states.GetDPoSState(validatorAddress) is { } value) + { + return new Validator(value); + } + + return null; + } + + internal static (IWorld, Validator) FetchValidator( + IWorld states, + Address operatorAddress, + PublicKey operatorPublicKey) + { + if (!operatorAddress.Equals(operatorPublicKey.Address)) + { + throw new PublicKeyAddressMatchingException(operatorAddress, operatorPublicKey); + } + + Address validatorAddress = Validator.DeriveAddress(operatorAddress); + Validator validator; + if (states.GetDPoSState(validatorAddress) is { } value) + { + validator = new Validator(value); + } + else + { + validator = new Validator(operatorAddress, operatorPublicKey); + states = states.SetDPoSState(validator.Address, validator.Serialize()); + states = ValidatorSigningInfoCtrl.SetSigningInfo( + states, + new ValidatorSigningInfo { Address = validator.Address }); + } + + return (states, validator); + } + + internal static IWorld Create( + IWorld states, + IActionContext ctx, + Address operatorAddress, + PublicKey operatorPublicKey, + FungibleAssetValue governanceToken, + IImmutableSet nativeTokens) + { + if (!governanceToken.Currency.Equals(Asset.GovernanceToken)) + { + throw new Exception.InvalidCurrencyException(Asset.GovernanceToken, governanceToken.Currency); + } + + FungibleAssetValue consensusToken = Asset.ConsensusFromGovernance(governanceToken); + if (consensusToken < Validator.MinSelfDelegation) + { + throw new InsufficientFungibleAssetValueException( + Validator.MinSelfDelegation, consensusToken, "Insufficient self delegation"); + } + + Address validatorAddress = Validator.DeriveAddress(operatorAddress); + if (states.GetDPoSState(validatorAddress) != null) + { + throw new DuplicatedValidatorException(validatorAddress); + } + + Validator validator; + (states, validator) = FetchValidator(states, operatorAddress, operatorPublicKey); + + states = DelegateCtrl.Execute( + states, + ctx, + operatorAddress, + validator.Address, + 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( + IWorldState states, Address validatorAddress, FungibleAssetValue consensusToken) + { + if (!(GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + FungibleAssetValue validatorConsensusToken + = states.GetBalance(validator.Address, Asset.ConsensusToken); + + if (validator.DelegatorShares.Equals(Asset.Share * 0)) + { + return FungibleAssetValue.FromRawValue( + Asset.Share, consensusToken.RawValue); + } + + if (validatorConsensusToken.RawValue == 0) + { + return null; + } + + FungibleAssetValue share + = (validator.DelegatorShares + * consensusToken.RawValue) + .DivRem(validatorConsensusToken.RawValue, out _); + + return share; + } + + internal static FungibleAssetValue? TokenPortionByShare( + IWorld states, + Address validatorAddress, + FungibleAssetValue token, + FungibleAssetValue share) + { + if (!(GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + if (validator.DelegatorShares.RawValue == 0) + { + return null; + } + + var (tokenPortion, _) + = (token * share.RawValue) + .DivRem(validator.DelegatorShares.RawValue); + + return tokenPortion; + } + + internal static FungibleAssetValue? ConsensusTokenFromShare( + IWorld states, Address validatorAddress, FungibleAssetValue share) + { + if (!(GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + FungibleAssetValue validatorConsensusToken + = states.GetBalance(validator.Address, Asset.ConsensusToken); + + // Is below conditional statement right? + // Need to be investigated + if (validatorConsensusToken.RawValue == 0) + { + return null; + } + + if (validator.DelegatorShares.RawValue == 0) + { + return null; + } + + FungibleAssetValue consensusToken + = (validatorConsensusToken + * share.RawValue) + .DivRem(validator.DelegatorShares.RawValue, out _); + + return consensusToken; + } + + internal static IWorld Bond( + IWorld states, + IActionContext ctx, + Address validatorAddress) + { + if (!(GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + validator.UnbondingCompletionBlockHeight = -1; + if (validator.Status != BondingStatus.Bonded) + { + states = states.TransferAsset( + ctx, + ReservedAddress.UnbondedPool, + ReservedAddress.BondedPool, + Asset.GovernanceFromConsensus( + states.GetBalance(validator.Address, Asset.ConsensusToken))); + } + + validator.Status = BondingStatus.Bonded; + states = states.SetDPoSState(validator.Address, validator.Serialize()); + return states; + } + + internal static IWorld Unbond( + IWorld states, + IActionContext ctx, + Address validatorAddress) + { + long blockHeight = ctx.BlockIndex; + if (!(GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + validator.UnbondingCompletionBlockHeight = blockHeight + UnbondingSet.Period; + if (validator.Status == BondingStatus.Bonded) + { + var consensusToken = states.GetBalance(validator.Address, Asset.ConsensusToken); + if (consensusToken.RawValue > 0) + { + // Transfer consensus token to unbonded pool if remaining. + states = states.TransferAsset( + ctx, + ReservedAddress.BondedPool, + ReservedAddress.UnbondedPool, + Asset.GovernanceFromConsensus( + states.GetBalance(validator.Address, Asset.ConsensusToken))); + } + } + + validator.Status = BondingStatus.Unbonding; + states = states.SetDPoSState(validator.Address, validator.Serialize()); + + states = UnbondingSetCtrl.AddValidatorAddressSet(states, validator.Address); + + return states; + } + + internal static IWorld Complete( + IWorld states, + IActionContext ctx, + Address validatorAddress) + { + long blockHeight = ctx.BlockIndex; + if (!(GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + if (!validator.IsMatured(blockHeight) || (validator.Status != BondingStatus.Unbonding)) + { + return states; + } + + validator.Status = BondingStatus.Unbonded; + states = states.SetDPoSState(validator.Address, validator.Serialize()); + + states = UnbondingSetCtrl.RemoveValidatorAddressSet(states, validator.Address); + + // Later implemented get rid of validator + if (validator.DelegatorShares == Asset.Share * 0) + { + } + + 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 new file mode 100644 index 0000000000..888b999dd3 --- /dev/null +++ b/Lib9c/Action/DPoS/Control/ValidatorDelegationSetCtrl.cs @@ -0,0 +1,73 @@ +#nullable enable +using Libplanet.Action.State; +using Libplanet.Crypto; +using Nekoyume.Action.DPoS.Model; + +namespace Nekoyume.Action.DPoS.Control +{ + internal static class ValidatorDelegationSetCtrl + { + internal static ValidatorDelegationSet? GetValidatorDelegationSet( + IWorldState states, Address validatorAddress) + { + Address validatorDelegationSetAddress = ValidatorDelegationSet.DeriveAddress( + validatorAddress); + + if (states.GetDPoSState(validatorDelegationSetAddress) is { } value) + { + return new ValidatorDelegationSet(value); + } + + return null; + } + + internal static (IWorld, ValidatorDelegationSet) FetchValidatorDelegationSet( + IWorld states, Address validatorAddress) + { + Address validatorDelegationSetAddress = ValidatorDelegationSet.DeriveAddress( + validatorAddress); + + ValidatorDelegationSet validatorDelegationSet; + if (states.GetDPoSState(validatorDelegationSetAddress) is { } value) + { + validatorDelegationSet = new ValidatorDelegationSet(value); + } + else + { + validatorDelegationSet = new ValidatorDelegationSet(validatorAddress); + states = states.SetDPoSState( + validatorDelegationSetAddress, validatorDelegationSet.Serialize()); + } + + return (states, validatorDelegationSet); + } + + internal static IWorld Add( + IWorld states, + Address validatorAddress, + Address delegationAddress) + { + ValidatorDelegationSet validatorDelegationSet; + (states, validatorDelegationSet) + = FetchValidatorDelegationSet(states, validatorAddress); + validatorDelegationSet.Add(delegationAddress); + states = states.SetDPoSState( + validatorDelegationSet.Address, validatorDelegationSet.Serialize()); + return states; + } + + internal static IWorld Remove( + IWorld states, + Address validatorAddress, + Address delegationAddress) + { + ValidatorDelegationSet validatorDelegationSet; + (states, validatorDelegationSet) + = FetchValidatorDelegationSet(states, validatorAddress); + validatorDelegationSet.Remove(delegationAddress); + states = states.SetDPoSState( + validatorDelegationSet.Address, validatorDelegationSet.Serialize()); + return states; + } + } +} diff --git a/Lib9c/Action/DPoS/Control/ValidatorPowerIndexCtrl.cs b/Lib9c/Action/DPoS/Control/ValidatorPowerIndexCtrl.cs new file mode 100644 index 0000000000..be8fc07de7 --- /dev/null +++ b/Lib9c/Action/DPoS/Control/ValidatorPowerIndexCtrl.cs @@ -0,0 +1,93 @@ +#nullable enable +using System.Collections.Generic; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Action.DPoS.Exception; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Model; +using Nekoyume.Module; + +namespace Nekoyume.Action.DPoS.Control +{ + internal static class ValidatorPowerIndexCtrl + { + internal static ValidatorPowerIndex? GetValidatorPowerIndex(IWorldState states) + { + if (states.GetDPoSState(ReservedAddress.ValidatorPowerIndex) is { } value) + { + return new ValidatorPowerIndex(value); + } + + return null; + } + + internal static (IWorld, ValidatorPowerIndex) FetchValidatorPowerIndex(IWorld states) + { + ValidatorPowerIndex validatorPowerIndex; + if (states.GetDPoSState(ReservedAddress.ValidatorPowerIndex) is { } value) + { + validatorPowerIndex = new ValidatorPowerIndex(value); + } + else + { + validatorPowerIndex = new ValidatorPowerIndex(); + states = states.SetDPoSState( + validatorPowerIndex.Address, validatorPowerIndex.Serialize()); + } + + return (states, validatorPowerIndex); + } + + internal static IWorld Update( + IWorld states, + Address validatorAddress) + { + ValidatorPowerIndex validatorPowerIndex; + (states, validatorPowerIndex) = FetchValidatorPowerIndex(states); + validatorPowerIndex.Index.RemoveWhere( + key => key.ValidatorAddress.Equals(validatorAddress)); + if (!(ValidatorCtrl.GetValidator(states, validatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorAddress); + } + + if (validator.Jailed) + { + return states; + } + + FungibleAssetValue consensusToken = states.GetBalance( + validatorAddress, Asset.ConsensusToken); + ValidatorPower validatorPower + = new ValidatorPower(validatorAddress, validator.OperatorPublicKey, consensusToken); + validatorPowerIndex.Index.Add(validatorPower); + states = states.SetDPoSState(validatorPowerIndex.Address, validatorPowerIndex.Serialize()); + return states; + } + + internal static IWorld Update(IWorld states, IEnumerable
validatorAddresses) + { + foreach (Address validatorAddress in validatorAddresses) + { + states = Update(states, validatorAddress); + } + + 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 new file mode 100644 index 0000000000..a9bc7c14ab --- /dev/null +++ b/Lib9c/Action/DPoS/Control/ValidatorRewardsCtrl.cs @@ -0,0 +1,86 @@ +#nullable enable +using System.Collections.Immutable; +using System.Linq; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Action.DPoS.Model; + +namespace Nekoyume.Action.DPoS.Control +{ + internal static class ValidatorRewardsCtrl + { + internal static ValidatorRewards? GetValidatorRewards( + IWorldState states, + Address validatorAddress) + { + if (states.GetDPoSState(validatorAddress) is { } value) + { + return new ValidatorRewards(value); + } + + return null; + } + + internal static (IWorld, ValidatorRewards) FetchValidatorRewards( + IWorld states, + Address validatorAddress, + Currency currency) + { + Address validatorRewardsAddress + = ValidatorRewards.DeriveAddress(validatorAddress, currency); + ValidatorRewards validatorRewards; + if (states.GetDPoSState(validatorRewardsAddress) is { } value) + { + validatorRewards = new ValidatorRewards(value); + } + else + { + validatorRewards = new ValidatorRewards(validatorAddress, currency); + states = states.SetDPoSState(validatorRewards.Address, validatorRewards.Serialize()); + } + + return (states, validatorRewards); + } + + internal static ImmutableSortedDictionary RewardsBetween( + IWorld states, + Address validatorAddress, + Currency currency, + long minBlockHeight, + long maxBlockHeight) + { + ValidatorRewards validatorRewards; + (_, validatorRewards) = FetchValidatorRewards(states, validatorAddress, currency); + return validatorRewards.Rewards.Where( + kv => minBlockHeight <= kv.Key && kv.Key < maxBlockHeight) + .ToImmutableSortedDictionary(); + } + + internal static FungibleAssetValue RewardSumBetween( + IWorld states, + Address validatorAddress, + Currency currency, + long minBlockHeight, + long maxBlockHeight) + { + return RewardsBetween( + states, validatorAddress, currency, minBlockHeight, maxBlockHeight) + .Aggregate(currency * 0, (total, next) => total + next.Value); + } + + internal static IWorld Add( + IWorld states, + Address validatorAddress, + Currency currency, + long blockHeight, + FungibleAssetValue reward) + { + ValidatorRewards validatorRewards; + (states, validatorRewards) = FetchValidatorRewards(states, validatorAddress, currency); + validatorRewards.Add(blockHeight, reward); + states = states.SetDPoSState(validatorRewards.Address, validatorRewards.Serialize()); + return states; + } + } +} diff --git a/Lib9c/Action/DPoS/Control/ValidatorSetCtrl.cs b/Lib9c/Action/DPoS/Control/ValidatorSetCtrl.cs new file mode 100644 index 0000000000..3440bf5f6d --- /dev/null +++ b/Lib9c/Action/DPoS/Control/ValidatorSetCtrl.cs @@ -0,0 +1,138 @@ +#nullable enable +using System.Linq; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Nekoyume.Action.DPoS.Exception; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Model; +using Nekoyume.Module; + +namespace Nekoyume.Action.DPoS.Control +{ + internal static class ValidatorSetCtrl + { + internal static ValidatorSet? GetValidatorSet(IWorldState states, Address address) + { + if (states.GetDPoSState(address) is { } value) + { + return new ValidatorSet(value); + } + + return null; + } + + internal static (IWorld, ValidatorSet) FetchValidatorSet(IWorld states, Address address) + { + ValidatorSet validatorSet; + if (states.GetDPoSState(address) is { } value) + { + validatorSet = new ValidatorSet(value); + } + else + { + validatorSet = new ValidatorSet(); + states = states.SetDPoSState( + address, validatorSet.Serialize()); + } + + return (states, validatorSet); + } + + internal static (IWorld, ValidatorSet) FetchBondedValidatorSet(IWorld states) + => FetchValidatorSet(states, ReservedAddress.BondedValidatorSet); + + // Have to be called on tip changed + internal static IWorld Update(IWorld states, IActionContext ctx) + { + states = UpdateSets(states); + states = UpdateBondedSetElements(states, ctx); + states = UpdateUnbondedSetElements(states, ctx); + states = UnbondingSetCtrl.Complete(states, ctx); + + return states; + } + + internal static IWorld UpdateSets(IWorld states) + { + ValidatorSet previousBondedSet; + (states, previousBondedSet) = FetchValidatorSet( + states, ReservedAddress.BondedValidatorSet); + ValidatorSet bondedSet = new ValidatorSet(); + ValidatorSet unbondedSet = new ValidatorSet(); + ValidatorPowerIndex validatorPowerIndex; + (states, validatorPowerIndex) + = ValidatorPowerIndexCtrl.FetchValidatorPowerIndex(states); + + foreach (var item in validatorPowerIndex.Index.Select((value, index) => (value, index))) + { + if (!(ValidatorCtrl.GetValidator( + states, item.value.ValidatorAddress) is { } validator)) + { + throw new NullValidatorException(item.value.ValidatorAddress); + } + + if (validator.Jailed) + { + throw new JailedValidatorException(validator.Address); + } + + if (item.index >= ValidatorSet.MaxBondedSetSize || + states.GetBalance(item.value.ValidatorAddress, Asset.ConsensusToken) + <= Asset.ConsensusToken * 0) + { + unbondedSet.Add(item.value); + } + else + { + bondedSet.Add(item.value); + } + } + + states = states.SetDPoSState( + ReservedAddress.PreviousBondedValidatorSet, previousBondedSet.Serialize()); + states = states.SetDPoSState( + ReservedAddress.BondedValidatorSet, bondedSet.Serialize()); + states = states.SetDPoSState( + ReservedAddress.UnbondedValidatorSet, unbondedSet.Serialize()); + + return states; + } + + internal static IWorld UpdateBondedSetElements(IWorld states, IActionContext ctx) + { + ValidatorSet bondedSet; + (states, bondedSet) = FetchBondedValidatorSet(states); + foreach (ValidatorPower validatorPower in bondedSet.Set) + { + if (!(ValidatorCtrl.GetValidator( + states, validatorPower.ValidatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorPower.ValidatorAddress); + } + + states = ValidatorCtrl.Bond(states, ctx, validatorPower.ValidatorAddress); + } + + return states; + } + + internal static IWorld UpdateUnbondedSetElements(IWorld states, IActionContext ctx) + { + ValidatorSet unbondedSet; + (states, unbondedSet) = FetchValidatorSet(states, ReservedAddress.UnbondedValidatorSet); + foreach (ValidatorPower validatorPower in unbondedSet.Set) + { + if (!(ValidatorCtrl.GetValidator( + states, validatorPower.ValidatorAddress) is { } validator)) + { + throw new NullValidatorException(validatorPower.ValidatorAddress); + } + + states = ValidatorCtrl.Unbond(states, ctx, validatorPower.ValidatorAddress); + } + + return states; + } + } +} 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/Delegate.cs b/Lib9c/Action/DPoS/Delegate.cs new file mode 100644 index 0000000000..89649b5a27 --- /dev/null +++ b/Lib9c/Action/DPoS/Delegate.cs @@ -0,0 +1,82 @@ +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 specified + /// of tokens to a given . + /// + [ActionType(ActionTypeValue)] + public sealed class Delegate : ActionBase + { + private const string ActionTypeValue = "delegate"; + + /// + /// Creates a new instance of action. + /// + /// The of the validator + /// to delegate tokens. + /// The amount of the asset to be delegated. + public Delegate(Address validator, FungibleAssetValue amount) + { + Validator = validator; + Amount = amount; + } + + public Delegate() + { + // 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()) + .Add("amount", Amount.Serialize()); + + /// + public override void LoadPlainValue(IValue plainValue) + { + var dict = (Bencodex.Types.Dictionary)plainValue; + Validator = dict["validator"].ToAddress(); + Amount = dict["amount"].ToFungibleAssetValue(); + } + + /// + public override IWorld Execute(IActionContext context) + { + context.UseGas(1); + IActionContext ctx = context; + var states = ctx.PreviousState; + + // if (ctx.Rehearsal) + // Rehearsal mode is not implemented + var nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + states = DelegateCtrl.Execute( + states, + ctx, + ctx.Signer, + Validator, + Amount, + nativeTokens); + + return states; + } + } +} diff --git a/Lib9c/Action/DPoS/Exception/DuplicatedValidatorException.cs b/Lib9c/Action/DPoS/Exception/DuplicatedValidatorException.cs new file mode 100644 index 0000000000..5779473259 --- /dev/null +++ b/Lib9c/Action/DPoS/Exception/DuplicatedValidatorException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Nekoyume.Action.DPoS.Exception +{ + public class DuplicatedValidatorException : System.Exception + { + public DuplicatedValidatorException(Address address) + : base($"Validator {address} is duplicated") + { + } + } +} diff --git a/Lib9c/Action/DPoS/Exception/InsufficientFungibleAssetValueException.cs b/Lib9c/Action/DPoS/Exception/InsufficientFungibleAssetValueException.cs new file mode 100644 index 0000000000..d5ea80dad9 --- /dev/null +++ b/Lib9c/Action/DPoS/Exception/InsufficientFungibleAssetValueException.cs @@ -0,0 +1,13 @@ +using Libplanet.Types.Assets; + +namespace Nekoyume.Action.DPoS.Exception +{ + public class InsufficientFungibleAssetValueException : System.Exception + { + public InsufficientFungibleAssetValueException( + FungibleAssetValue required, FungibleAssetValue actual, string message) + : base($"{message}, required : {required} > actual : {actual}") + { + } + } +} diff --git a/Lib9c/Action/DPoS/Exception/InvalidCurrencyException.cs b/Lib9c/Action/DPoS/Exception/InvalidCurrencyException.cs new file mode 100644 index 0000000000..8393643c3b --- /dev/null +++ b/Lib9c/Action/DPoS/Exception/InvalidCurrencyException.cs @@ -0,0 +1,12 @@ +using Libplanet.Types.Assets; + +namespace Nekoyume.Action.DPoS.Exception +{ + public class InvalidCurrencyException : System.Exception + { + public InvalidCurrencyException(Currency expected, Currency actual) + : base($"Expected {expected}, found {actual}") + { + } + } +} diff --git a/Lib9c/Action/DPoS/Exception/InvalidExchangeRateException.cs b/Lib9c/Action/DPoS/Exception/InvalidExchangeRateException.cs new file mode 100644 index 0000000000..d07815c2e6 --- /dev/null +++ b/Lib9c/Action/DPoS/Exception/InvalidExchangeRateException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Nekoyume.Action.DPoS.Exception +{ + public class InvalidExchangeRateException : System.Exception + { + public InvalidExchangeRateException(Address address) + : base($"Exchange of Validator {address} is invalid") + { + } + } +} diff --git a/Lib9c/Action/DPoS/Exception/JailedValidatorException.cs b/Lib9c/Action/DPoS/Exception/JailedValidatorException.cs new file mode 100644 index 0000000000..d007cd0708 --- /dev/null +++ b/Lib9c/Action/DPoS/Exception/JailedValidatorException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Nekoyume.Action.DPoS.Exception +{ + public class JailedValidatorException : System.Exception + { + public JailedValidatorException(Address address) + : base($"Validator {address} is jailed") + { + } + } +} diff --git a/Lib9c/Action/DPoS/Exception/MaximumRedelegationEntriesException.cs b/Lib9c/Action/DPoS/Exception/MaximumRedelegationEntriesException.cs new file mode 100644 index 0000000000..c525315da7 --- /dev/null +++ b/Lib9c/Action/DPoS/Exception/MaximumRedelegationEntriesException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Nekoyume.Action.DPoS.Exception +{ + public class MaximumRedelegationEntriesException : System.Exception + { + public MaximumRedelegationEntriesException(Address address, long count) + : base($"Redelegation {address} reached maximum entry size : {count}") + { + } + } +} diff --git a/Lib9c/Action/DPoS/Exception/MaximumUndelegationEntriesException.cs b/Lib9c/Action/DPoS/Exception/MaximumUndelegationEntriesException.cs new file mode 100644 index 0000000000..dc5845ef3e --- /dev/null +++ b/Lib9c/Action/DPoS/Exception/MaximumUndelegationEntriesException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Nekoyume.Action.DPoS.Exception +{ + public class MaximumUndelegationEntriesException : System.Exception + { + public MaximumUndelegationEntriesException(Address address, long count) + : base($"Undelegation {address} reached maximum entry size : {count}") + { + } + } +} diff --git a/Lib9c/Action/DPoS/Exception/NullDelegationException.cs b/Lib9c/Action/DPoS/Exception/NullDelegationException.cs new file mode 100644 index 0000000000..dca2e32dab --- /dev/null +++ b/Lib9c/Action/DPoS/Exception/NullDelegationException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Nekoyume.Action.DPoS.Exception +{ + public class NullDelegationException : System.Exception + { + public NullDelegationException(Address address) + : base($"Delegation {address} not found") + { + } + } +} diff --git a/Lib9c/Action/DPoS/Exception/NullNativeTokensException.cs b/Lib9c/Action/DPoS/Exception/NullNativeTokensException.cs new file mode 100644 index 0000000000..2bc1e73c80 --- /dev/null +++ b/Lib9c/Action/DPoS/Exception/NullNativeTokensException.cs @@ -0,0 +1,10 @@ +namespace Nekoyume.Action.DPoS.Exception +{ + public class NullNativeTokensException : System.Exception + { + public NullNativeTokensException() + : base($"At least one native token have to be set on block policy") + { + } + } +} diff --git a/Lib9c/Action/DPoS/Exception/NullRedelegationException.cs b/Lib9c/Action/DPoS/Exception/NullRedelegationException.cs new file mode 100644 index 0000000000..cb84eb5700 --- /dev/null +++ b/Lib9c/Action/DPoS/Exception/NullRedelegationException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Nekoyume.Action.DPoS.Exception +{ + public class NullRedelegationException : System.Exception + { + public NullRedelegationException(Address address) + : base($"Redelegation {address} not found") + { + } + } +} diff --git a/Lib9c/Action/DPoS/Exception/NullUndelegationException.cs b/Lib9c/Action/DPoS/Exception/NullUndelegationException.cs new file mode 100644 index 0000000000..41b06457e7 --- /dev/null +++ b/Lib9c/Action/DPoS/Exception/NullUndelegationException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Nekoyume.Action.DPoS.Exception +{ + public class NullUndelegationException : System.Exception + { + public NullUndelegationException(Address address) + : base($"Undelegation {address} not found") + { + } + } +} diff --git a/Lib9c/Action/DPoS/Exception/NullValidatorException.cs b/Lib9c/Action/DPoS/Exception/NullValidatorException.cs new file mode 100644 index 0000000000..491106ff4a --- /dev/null +++ b/Lib9c/Action/DPoS/Exception/NullValidatorException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Nekoyume.Action.DPoS.Exception +{ + public class NullValidatorException : System.Exception + { + public NullValidatorException(Address address) + : base($"Validator {address} not found") + { + } + } +} diff --git a/Lib9c/Action/DPoS/Exception/PostmatureUndelegationEntryException.cs b/Lib9c/Action/DPoS/Exception/PostmatureUndelegationEntryException.cs new file mode 100644 index 0000000000..0d85270857 --- /dev/null +++ b/Lib9c/Action/DPoS/Exception/PostmatureUndelegationEntryException.cs @@ -0,0 +1,14 @@ +using Libplanet.Crypto; + +namespace Nekoyume.Action.DPoS.Exception +{ + public class PostmatureUndelegationEntryException : System.Exception + { + public PostmatureUndelegationEntryException( + long blockHeight, long completionBlockHeight, Address address) + : base($"UndelegationEntry {address} is postmatured, " + + $"blockHeight : {blockHeight} > completionBlockHeight : {completionBlockHeight}") + { + } + } +} diff --git a/Lib9c/Action/DPoS/Exception/PublicKeyAddressMatchingException.cs b/Lib9c/Action/DPoS/Exception/PublicKeyAddressMatchingException.cs new file mode 100644 index 0000000000..34d44961b7 --- /dev/null +++ b/Lib9c/Action/DPoS/Exception/PublicKeyAddressMatchingException.cs @@ -0,0 +1,13 @@ +using Libplanet.Crypto; + +namespace Nekoyume.Action.DPoS.Exception +{ + public class PublicKeyAddressMatchingException : System.Exception + { + public PublicKeyAddressMatchingException(Address expected, PublicKey publicKey) + : base($"publicKey {publicKey} does not match to address " + + $": Expected {expected}, found {publicKey.Address}") + { + } + } +} diff --git a/Lib9c/Action/DPoS/InitializeValidators.cs b/Lib9c/Action/DPoS/InitializeValidators.cs new file mode 100644 index 0000000000..263ff662b1 --- /dev/null +++ b/Lib9c/Action/DPoS/InitializeValidators.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Numerics; +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; +using Nekoyume.Module; +using Serilog; + +namespace Nekoyume.Action.DPoS +{ + [ActionType(ActionTypeValue)] + public sealed class InitializeValidators : ActionBase + { + private const string ActionTypeValue = "initialize_validators"; + + public InitializeValidators(Dictionary validators) + { + Validators = validators.ToImmutableDictionary(); + } + + public InitializeValidators() + { + } + + public ImmutableDictionary Validators { get; set; } + + public override IValue PlainValue { + get + { + var validators = Dictionary.Empty; +#pragma warning disable LAA1002 + foreach (var (validator, power) in Validators) + { + validators = validators.Add((Binary)validator.Serialize(), power); + } +#pragma warning restore LAA1002 + + return Dictionary.Empty + .Add("type_id", new Text(ActionTypeValue)) + .Add("validators", validators); + } + } + + public override void LoadPlainValue(IValue plainValue) + { + var dict = (Bencodex.Types.Dictionary)plainValue; + var validatorDict = (Dictionary)dict["validators"]; + Validators = validatorDict.Select( + pair => + { + var key = pair.Key.ToPublicKey(); + var power = (Integer)pair.Value; + return new KeyValuePair(key, power); + }).ToImmutableDictionary(); + } + + public override IWorld Execute(IActionContext context) + { + Log.Debug("InitializeValidators"); + context.UseGas(1); + var states = context.PreviousState; + + var nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); +#pragma warning disable LAA1002 + foreach(var (validator, power) in Validators) + { + var amount = new FungibleAssetValue(Asset.GovernanceToken, power, 0); + states = states.MintAsset( + context, + validator.Address, + amount); + states = ValidatorCtrl.Create( + states, + context, + validator.Address, + validator, + amount, + nativeTokens); + } +#pragma warning restore LAA1002 + + Log.Debug("InitializeValidators complete"); + return states; + } + } +} diff --git a/Lib9c/Action/DPoS/Misc/Asset.cs b/Lib9c/Action/DPoS/Misc/Asset.cs new file mode 100644 index 0000000000..27f3cd454b --- /dev/null +++ b/Lib9c/Action/DPoS/Misc/Asset.cs @@ -0,0 +1,26 @@ +using System.Numerics; +using Libplanet.Types.Assets; + +namespace Nekoyume.Action.DPoS.Misc +{ + public struct Asset + { + public static readonly Currency GovernanceToken = + Currency.Legacy("NCG", 2, null); + + public static readonly Currency ConsensusToken = + Currency.Uncapped("ConsensusToken", 0, minters: null); + + public static readonly Currency Share = + Currency.Uncapped("Share", 0, minters: null); + + public static FungibleAssetValue ConsensusFromGovernance(FungibleAssetValue governanceToken) + => FungibleAssetValue.FromRawValue(ConsensusToken, governanceToken.RawValue); + + public static FungibleAssetValue ConsensusFromGovernance(BigInteger amount) + => ConsensusFromGovernance(GovernanceToken * amount); + + public static FungibleAssetValue GovernanceFromConsensus(FungibleAssetValue consensusToken) + => FungibleAssetValue.FromRawValue(GovernanceToken, consensusToken.RawValue); + } +} diff --git a/Lib9c/Action/DPoS/Misc/ReservedAddress.cs b/Lib9c/Action/DPoS/Misc/ReservedAddress.cs new file mode 100644 index 0000000000..592c2a2a64 --- /dev/null +++ b/Lib9c/Action/DPoS/Misc/ReservedAddress.cs @@ -0,0 +1,43 @@ +using Libplanet.Crypto; + +namespace Nekoyume.Action.DPoS.Misc +{ + public static class ReservedAddress + { + public static readonly Address DPoSAccountAddress + = new Address("0000000000000000000000000000000000100000"); + + public static readonly Address BondedPool + = new Address("0000000000000000000000000000000000100001"); + + public static readonly Address UnbondedPool + = new Address("0000000000000000000000000000000000100002"); + + public static readonly Address RewardPool + = new Address("0000000000000000000000000000000000100003"); + + public static readonly Address ValidatorPowerIndex + = new Address("0000000000000000000000000000000000100004"); + + public static readonly Address PreviousBondedValidatorSet + = new Address("0000000000000000000000000000000000100005"); + + public static readonly Address BondedValidatorSet + = new Address("0000000000000000000000000000000000100006"); + + public static readonly Address UnbondedValidatorSet + = new Address("0000000000000000000000000000000000100007"); + + public static readonly Address UnbondingSet + = new Address("0000000000000000000000000000000000100008"); + + public static readonly Address BlockRewardHistory + = new Address("0000000000000000000000000000000000100009"); + + public static readonly Address CommunityPool + = new Address("0000000000000000000000000000000000100010"); + + public static readonly Address ProposerInfo + = new Address("0000000000000000000000000000000000100011"); + } +} diff --git a/Lib9c/Action/DPoS/Model/BondingStatus.cs b/Lib9c/Action/DPoS/Model/BondingStatus.cs new file mode 100644 index 0000000000..e71c3b1934 --- /dev/null +++ b/Lib9c/Action/DPoS/Model/BondingStatus.cs @@ -0,0 +1,21 @@ +namespace Nekoyume.Action.DPoS.Model +{ + public enum BondingStatus : byte + { + /// + /// For delegation : Current delegation is bonded. + /// For validator : Current validator has enough consensus power to vote. + /// + Bonded = 0, + + /// + /// . + /// + Unbonding = 1, + + /// + /// . + /// + Unbonded = 2, + } +} diff --git a/Lib9c/Action/DPoS/Model/Delegation.cs b/Lib9c/Action/DPoS/Model/Delegation.cs new file mode 100644 index 0000000000..a204ab5629 --- /dev/null +++ b/Lib9c/Action/DPoS/Model/Delegation.cs @@ -0,0 +1,97 @@ +#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 +{ + public class Delegation : IEquatable + { + public Delegation(Address delegatorAddress, Address validatorAddress) + { + Address = DeriveAddress(delegatorAddress, validatorAddress); + DelegatorAddress = delegatorAddress; + ValidatorAddress = validatorAddress; + LatestDistributeHeight = 0; + } + + public Delegation(IValue serialized) + { + List serializedList = (List)serialized; + Address = serializedList[0].ToAddress(); + DelegatorAddress = serializedList[1].ToAddress(); + ValidatorAddress = serializedList[2].ToAddress(); + LatestDistributeHeight = serializedList[3].ToLong(); + } + + public Delegation(Delegation delegation) + { + Address = delegation.Address; + DelegatorAddress = delegation.DelegatorAddress; + ValidatorAddress = delegation.ValidatorAddress; + LatestDistributeHeight = delegation.LatestDistributeHeight; + } + + public Address Address { get; } + + public Address DelegatorAddress { get; } + + public Address ValidatorAddress { get; } + + 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); + } + + public static bool operator !=(Delegation obj, Delegation other) + { + return !(obj == other); + } + + public static Address DeriveAddress(Address delegatorAddress, Address validatorAddress) + { + return AddressHelper.Derive(AddressHelper.Derive(delegatorAddress, validatorAddress.ToByteArray()), "Delegation"); + } + + public IValue Serialize() + { + return List.Empty + .Add(Address.Serialize()) + .Add(DelegatorAddress.Serialize()) + .Add(ValidatorAddress.Serialize()) + .Add(LatestDistributeHeight.Serialize()); + } + + public override bool Equals(object? obj) + { + return Equals(obj as Delegation); + } + + public bool Equals(Delegation? other) + { + return !(other is null) && + Address.Equals(other.Address) && + DelegatorAddress.Equals(other.DelegatorAddress) && + ValidatorAddress.Equals(other.ValidatorAddress) && + LatestDistributeHeight.Equals(other.LatestDistributeHeight); + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(Address.ToByteArray()); + } + } +} 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/ProposerInfo.cs b/Lib9c/Action/DPoS/Model/ProposerInfo.cs new file mode 100644 index 0000000000..a5a6b0f8ba --- /dev/null +++ b/Lib9c/Action/DPoS/Model/ProposerInfo.cs @@ -0,0 +1,31 @@ +using Bencodex.Types; +using Libplanet.Crypto; +using Nekoyume.Action.DPoS.Util; + +namespace Nekoyume.Action.DPoS.Model +{ + public class ProposerInfo + { + public ProposerInfo(long blockIndex, Address proposer) + { + BlockIndex = blockIndex; + Proposer = proposer; + } + + public ProposerInfo(IValue bencoded) + { + var dict = (Dictionary)bencoded; + BlockIndex = (Integer)dict["index"]; + Proposer = dict["proposer"].ToAddress(); + } + + public long BlockIndex { get; } + + public Address Proposer { get; } + + public IValue Bencoded => + Dictionary.Empty + .Add((Text)"index", BlockIndex) + .Add((Text)"proposer", Proposer.Bencoded); + } +} diff --git a/Lib9c/Action/DPoS/Model/Redelegation.cs b/Lib9c/Action/DPoS/Model/Redelegation.cs new file mode 100644 index 0000000000..57c902ecbd --- /dev/null +++ b/Lib9c/Action/DPoS/Model/Redelegation.cs @@ -0,0 +1,142 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using Bencodex.Types; +using Libplanet.Common; +using Libplanet.Crypto; +using Nekoyume.Action.DPoS.Util; + +namespace Nekoyume.Action.DPoS.Model +{ + public class Redelegation : IEquatable + { + public Redelegation( + Address delegatorAddress, Address srcValidatorAddress, Address dstValidatorAddress) + { + Address = DeriveAddress(delegatorAddress, srcValidatorAddress, dstValidatorAddress); + DelegatorAddress = delegatorAddress; + SrcValidatorAddress = srcValidatorAddress; + DstValidatorAddress = dstValidatorAddress; + RedelegationEntryIndex = 0; + RedelegationEntryAddresses = new SortedList(); + } + + public Redelegation(IValue serialized) + { + List serializedList = (List)serialized; + Address = serializedList[0].ToAddress(); + DelegatorAddress = serializedList[1].ToAddress(); + SrcValidatorAddress = serializedList[2].ToAddress(); + DstValidatorAddress = serializedList[3].ToAddress(); + RedelegationEntryIndex = serializedList[4].ToLong(); + RedelegationEntryAddresses = new SortedList(); + foreach ( + IValue serializedRedelegationEntryAddress + in (List)serializedList[5]) + { + List items = (List)serializedRedelegationEntryAddress; + RedelegationEntryAddresses.Add(items[0].ToLong(), items[1].ToAddress()); + } + } + + public Redelegation(Redelegation redelegation) + { + Address = redelegation.Address; + DelegatorAddress = redelegation.DelegatorAddress; + SrcValidatorAddress = redelegation.SrcValidatorAddress; + DstValidatorAddress = redelegation.DstValidatorAddress; + RedelegationEntryIndex = redelegation.RedelegationEntryIndex; + RedelegationEntryAddresses = redelegation.RedelegationEntryAddresses; + } + + // TODO: Better structure + // This hard coding will cause some problems when it's modified + // May be it would be better to be serialized + public static int MaximumRedelegationEntries { get => 10; } + + public Address Address { get; } + + public Address DelegatorAddress { get; } + + public Address SrcValidatorAddress { get; } + + public Address DstValidatorAddress { get; } + + public Address SrcDelegationAddress + => Delegation.DeriveAddress(DelegatorAddress, SrcValidatorAddress); + + public Address DstDelegationAddress + => Delegation.DeriveAddress(DelegatorAddress, DstValidatorAddress); + + public long RedelegationEntryIndex { get; set; } + + public SortedList RedelegationEntryAddresses { get; set; } + + public static bool operator ==(Redelegation obj, Redelegation other) + { + return obj.Equals(other); + } + + public static bool operator !=(Redelegation obj, Redelegation other) + { + return !(obj == other); + } + + public static Address DeriveAddress( + Address delegatorAddress, Address srcValidatorAddress, Address dstValidatorAddress) + { + return AddressHelper.Derive(AddressHelper.Derive(AddressHelper.Derive(delegatorAddress, srcValidatorAddress.ToByteArray()), dstValidatorAddress.ToByteArray()), "Redelegation"); + } + + public IValue Serialize() + { + List serializedRedelegationEntryAddresses = List.Empty; +#pragma warning disable LAA1002 + foreach ( + KeyValuePair redelegationEntryAddressKV + in RedelegationEntryAddresses) + { + serializedRedelegationEntryAddresses = + serializedRedelegationEntryAddresses.Add( + List.Empty + .Add(redelegationEntryAddressKV.Key.Serialize()) + .Add(redelegationEntryAddressKV.Value.Serialize())); + } +#pragma warning restore LAA1002 + + return List.Empty + .Add(Address.Serialize()) + .Add(DelegatorAddress.Serialize()) + .Add(SrcValidatorAddress.Serialize()) + .Add(DstValidatorAddress.Serialize()) + .Add(RedelegationEntryIndex.Serialize()) + .Add(serializedRedelegationEntryAddresses); + } + + public override bool Equals(object? obj) + { + return Equals(obj as Redelegation); + } + + public bool Equals(Redelegation? other) + { + return !(other is null) && + Address.Equals(other.Address) && + DelegatorAddress.Equals(other.DelegatorAddress) && + SrcValidatorAddress.Equals(other.SrcValidatorAddress) && + DstValidatorAddress.Equals(other.DstValidatorAddress) && + SrcDelegationAddress.Equals(other.SrcDelegationAddress) && + DstDelegationAddress.Equals(other.DstDelegationAddress) && + RedelegationEntryIndex == other.RedelegationEntryIndex && +#pragma warning disable LAA1002 + RedelegationEntryAddresses.SequenceEqual(other.RedelegationEntryAddresses); +#pragma warning restore LAA1002 + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(Address.ToByteArray()); + } + } +} diff --git a/Lib9c/Action/DPoS/Model/RedelegationEntry.cs b/Lib9c/Action/DPoS/Model/RedelegationEntry.cs new file mode 100644 index 0000000000..dfbe2fc8eb --- /dev/null +++ b/Lib9c/Action/DPoS/Model/RedelegationEntry.cs @@ -0,0 +1,160 @@ +#nullable enable +using System; +using Bencodex.Types; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Util; + +namespace Nekoyume.Action.DPoS.Model +{ + public class RedelegationEntry : IEquatable + { + private FungibleAssetValue _redelegatingShare; + private FungibleAssetValue _unbondingConsensusToken; + private FungibleAssetValue _issuedShare; + + public RedelegationEntry( + Address redelegationAddress, + FungibleAssetValue redelegatingShare, + FungibleAssetValue unbondingConsensusToken, + FungibleAssetValue issuedShare, + long index, + long blockHeight) + { + 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) + { + List serializedList = (List)serialized; + Address = serializedList[0].ToAddress(); + RedelegationAddress = serializedList[1].ToAddress(); + RedelegatingShare = serializedList[2].ToFungibleAssetValue(); + 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; } + + public Address RedelegationAddress { get; set; } + + public FungibleAssetValue RedelegatingShare + { + get => _redelegatingShare; + set + { + if (!value.Currency.Equals(Asset.Share)) + { + throw new Exception.InvalidCurrencyException(Asset.Share, value.Currency); + } + + _redelegatingShare = value; + } + } + + public FungibleAssetValue InitialConsensusToken { get; set; } + + public FungibleAssetValue UnbondingConsensusToken + { + get => _unbondingConsensusToken; + set + { + if (!value.Currency.Equals(Asset.ConsensusToken)) + { + throw new Exception.InvalidCurrencyException(Asset.ConsensusToken, value.Currency); + } + + _unbondingConsensusToken = value; + } + } + + public FungibleAssetValue IssuedShare + { + get => _issuedShare; + set + { + if (!value.Currency.Equals(Asset.Share)) + { + throw new Exception.InvalidCurrencyException(Asset.Share, value.Currency); + } + + _issuedShare = value; + } + } + + public long Index { get; set; } + + public long CreationHeight { get; set; } + + public long CompletionBlockHeight { get; set; } + + public static bool operator ==(RedelegationEntry obj, RedelegationEntry other) + { + return obj.Equals(other); + } + + public static bool operator !=(RedelegationEntry obj, RedelegationEntry other) + { + return !(obj == other); + } + + public static Address DeriveAddress(Address redelegationAddress, long index) + { + return AddressHelper.Derive(redelegationAddress, $"RedelegationEntry{index}"); + } + + public bool IsMatured(long blockHeight) => blockHeight >= CompletionBlockHeight; + + public IValue Serialize() + { + return List.Empty + .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(CreationHeight.Serialize()); + } + + public override bool Equals(object? obj) + { + return Equals(obj as RedelegationEntry); + } + + public bool Equals(RedelegationEntry? other) + { + return !(other is null) && + 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 && + CreationHeight == other.CreationHeight; + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(Address.ToByteArray()); + } + } +} diff --git a/Lib9c/Action/DPoS/Model/UnbondingSet.cs b/Lib9c/Action/DPoS/Model/UnbondingSet.cs new file mode 100644 index 0000000000..a8f9b1542b --- /dev/null +++ b/Lib9c/Action/DPoS/Model/UnbondingSet.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using Bencodex.Types; +using Libplanet.Crypto; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Util; + +namespace Nekoyume.Action.DPoS.Model +{ + public class UnbondingSet + { + public UnbondingSet() + { + ValidatorAddressSet = new SortedSet
(); + UndelegationAddressSet = new SortedSet
(); + RedelegationAddressSet = new SortedSet
(); + } + + public UnbondingSet(IValue serialized) + { + List serializedList = (List)serialized; + ValidatorAddressSet + = new SortedSet
( + ((List)serializedList[0]).Select(x => x.ToAddress())); + UndelegationAddressSet + = new SortedSet
( + ((List)serializedList[1]).Select(x => x.ToAddress())); + RedelegationAddressSet + = new SortedSet
( + ((List)serializedList[2]).Select(x => x.ToAddress())); + } + + public UnbondingSet(UnbondingSet unbondingSet) + { + ValidatorAddressSet = unbondingSet.ValidatorAddressSet; + UndelegationAddressSet = unbondingSet.UndelegationAddressSet; + RedelegationAddressSet = unbondingSet.RedelegationAddressSet; + } + + public static long Period => 50400 * 4; + + public SortedSet
ValidatorAddressSet { get; set; } + + public SortedSet
UndelegationAddressSet { get; set; } + + public SortedSet
RedelegationAddressSet { get; set; } + + public Address Address => ReservedAddress.UnbondingSet; + + public IValue Serialize() + { + return List.Empty + .Add(new List(ValidatorAddressSet.Select( + address => address.Serialize()))) + .Add(new List(UndelegationAddressSet.Select( + address => address.Serialize()))) + .Add(new List(RedelegationAddressSet.Select( + address => address.Serialize()))); + } + } +} diff --git a/Lib9c/Action/DPoS/Model/Undelegation.cs b/Lib9c/Action/DPoS/Model/Undelegation.cs new file mode 100644 index 0000000000..6313ff1c4e --- /dev/null +++ b/Lib9c/Action/DPoS/Model/Undelegation.cs @@ -0,0 +1,131 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using Bencodex.Types; +using Libplanet.Common; +using Libplanet.Crypto; +using Nekoyume.Action.DPoS.Util; + +namespace Nekoyume.Action.DPoS.Model +{ + public class Undelegation : IEquatable + { + public Undelegation(Address delegatorAddress, Address validatorAddress) + { + Address = DeriveAddress(delegatorAddress, validatorAddress); + DelegatorAddress = delegatorAddress; + ValidatorAddress = validatorAddress; + UndelegationEntryIndex = 0; + UndelegationEntryAddresses = new SortedList(); + } + + public Undelegation(IValue serialized) + { + List serializedList = (List)serialized; + Address = serializedList[0].ToAddress(); + DelegatorAddress = serializedList[1].ToAddress(); + ValidatorAddress = serializedList[2].ToAddress(); + UndelegationEntryIndex = serializedList[3].ToLong(); + UndelegationEntryAddresses = new SortedList(); + foreach ( + IValue serializedUndelegationEntryAddress + in (List)serializedList[4]) + { + List items = (List)serializedUndelegationEntryAddress; + UndelegationEntryAddresses.Add(items[0].ToLong(), items[1].ToAddress()); + } + } + + public Undelegation(Undelegation undelegation) + { + Address = undelegation.Address; + DelegatorAddress = undelegation.DelegatorAddress; + ValidatorAddress = undelegation.ValidatorAddress; + UndelegationEntryIndex = undelegation.UndelegationEntryIndex; + UndelegationEntryAddresses = undelegation.UndelegationEntryAddresses; + } + + // TODO: Better structure + // This hard coding will cause some problems when it's modified + // May be it would be better to be serialized + public static int MaximumUndelegationEntries { get => 10; } + + public Address Address { get; } + + public Address DelegatorAddress { get; } + + public Address ValidatorAddress { get; } + + public Address DelegationAddress + { + get => Delegation.DeriveAddress(DelegatorAddress, ValidatorAddress); + } + + public long UndelegationEntryIndex { get; set; } + + public SortedList UndelegationEntryAddresses { get; set; } + + public static bool operator ==(Undelegation obj, Undelegation other) + { + return obj.Equals(other); + } + + public static bool operator !=(Undelegation obj, Undelegation other) + { + return !(obj == other); + } + + public static Address DeriveAddress(Address delegatorAddress, Address validatorAddress) + { + return AddressHelper.Derive(AddressHelper.Derive(delegatorAddress, validatorAddress.ToByteArray()), "Undelegation"); + } + + public IValue Serialize() + { + List serializedUndelegationEntryAddresses = List.Empty; +#pragma warning disable LAA1002 + foreach ( + KeyValuePair undelegationEntryAddressKV + in UndelegationEntryAddresses) + { + serializedUndelegationEntryAddresses = + serializedUndelegationEntryAddresses.Add( + List.Empty + .Add(undelegationEntryAddressKV.Key.Serialize()) + .Add(undelegationEntryAddressKV.Value.Serialize())); + } +#pragma warning restore LAA1002 + + return List.Empty + .Add(Address.Serialize()) + .Add(DelegatorAddress.Serialize()) + .Add(ValidatorAddress.Serialize()) + .Add(UndelegationEntryIndex.Serialize()) + .Add(serializedUndelegationEntryAddresses); + } + + public override bool Equals(object? obj) + { + return Equals(obj as Undelegation); + } + + public bool Equals(Undelegation? other) + { + return !(other is null) && + Address.Equals(other.Address) && + DelegatorAddress.Equals(other.DelegatorAddress) && + ValidatorAddress.Equals(other.ValidatorAddress) && + DelegationAddress.Equals(other.DelegationAddress) && + UndelegationEntryIndex == other.UndelegationEntryIndex && +#pragma warning disable LAA1002 + UndelegationEntryAddresses.SequenceEqual(other.UndelegationEntryAddresses); +#pragma warning restore LAA1002 + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(Address.ToByteArray()); + } + } +} diff --git a/Lib9c/Action/DPoS/Model/UndelegationEntry.cs b/Lib9c/Action/DPoS/Model/UndelegationEntry.cs new file mode 100644 index 0000000000..2130e14942 --- /dev/null +++ b/Lib9c/Action/DPoS/Model/UndelegationEntry.cs @@ -0,0 +1,120 @@ +#nullable enable +using System; +using Bencodex.Types; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Util; + +namespace Nekoyume.Action.DPoS.Model +{ + public class UndelegationEntry : IEquatable + { + private FungibleAssetValue _unbondingConsensusToken; + + public UndelegationEntry( + Address undelegationAddress, + FungibleAssetValue unbondingConsensusToken, + long index, + long blockHeight) + { + Address = DeriveAddress(undelegationAddress, index); + UndelegationAddress = undelegationAddress; + InitialConsensusToken = unbondingConsensusToken; + UnbondingConsensusToken = unbondingConsensusToken; + Index = index; + CompletionBlockHeight = blockHeight + UnbondingSet.Period; + CreationHeight = blockHeight; + } + + public UndelegationEntry(IValue serialized) + { + List serializedList = (List)serialized; + Address = serializedList[0].ToAddress(); + UndelegationAddress = serializedList[1].ToAddress(); + 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; + set + { + if (!value.Currency.Equals(Asset.ConsensusToken)) + { + throw new Exception.InvalidCurrencyException(Asset.ConsensusToken, value.Currency); + } + + _unbondingConsensusToken = value; + } + } + + public long Index { get; set; } + + public long CreationHeight { get; set; } + + public long CompletionBlockHeight { get; set; } + + public static bool operator ==(UndelegationEntry obj, UndelegationEntry other) + { + return obj.Equals(other); + } + + public static bool operator !=(UndelegationEntry obj, UndelegationEntry other) + { + return !(obj == other); + } + + public static Address DeriveAddress(Address undelegationAddress, long index) + { + return AddressHelper.Derive(undelegationAddress, $"UndelegationEntry{index}"); + } + + public bool IsMatured(long blockHeight) => blockHeight >= CompletionBlockHeight; + + 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(CreationHeight.Serialize()); + } + + public override bool Equals(object? obj) + { + return Equals(obj as UndelegationEntry); + } + + 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 && + CreationHeight == other.CreationHeight; + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(Address.ToByteArray()); + } + } +} diff --git a/Lib9c/Action/DPoS/Model/Validator.cs b/Lib9c/Action/DPoS/Model/Validator.cs new file mode 100644 index 0000000000..870fbaac4b --- /dev/null +++ b/Lib9c/Action/DPoS/Model/Validator.cs @@ -0,0 +1,135 @@ +#nullable enable +using System; +using System.Numerics; +using Bencodex.Types; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Util; + +namespace Nekoyume.Action.DPoS.Model +{ + public class Validator : IEquatable + { + private FungibleAssetValue _delegatorShares; + + public Validator( + Address operatorAddress, + PublicKey operatorPublicKey) + { + Address = DeriveAddress(operatorAddress); + OperatorAddress = operatorAddress; + OperatorPublicKey = operatorPublicKey; + Jailed = false; + Status = BondingStatus.Unbonded; + UnbondingCompletionBlockHeight = -1; + DelegatorShares = Asset.Share * 0; + } + + public Validator(IValue serialized) + { + var dict = (Bencodex.Types.Dictionary)serialized; + Address = dict["addr"].ToAddress(); + OperatorAddress = dict["op_addr"].ToAddress(); + OperatorPublicKey = dict["op_pub"].ToPublicKey(); + Jailed = dict["jailed"].ToBoolean(); + Status = dict["status"].ToEnum(); + UnbondingCompletionBlockHeight = dict["unbonding"].ToLong(); + DelegatorShares = dict["shares"].ToFungibleAssetValue(); + } + + // TODO: Better structure + // This hard coding will cause some problems when it's modified + // May be it would be better to be serialized + public static FungibleAssetValue MinSelfDelegation => Asset.ConsensusToken * 100; + + public static BigInteger CommissionNumerator => 1; + + public static BigInteger CommissionDenominator => 10; + + public static double CommissionMaxRate => 0.2; + + public static double CommissionMaxChangeRate => 0.01; + + public Address Address { get; set; } + + public Address OperatorAddress { get; set; } + + public PublicKey OperatorPublicKey { get; set; } + + public bool Jailed { get; set; } + + public BondingStatus Status { get; set; } + + public long UnbondingCompletionBlockHeight { get; set; } + + public FungibleAssetValue DelegatorShares + { + get => _delegatorShares; + set + { + if (!value.Currency.Equals(Asset.Share)) + { + throw new Exception.InvalidCurrencyException(Asset.Share, value.Currency); + } + + _delegatorShares = value; + } + } + + public static bool operator ==(Validator obj, Validator other) + { + return obj.Equals(other); + } + + public static bool operator !=(Validator obj, Validator other) + { + return !(obj == other); + } + + public static Address DeriveAddress(Address operatorAddress) + { + return AddressHelper.Derive(operatorAddress, "ValidatorAddress"); + } + + public bool IsMatured(long blockHeight) + => UnbondingCompletionBlockHeight > 0 + && Status != BondingStatus.Bonded + && blockHeight >= UnbondingCompletionBlockHeight; + + public IValue Serialize() + { + return Dictionary.Empty + .Add("addr", Address.Serialize()) + .Add("op_addr", OperatorAddress.Serialize()) + .Add("op_pub", OperatorPublicKey.Serialize()) + .Add("jailed", Jailed.Serialize()) + .Add("status", Status.Serialize()) + .Add("unbonding", UnbondingCompletionBlockHeight.Serialize()) + .Add("shares", DelegatorShares.Serialize()); + } + + public override bool Equals(object? obj) + { + return Equals(obj as Validator); + } + + public bool Equals(Validator? other) + { + return !(other is null) && + Address.Equals(other.Address) && + OperatorAddress.Equals(other.OperatorAddress) && + OperatorPublicKey.Equals(other.OperatorPublicKey) && + Jailed == other.Jailed && + Status == other.Status && + UnbondingCompletionBlockHeight == other.UnbondingCompletionBlockHeight && + DelegatorShares.Equals(other.DelegatorShares); + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(Address.ToByteArray()); + } + } +} diff --git a/Lib9c/Action/DPoS/Model/ValidatorDelegationSet.cs b/Lib9c/Action/DPoS/Model/ValidatorDelegationSet.cs new file mode 100644 index 0000000000..7ddf2f6c12 --- /dev/null +++ b/Lib9c/Action/DPoS/Model/ValidatorDelegationSet.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Bencodex.Types; +using Libplanet.Crypto; +using Nekoyume.Action.DPoS.Util; + +namespace Nekoyume.Action.DPoS.Model +{ + public class ValidatorDelegationSet + { + private readonly SortedSet
_set; + + public ValidatorDelegationSet(Address validatorAddress) + { + Address = DeriveAddress(validatorAddress); + ValidatorAddress = validatorAddress; + _set = new SortedSet
(); + } + + public ValidatorDelegationSet(IValue serialized) + { + var dict = (Dictionary)serialized; + Address = dict["addr"].ToAddress(); + ValidatorAddress = dict["val_addr"].ToAddress(); + _set = new SortedSet
(((List)dict["set"]).Select(x => x.ToAddress())); + } + + public ValidatorDelegationSet(ValidatorDelegationSet bondedValidatorSet) + { + Address = bondedValidatorSet.Address; + ValidatorAddress = bondedValidatorSet.ValidatorAddress; + _set = bondedValidatorSet._set; + } + + public Address Address { get; } + + public Address ValidatorAddress { get; } + + public ImmutableSortedSet
Set => _set.ToImmutableSortedSet(); + + public static Address DeriveAddress(Address validatorAddress) + { + return AddressHelper.Derive(validatorAddress, "ValidatorDelegationSetAddress"); + } + + public void Add(Address delegationAddress) + { + _set.Add(delegationAddress); + } + + public void Remove(Address delegationAddress) + { + _set.Remove(delegationAddress); + } + + public IValue Serialize() + { + return Dictionary.Empty + .Add("addr", Address.Serialize()) + .Add("val_addr", ValidatorAddress.Serialize()) + .Add("set", new List(Set.Select(x => x.Serialize()))); + } + } +} diff --git a/Lib9c/Action/DPoS/Model/ValidatorPower.cs b/Lib9c/Action/DPoS/Model/ValidatorPower.cs new file mode 100644 index 0000000000..8440b7f3d3 --- /dev/null +++ b/Lib9c/Action/DPoS/Model/ValidatorPower.cs @@ -0,0 +1,117 @@ +#nullable enable +using System; +using Bencodex.Types; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Util; + +#pragma warning disable S1210 +namespace Nekoyume.Action.DPoS.Model +{ + public class ValidatorPower + : IEquatable, IComparable, IComparable + { + private FungibleAssetValue _consensusToken; + + public ValidatorPower( + Address validatorAddress, + PublicKey operatorPublicKey, + FungibleAssetValue consensusToken) + { + ValidatorAddress = validatorAddress; + OperatorPublicKey = operatorPublicKey; + ConsensusToken = consensusToken; + } + + public ValidatorPower(IValue serialized) + { + List serializedList = (List)serialized; + ValidatorAddress = serializedList[0].ToAddress(); + OperatorPublicKey = serializedList[1].ToPublicKey(); + ConsensusToken = serializedList[2].ToFungibleAssetValue(); + } + + public Address ValidatorAddress { get; set; } + + public PublicKey OperatorPublicKey { get; set; } + + public FungibleAssetValue ConsensusToken + { + get => _consensusToken; + set + { + if (!value.Currency.Equals(Asset.ConsensusToken)) + { + throw new Exception.InvalidCurrencyException(Asset.ConsensusToken, value.Currency); + } + + _consensusToken = value; + } + } + + public static bool operator ==(ValidatorPower obj, ValidatorPower other) + { + return obj.Equals(other); + } + + public static bool operator !=(ValidatorPower obj, ValidatorPower other) + { + return !(obj == other); + } + + public IValue Serialize() => List.Empty + .Add(ValidatorAddress.Serialize()) + .Add(OperatorPublicKey.Serialize()) + .Add(ConsensusToken.Serialize()); + + public override bool Equals(object? obj) + { + return Equals(obj as ValidatorPower); + } + + public bool Equals(ValidatorPower? other) + { + return !(other is null) && + _consensusToken.Equals(other._consensusToken) && + ValidatorAddress.Equals(other.ValidatorAddress) && + OperatorPublicKey.Equals(other.OperatorPublicKey) && + ConsensusToken.Equals(other.ConsensusToken); + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(ValidatorAddress.ToByteArray()); + } + + int IComparable.CompareTo(ValidatorPower? other) + { + if (other is not { }) + { + return -1; + } + int result + = ConsensusToken.Equals(other.ConsensusToken) + ? ((IComparable
)ValidatorAddress).CompareTo(other.ValidatorAddress) + : ConsensusToken.CompareTo(other.ConsensusToken); + + return -result; + } + + int IComparable.CompareTo(object? obj) + { + if (obj is ValidatorPower other) + { + return ((IComparable)this).CompareTo(other); + } + + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + + throw new ArgumentException(nameof(obj)); + } + } +} diff --git a/Lib9c/Action/DPoS/Model/ValidatorPowerIndex.cs b/Lib9c/Action/DPoS/Model/ValidatorPowerIndex.cs new file mode 100644 index 0000000000..cffd2b2e71 --- /dev/null +++ b/Lib9c/Action/DPoS/Model/ValidatorPowerIndex.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.Linq; +using Bencodex.Types; +using Libplanet.Crypto; +using Nekoyume.Action.DPoS.Misc; + +namespace Nekoyume.Action.DPoS.Model +{ + public class ValidatorPowerIndex + { + public ValidatorPowerIndex() + { + Index = new SortedSet(); + } + + public ValidatorPowerIndex(IValue serialized) + { + IEnumerable items + = ((List)serialized).Select(item => new ValidatorPower(item)); + Index = new SortedSet(items); + } + + public ValidatorPowerIndex(ValidatorPowerIndex consensusPowerIndexInfo) + { + Index = consensusPowerIndexInfo.Index; + } + + public SortedSet Index { get; set; } + + public Address Address => ReservedAddress.ValidatorPowerIndex; + +#pragma warning disable S2365 + public List
ValidatorAddresses + => Index.Select(key => key.ValidatorAddress).ToList(); +#pragma warning restore S2365 + + public IValue Serialize() + => new List(Index.Select(consensusPowerKey => consensusPowerKey.Serialize())); + } +} diff --git a/Lib9c/Action/DPoS/Model/ValidatorRewards.cs b/Lib9c/Action/DPoS/Model/ValidatorRewards.cs new file mode 100644 index 0000000000..5990a44b57 --- /dev/null +++ b/Lib9c/Action/DPoS/Model/ValidatorRewards.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Action.DPoS.Util; + +namespace Nekoyume.Action.DPoS.Model +{ + public class ValidatorRewards + { + private readonly SortedList _rewards; + + public ValidatorRewards(Address validatorAddress, Currency currency) + { + Address = DeriveAddress(validatorAddress, currency); + ValidatorAddress = validatorAddress; + Currency = currency; + _rewards = new SortedList(); + } + + public ValidatorRewards(IValue serialized) + { + var dict = (Dictionary)serialized; + Address = dict["addr"].ToAddress(); + ValidatorAddress = dict["val_addr"].ToAddress(); + Currency = dict["currency"].ToCurrency(); + _rewards = new SortedList(); + foreach ( + KeyValuePair kv + in (Dictionary)dict["rewards"]) + { + _rewards.Add(kv.Key.ToLong(), kv.Value.ToFungibleAssetValue()); + } + } + + public ValidatorRewards(ValidatorRewards validatorRewards) + { + Address = validatorRewards.Address; + ValidatorAddress = validatorRewards.ValidatorAddress; + Currency = validatorRewards.Currency; + _rewards = validatorRewards._rewards; + } + + public Address Address { get; } + + public Address ValidatorAddress { get; } + + public Currency Currency { get; } + + public ImmutableSortedDictionary Rewards + => _rewards.ToImmutableSortedDictionary(); + + public static Address DeriveAddress(Address validatorAddress, Currency currency) + { + return AddressHelper.Derive(AddressHelper.Derive(validatorAddress, "ValidatorRewardsAddress"), currency.Ticker); + } + + public void Add(long blockHeight, FungibleAssetValue reward) + { + if (!reward.Currency.Equals(Currency)) + { + throw new Exception.InvalidCurrencyException(Currency, reward.Currency); + } + + _rewards.Add(blockHeight, reward); + } + + public IValue Serialize() + { + Dictionary serializedRewards = Dictionary.Empty; + foreach ( + KeyValuePair rewards in Rewards) + { + serializedRewards + = (Dictionary)serializedRewards + .Add((IKey)rewards.Key.Serialize(), rewards.Value.Serialize()); + } + + return Dictionary.Empty + .Add("addr", Address.Serialize()) + .Add("val_addr", ValidatorAddress.Serialize()) + .Add("currency", Currency.Serialize()) + .Add("rewards", serializedRewards); + } + } +} diff --git a/Lib9c/Action/DPoS/Model/ValidatorSet.cs b/Lib9c/Action/DPoS/Model/ValidatorSet.cs new file mode 100644 index 0000000000..18913a76bc --- /dev/null +++ b/Lib9c/Action/DPoS/Model/ValidatorSet.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Action.DPoS.Misc; + +namespace Nekoyume.Action.DPoS.Model +{ + public class ValidatorSet + { + private readonly SortedSet _set; + + public ValidatorSet() + { + _set = new SortedSet(); + } + + public ValidatorSet(IValue serialized) + { + IEnumerable validatorPowerEnum + = ((List)serialized).Select(x => new ValidatorPower(x)); + _set = new SortedSet(validatorPowerEnum); + } + + public ValidatorSet(ValidatorSet bondedValidatorSet) + { + _set = bondedValidatorSet._set; + } + + public static int MaxBondedSetSize => 100; + + public static Address PreviousBondedAddress => ReservedAddress.PreviousBondedValidatorSet; + + public static Address BondedAddress => ReservedAddress.BondedValidatorSet; + + public static Address UnbondedAddress => ReservedAddress.UnbondedValidatorSet; + + public long Count => _set.Count; + + public ImmutableSortedSet Set => _set.ToImmutableSortedSet(); + + public FungibleAssetValue TotalConsensusToken + => Set.Aggregate( + Asset.ConsensusToken * 0, (total, next) => total + next.ConsensusToken); + + public ValidatorPower this[int index] => _set.ElementAt(index); + + public void Add(ValidatorPower validatorPower) + { + _set.Add(validatorPower); + } + + public IValue Serialize() + { + return new List(_set.Select(x => x.Serialize())); + } + } +} 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/PromoteValidator.cs b/Lib9c/Action/DPoS/PromoteValidator.cs new file mode 100644 index 0000000000..3311e65032 --- /dev/null +++ b/Lib9c/Action/DPoS/PromoteValidator.cs @@ -0,0 +1,91 @@ +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.Exception; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Util; + +namespace Nekoyume.Action.DPoS +{ + /// + /// A system action for DPoS that promotes non-validator node to a validator. + /// + [ActionType(ActionTypeValue)] + public sealed class PromoteValidator : ActionBase + { + private const string ActionTypeValue = "promote_validator"; + + /// + /// Create a new instance of action. + /// + /// The of the target + /// to promote validator. + /// The amount of the asset to be initialize delegation. + public PromoteValidator(PublicKey validator, FungibleAssetValue amount) + { + Validator = validator; + Amount = amount; + } + + public PromoteValidator() + { + // Used only for deserialization. See also class Libplanet.Action.Sys.Registry. + // FIXME: do not fill ambiguous validator field. + // Suggestion: https://gist.github.com/riemannulus/7405e0d361364c6afa0ab433905ae81c + Validator = new PrivateKey().PublicKey; + } + + /// + /// The of the target promoting to a validator. + /// + public PublicKey Validator { get; set; } + + /// + /// The amount of the asset to be initially delegated. + /// + public FungibleAssetValue Amount { get; set; } + + /// + public override IValue PlainValue => Bencodex.Types.Dictionary.Empty + .Add("type_id", new Text(ActionTypeValue)) + .Add("validator", Validator.Serialize()) + .Add("amount", Amount.Serialize()); + + /// + public override void LoadPlainValue(IValue plainValue) + { + var dict = (Bencodex.Types.Dictionary)plainValue; + Validator = dict["validator"].ToPublicKey(); + Amount = dict["amount"].ToFungibleAssetValue(); + } + + /// + public override IWorld Execute(IActionContext context) + { + context.UseGas(1); + IActionContext ctx = context; + if (!ctx.Signer.Equals(Validator.Address)) + { + throw new PublicKeyAddressMatchingException(ctx.Signer, Validator); + } + + var states = ctx.PreviousState; + var nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + + states = ValidatorCtrl.Create( + states, + ctx, + ctx.Signer, + Validator, + Amount, + nativeTokens); + + return states; + } + } +} diff --git a/Lib9c/Action/DPoS/Redelegate.cs b/Lib9c/Action/DPoS/Redelegate.cs new file mode 100644 index 0000000000..08aa4d6945 --- /dev/null +++ b/Lib9c/Action/DPoS/Redelegate.cs @@ -0,0 +1,94 @@ +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 specified + /// of shared tokens to from . + /// + [ActionType(ActionTypeValue)] + public sealed class Redelegate : ActionBase + { + private const string ActionTypeValue = "redelegate"; + + /// + /// Creates a new instance of action. + /// + /// The of the validator that + /// delegated previously. + /// The of the validator + /// to be newly delegated. + /// The amount of the shared asset to be re-delegated. + public Redelegate(Address src, Address dst, FungibleAssetValue amount) + { + SrcValidator = src; + DstValidator = dst; + ShareAmount = amount; + } + + public Redelegate() + { + // Used only for deserialization. See also class Libplanet.Action.Sys.Registry. + } + + /// + /// The of the validator that was previously delegated to. + /// + public Address SrcValidator { get; set; } + + /// + /// The of the validator as a destination of moved voting power. + /// + public Address DstValidator { get; set; } + + /// + /// The amount of the shared token to move delegation. + /// + public FungibleAssetValue ShareAmount { get; set; } + + /// + public override IValue PlainValue => Bencodex.Types.Dictionary.Empty + .Add("type_id", new Text(ActionTypeValue)) + .Add("src", SrcValidator.Serialize()) + .Add("dst", DstValidator.Serialize()) + .Add("amount", ShareAmount.Serialize()); + + /// + public override void LoadPlainValue(IValue plainValue) + { + var dict = (Bencodex.Types.Dictionary)plainValue; + SrcValidator = dict["src"].ToAddress(); + DstValidator = dict["dst"].ToAddress(); + ShareAmount = dict["amount"].ToFungibleAssetValue(); + } + + /// + public override IWorld Execute(IActionContext context) + { + context.UseGas(1); + IActionContext ctx = context; + var states = ctx.PreviousState; + var nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + + states = RedelegateCtrl.Execute( + states, + ctx, + ctx.Signer, + SrcValidator, + DstValidator, + ShareAmount, + nativeTokens); + + return states; + } + } +} diff --git a/Lib9c/Action/DPoS/Sys/AllocateReward.cs b/Lib9c/Action/DPoS/Sys/AllocateReward.cs new file mode 100644 index 0000000000..82c74d6751 --- /dev/null +++ b/Lib9c/Action/DPoS/Sys/AllocateReward.cs @@ -0,0 +1,51 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Nekoyume.Action.DPoS.Control; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Model; + +namespace Nekoyume.Action.DPoS.Sys +{ + /// + /// An action for allocate reward to validators and delegators in previous block. + /// Should be executed at the beginning of the block. + /// + public sealed class AllocateReward : ActionBase + { + /// + /// Creates a new instance of . + /// + public AllocateReward() + { + } + + /// + public override IValue PlainValue => new Bencodex.Types.Boolean(true); + + /// + public override void LoadPlainValue(IValue plainValue) + { + // Method intentionally left empty. + } + + /// + public override IWorld Execute(IActionContext context) + { + var states = context.PreviousState; + var nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + var previousProposerInfo = + new ProposerInfo(states.GetDPoSState(ReservedAddress.ProposerInfo)); + states = AllocateRewardCtrl.Execute( + states, + context, + nativeTokens, + context.LastCommit?.Votes, + previousProposerInfo); + + return states; + } + } +} diff --git a/Lib9c/Action/DPoS/Sys/Mortgage.cs b/Lib9c/Action/DPoS/Sys/Mortgage.cs new file mode 100644 index 0000000000..f52580397a --- /dev/null +++ b/Lib9c/Action/DPoS/Sys/Mortgage.cs @@ -0,0 +1,57 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Types.Assets; +using Nekoyume.Module; + +namespace Nekoyume.Action.DPoS.Sys +{ + /// + /// An action for mortgage gas fee for a transaction. + /// Should be executed at the beginning of the tx. + /// + public sealed class Mortgage : ActionBase + { + /// + /// Creates a new instance of . + /// + public Mortgage() + { + } + + /// + public override IValue PlainValue => new Bencodex.Types.Boolean(true); + + /// + public override void LoadPlainValue(IValue plainValue) + { + // Method intentionally left empty. + } + + /// + public override IWorld Execute(IActionContext context) + { + var state = context.PreviousState; + if (context.MaxGasPrice is not { } realGasPrice) + { + return state; + } + + var balance = state.GetBalance(context.Signer, realGasPrice.Currency); + if (balance < realGasPrice * context.GasLimit()) + { + var msg = + $"The account {context.Signer}'s balance of {realGasPrice.Currency} is " + + "insufficient to pay gas fee: " + + $"{balance} < {realGasPrice * context.GasLimit()}."; + throw new InsufficientBalanceException(msg, context.Signer, balance); + } + + return state.BurnAsset( + context, + context.Signer, + realGasPrice * context.GasLimit()); + } + } +} diff --git a/Lib9c/Action/DPoS/Sys/RecordProposer.cs b/Lib9c/Action/DPoS/Sys/RecordProposer.cs new file mode 100644 index 0000000000..1f31206923 --- /dev/null +++ b/Lib9c/Action/DPoS/Sys/RecordProposer.cs @@ -0,0 +1,38 @@ +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Model; + +namespace Nekoyume.Action.DPoS.Sys +{ + /// + /// An action for recording proposer of the block to use in next block's reward distribution. + /// + public sealed class RecordProposer : ActionBase + { + /// + /// Creates a new instance of . + /// + public RecordProposer() + { + } + + /// + public override IValue PlainValue => new Bencodex.Types.Boolean(true); + + /// + public override void LoadPlainValue(IValue plainValue) + { + // Method intentionally left empty. + } + + /// + public override IWorld Execute(IActionContext context) + { + return context.PreviousState.SetDPoSState( + ReservedAddress.ProposerInfo, + new ProposerInfo(context.BlockIndex, context.Miner).Bencoded); + } + } +} diff --git a/Lib9c/Action/DPoS/Sys/Refund.cs b/Lib9c/Action/DPoS/Sys/Refund.cs new file mode 100644 index 0000000000..cee0e58551 --- /dev/null +++ b/Lib9c/Action/DPoS/Sys/Refund.cs @@ -0,0 +1,51 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Nekoyume.Module; + +namespace Nekoyume.Action.DPoS.Sys +{ + /// + /// An action for refund gas fee for a transaction. + /// Should be executed at the beginning of the tx. + /// + public sealed class Refund : ActionBase + { + /// + /// Creates a new instance of . + /// + public Refund() + { + } + + /// + public override IValue PlainValue => new Bencodex.Types.Boolean(true); + + /// + public override void LoadPlainValue(IValue plainValue) + { + // Method intentionally left empty. + } + + /// + public override IWorld Execute(IActionContext context) + { + var world = context.PreviousState; + if (context.MaxGasPrice is not { Sign: > 0 } realGasPrice) + { + return world; + } + + if (context.GasLimit() - context.GasUsed() <= 0) + { + return world; + } + + return world.MintAsset( + context, + context.Signer, + (context.GasLimit() - context.GasUsed()) * realGasPrice); + } + } +} diff --git a/Lib9c/Action/DPoS/Sys/Reward.cs b/Lib9c/Action/DPoS/Sys/Reward.cs new file mode 100644 index 0000000000..6a86960531 --- /dev/null +++ b/Lib9c/Action/DPoS/Sys/Reward.cs @@ -0,0 +1,51 @@ +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Module; + +namespace Nekoyume.Action.DPoS.Sys +{ + /// + /// An action for reward for a transaction. + /// Should be executed at the beginning of the tx. + /// + public sealed class Reward : ActionBase + { + /// + /// Creates a new instance of . + /// + public Reward() + { + } + + /// + public override IValue PlainValue => new Bencodex.Types.Boolean(true); + + /// + public override void LoadPlainValue(IValue plainValue) + { + // Method intentionally left empty. + } + + /// + public override IWorld Execute(IActionContext context) + { + var world = context.PreviousState; + if (context.MaxGasPrice is not { Sign: > 0 } realGasPrice) + { + return world; + } + + if (context.GasUsed() <= 0) + { + return world; + } + + return world.MintAsset( + context, + ReservedAddress.RewardPool, + realGasPrice * context.GasUsed()); + } + } +} diff --git a/Lib9c/Action/DPoS/Sys/UpdateValidators.cs b/Lib9c/Action/DPoS/Sys/UpdateValidators.cs new file mode 100644 index 0000000000..410866ceb4 --- /dev/null +++ b/Lib9c/Action/DPoS/Sys/UpdateValidators.cs @@ -0,0 +1,51 @@ +using System.Linq; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Nekoyume.Action.DPoS.Control; +using Nekoyume.Action.DPoS.Model; +using Nekoyume.Module; + +namespace Nekoyume.Action.DPoS.Sys +{ + /// + /// An action for update validators. + /// Should be executed at the end of the block. + /// + public sealed class UpdateValidators : ActionBase + { + /// + /// Creates a new instance of . + /// + public UpdateValidators() + { + } + + /// + public override IValue PlainValue => new Bencodex.Types.Boolean(true); + + /// + public override void LoadPlainValue(IValue plainValue) + { + // Method intentionally left empty. + } + + /// + public override IWorld Execute(IActionContext context) + { + var states = context.PreviousState; + states = ValidatorSetCtrl.Update(states, context); + ValidatorSet bondedSet; + (states, bondedSet) = ValidatorSetCtrl.FetchBondedValidatorSet(states); + var validatorSet = new Libplanet.Types.Consensus.ValidatorSet( + bondedSet.Set.Select( + v => new Libplanet.Types.Consensus.Validator( + v.OperatorPublicKey, + v.ConsensusToken.RawValue)) + .ToList()); + states = states.SetValidatorSet(validatorSet); + + return states; + } + } +} diff --git a/Lib9c/Action/DPoS/Undelegate.cs b/Lib9c/Action/DPoS/Undelegate.cs new file mode 100644 index 0000000000..d8b51d78a6 --- /dev/null +++ b/Lib9c/Action/DPoS/Undelegate.cs @@ -0,0 +1,85 @@ +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 cancels specified + /// of shared tokens to a given . + /// + [ActionType(ActionTypeValue)] + public sealed class Undelegate : ActionBase + { + private const string ActionTypeValue = "undelegate"; + + /// + /// Creates a new instance of action. + /// + /// The of the validator + /// to undelegate tokens. + /// The amount of the asset to be undelegated. + public Undelegate(Address validator, FungibleAssetValue amount) + { + Validator = validator; + ShareAmount = amount; + } + + public Undelegate() + { + // Used only for deserialization. See also class Libplanet.Action.Sys.Registry. + } + + /// + /// The of the validator to cancel the . + /// + public Address Validator { get; set; } + + /// + /// The amount of the asset to be undelegated. + /// + public FungibleAssetValue ShareAmount { get; set; } + + /// + public override IValue PlainValue => Bencodex.Types.Dictionary.Empty + .Add("type_id", new Text(ActionTypeValue)) + .Add("validator", Validator.Serialize()) + .Add("amount", ShareAmount.Serialize()); + + /// + public override void LoadPlainValue(IValue plainValue) + { + var dict = (Bencodex.Types.Dictionary)plainValue; + Validator = dict["validator"].ToAddress(); + ShareAmount = dict["amount"].ToFungibleAssetValue(); + } + + /// + public override IWorld Execute(IActionContext context) + { + context.UseGas(1); + IActionContext ctx = context; + var states = ctx.PreviousState; + var nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + + // if (ctx.Rehearsal) + // Rehearsal mode is not implemented + states = UndelegateCtrl.Execute( + states, + ctx, + ctx.Signer, + Validator, + ShareAmount, + nativeTokens); + + return states; + } + } +} 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/AddressHelper.cs b/Lib9c/Action/DPoS/Util/AddressHelper.cs new file mode 100644 index 0000000000..8f4ff00bf4 --- /dev/null +++ b/Lib9c/Action/DPoS/Util/AddressHelper.cs @@ -0,0 +1,25 @@ +using System.Security.Cryptography; +using System.Text; +using Libplanet.Crypto; + +namespace Nekoyume.Action.DPoS.Util +{ + internal static class AddressHelper + { + public static Address Derive(this Address address, byte[] key) + { + var bytes = address.ToByteArray(); + byte[] hashed; + + using (var hmac = new HMACSHA1(key)) + { + hashed = hmac.ComputeHash(bytes); + } + + return new Address(hashed); + } + + public static Address Derive(this Address address, string key) + => address.Derive(Encoding.UTF8.GetBytes(key)); + } +} diff --git a/Lib9c/Action/DPoS/Util/DPoSModule.cs b/Lib9c/Action/DPoS/Util/DPoSModule.cs new file mode 100644 index 0000000000..3ec155ef75 --- /dev/null +++ b/Lib9c/Action/DPoS/Util/DPoSModule.cs @@ -0,0 +1,30 @@ +#nullable enable +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Nekoyume.Action.DPoS.Misc; + +namespace Nekoyume.Action.DPoS +{ + public static class DPoSModule + { + public static IValue? GetDPoSState(this IWorldState world, Address address) + { + return world.GetAccountState(ReservedAddress.DPoSAccountAddress).GetState(address); + } + + public static IWorld SetDPoSState(this IWorld world, Address address, IValue value) + { + var account = world.GetAccount(ReservedAddress.DPoSAccountAddress); + 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); + } + } +} diff --git a/Lib9c/Action/DPoS/Util/MarshalHelper.cs b/Lib9c/Action/DPoS/Util/MarshalHelper.cs new file mode 100644 index 0000000000..26700c32fe --- /dev/null +++ b/Lib9c/Action/DPoS/Util/MarshalHelper.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Immutable; +using System.Globalization; +using System.Numerics; +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Nekoyume.Action.DPoS.Util +{ + internal static class MarshalHelper + { + public static IValue Serialize(this Address address) => + new Binary(address.ByteArray); + + public static IValue Serialize(this PublicKey publicKey) => + new Binary(publicKey.Format(false)); + + public static IValue Serialize(this bool boolean) => + new Bencodex.Types.Boolean(boolean); + + public static IValue Serialize(this int number) => + (Text)number.ToString(CultureInfo.InvariantCulture); + + public static IValue Serialize(this long number) => + (Text)number.ToString(CultureInfo.InvariantCulture); + + public static IValue Serialize(this double number) => + (Text)number.ToString(CultureInfo.InvariantCulture); + + public static IValue Serialize(this BigInteger number) => + (Bencodex.Types.Integer)number; + + public static IValue Serialize(this Enum type) => (Text)type.ToString(); + + public static IValue Serialize(this Guid number) => + new Binary(number.ToByteArray()); + + public static IValue Serialize(this FungibleAssetValue fungibleAssetValue) + { + return Dictionary.Empty + .Add("currency", fungibleAssetValue.Currency.Serialize()) + .Add("majorUnit", fungibleAssetValue.MajorUnit.Serialize()) + .Add("minorUnit", fungibleAssetValue.MinorUnit.Serialize()); + } + + public static Address ToAddress(this IValue serialized) => + new Address((Binary)serialized); + + public static PublicKey ToPublicKey(this IValue serialized) => + new PublicKey(((Binary)serialized).ToImmutableArray()); + + public static bool ToBoolean(this IValue serialized) => + ((Bencodex.Types.Boolean)serialized).Value; + + public static int ToInteger(this IValue serialized) => + int.Parse(((Text)serialized).Value, CultureInfo.InvariantCulture); + + public static long ToLong(this IValue serialized) => + long.Parse(((Text)serialized).Value, CultureInfo.InvariantCulture); + + public static double ToDouble(this IValue serialized) => + double.Parse(((Text)serialized).Value, CultureInfo.InvariantCulture); + + public static BigInteger ToBigInteger(this IValue serialized) => + ((Integer)serialized).Value; + + public static T ToEnum(this IValue serialized) + where T : struct + { + return (T)Enum.Parse(typeof(T), (Text)serialized); + } + + public static Guid ToGuid(this IValue serialized) => + new Guid(((Binary)serialized).ToByteArray()); + + public static Currency ToCurrency(this IValue serialized) => + new Currency(serialized); + + public static FungibleAssetValue ToFungibleAssetValue(this IValue serialized) => + new FungibleAssetValue(serialized); + } +} diff --git a/Lib9c/Action/DPoS/WithdrawDelegator.cs b/Lib9c/Action/DPoS/WithdrawDelegator.cs new file mode 100644 index 0000000000..2215cf7f0f --- /dev/null +++ b/Lib9c/Action/DPoS/WithdrawDelegator.cs @@ -0,0 +1,90 @@ +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.Model; +using Nekoyume.Action.DPoS.Util; +using Nekoyume.Module; + +namespace Nekoyume.Action.DPoS +{ + /// + /// A system action for DPoS that withdraws reward tokens from given . + /// + [ActionType(ActionTypeValue)] + public sealed class WithdrawDelegator : ActionBase + { + private const string ActionTypeValue = "withdraw_delegator"; + + /// + /// Creates a new instance of action. + /// + /// The of the validator + /// from which to withdraw the tokens. + public WithdrawDelegator(Address validator) + { + Validator = validator; + } + + public WithdrawDelegator() + { + // Used only for deserialization. See also class Libplanet.Action.Sys.Registry. + } + + /// + /// The of the validator to withdraw. + /// + public Address Validator { 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) + { + Validator = plainValue.ToAddress(); + } + + /// + public override IWorld Execute(IActionContext context) + { + context.UseGas(1); + IActionContext ctx = context; + var states = ctx.PreviousState; + var nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + + // if (ctx.Rehearsal) + // Rehearsal mode is not implemented + states = DelegateCtrl.Distribute( + states, + ctx, + nativeTokens, + Delegation.DeriveAddress(ctx.Signer, Validator)); + +#pragma warning disable LAA1002 + foreach (Currency nativeToken in nativeTokens) + { + FungibleAssetValue reward = states.GetBalance( + AllocateRewardCtrl.RewardAddress(ctx.Signer), nativeToken); + if (reward.Sign > 0) + { + states = states.TransferAsset( + ctx, + AllocateRewardCtrl.RewardAddress(ctx.Signer), + ctx.Signer, + reward); + } + } +#pragma warning restore LAA1002 + + return states; + } + } +} diff --git a/Lib9c/Action/DPoS/WithdrawValidator.cs b/Lib9c/Action/DPoS/WithdrawValidator.cs new file mode 100644 index 0000000000..70563bfa9b --- /dev/null +++ b/Lib9c/Action/DPoS/WithdrawValidator.cs @@ -0,0 +1,68 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Types.Assets; +using Nekoyume.Action.DPoS.Control; +using Nekoyume.Action.DPoS.Misc; +using Nekoyume.Action.DPoS.Model; +using Nekoyume.Module; + +namespace Nekoyume.Action.DPoS +{ + /// + /// A system action for DPoS that withdraws commission tokens from . + /// + [ActionType(ActionTypeValue)] + public sealed class WithdrawValidator : ActionBase + { + private const string ActionTypeValue = "withdraw_validator"; + + /// + /// Creates a new instance of action. + /// + public WithdrawValidator() + { + } + + /// + public override IValue PlainValue => Bencodex.Types.Dictionary.Empty + .Add("type_id", new Text(ActionTypeValue)); + + /// + public override void LoadPlainValue(IValue plainValue) + { + // Method intentionally left empty. + } + + /// + public override IWorld Execute(IActionContext context) + { + context.UseGas(1); + IActionContext ctx = context; + var states = ctx.PreviousState; + var nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + + // if (ctx.Rehearsal) + // Rehearsal mode is not implemented +#pragma warning disable LAA1002 + foreach (Currency nativeToken in nativeTokens) + { + FungibleAssetValue reward = states.GetBalance( + AllocateRewardCtrl.RewardAddress(ctx.Signer), nativeToken); + if (reward.Sign > 0) + { + states = states.TransferAsset( + ctx, + AllocateRewardCtrl.RewardAddress(ctx.Signer), + ctx.Signer, + reward); + } + } +#pragma warning restore LAA1002 + + return states; + } + } +} diff --git a/Lib9c/Action/InitializeStates.cs b/Lib9c/Action/InitializeStates.cs index 31f6f359f5..6965c97ee1 100644 --- a/Lib9c/Action/InitializeStates.cs +++ b/Lib9c/Action/InitializeStates.cs @@ -9,6 +9,7 @@ using Nekoyume.Model.State; using Nekoyume.Module; using Libplanet.Crypto; +using Libplanet.Types.Assets; namespace Nekoyume.Action { @@ -57,6 +58,8 @@ public class InitializeStates : GameAction, IInitializeStatesV1 public ISet
AssetMinters { get; set; } + public Dictionary InitialFavs { get; set; } + Dictionary IInitializeStatesV1.Ranking => Ranking; Dictionary IInitializeStatesV1.Shop => Shop; Dictionary IInitializeStatesV1.TableSheets => TableSheets; @@ -87,7 +90,8 @@ public InitializeStates( AdminState adminAddressState = null, AuthorizedMinersState authorizedMinersState = null, CreditsState creditsState = null, - ISet
assetMinters = null) + ISet
assetMinters = null, + Dictionary initialFavs = null) { Ranking = (Dictionary)rankingState.Serialize(); Shop = (Dictionary)shopState.Serialize(); @@ -114,6 +118,17 @@ public InitializeStates( { AssetMinters = assetMinters; } + + if (!(initialFavs is null)) + { +#pragma warning disable LAA1002 + InitialFavs = new Dictionary( + initialFavs.Select( + kv => new KeyValuePair( + (Binary)kv.Key.Serialize(), + kv.Value.Serialize()))); +#pragma warning restore LAA1002 + } } public override IWorld Execute(IActionContext context) @@ -192,6 +207,17 @@ public override IWorld Execute(IActionContext context) ); } + if (InitialFavs is { }) + { + foreach (var (address, fav) in InitialFavs) + { + states = states.MintAsset( + ctx, + new Address(address), + new FungibleAssetValue(fav)); + } + } + return states; } @@ -235,6 +261,11 @@ protected override IImmutableDictionary PlainValueInternal rv = rv.Add("asset_minters", new List(AssetMinters.Select(addr => addr.Serialize()))); } + if (!(InitialFavs is null)) + { + rv = rv.Add("initial_favs", InitialFavs); + } + return rv; } } @@ -274,6 +305,11 @@ protected override void LoadPlainValueInternal(IImmutableDictionary addr.ToAddress()).ToHashSet(); } + + if (plainValue.TryGetValue("initial_favs", out IValue intialFavs)) + { + InitialFavs = (Dictionary)intialFavs; + } } } } diff --git a/Lib9c/Module/LegacyModule.cs b/Lib9c/Module/LegacyModule.cs index d4313b3577..a38ea53fbd 100644 --- a/Lib9c/Module/LegacyModule.cs +++ b/Lib9c/Module/LegacyModule.cs @@ -103,6 +103,14 @@ public static IWorld SetValidator( world.GetAccount(ReservedAddresses.LegacyAccount) .SetValidator(validator)); + public static IWorld SetValidatorSet( + this IWorld world, + ValidatorSet validatorSet) => + world.SetAccount( + ReservedAddresses.LegacyAccount, + world.GetAccount(ReservedAddresses.LegacyAccount) + .SetValidatorSet(validatorSet)); + // Methods from AccountExtensions public static IWorld MarkBalanceChanged( this IWorld world,