diff --git a/.Lib9c.Tests/Action/ActionContext.cs b/.Lib9c.Tests/Action/ActionContext.cs index 9c157693b4..1afdbc9f7e 100644 --- a/.Lib9c.Tests/Action/ActionContext.cs +++ b/.Lib9c.Tests/Action/ActionContext.cs @@ -28,6 +28,8 @@ public class ActionContext : IActionContext public int BlockProtocolVersion { get; set; } + public BlockCommit LastCommit { get; set; } + public IWorld PreviousState { get; set; } public int RandomSeed { get; set; } diff --git a/.Lib9c.Tests/Action/CombinationConsumableTest.cs b/.Lib9c.Tests/Action/CombinationConsumableTest.cs new file mode 100644 index 0000000000..44844b56f3 --- /dev/null +++ b/.Lib9c.Tests/Action/CombinationConsumableTest.cs @@ -0,0 +1,133 @@ +namespace Lib9c.Tests.Action +{ + using System.Globalization; + using System.Linq; + using Libplanet.Action; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Model; + using Nekoyume.Model.Item; + using Nekoyume.Model.Mail; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Xunit; + + public class CombinationConsumableTest + { + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + private readonly IRandom _random; + private readonly TableSheets _tableSheets; + private IWorld _initialState; + + public CombinationConsumableTest() + { + _agentAddress = new PrivateKey().Address; + _avatarAddress = _agentAddress.Derive("avatar"); + var slotAddress = _avatarAddress.Derive( + string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0 + ) + ); + var sheets = TableSheetsImporter.ImportSheets(); + _random = new TestRandom(); + _tableSheets = new TableSheets(sheets); + + var agentState = new AgentState(_agentAddress); + agentState.avatarAddresses[0] = _avatarAddress; + + var gameConfigState = new GameConfigState(); + + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 1, + _tableSheets.GetAvatarSheets(), + gameConfigState, + default + ); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + var gold = new GoldCurrencyState(Currency.Legacy("NCG", 2, null)); +#pragma warning restore CS0618 + + _initialState = new World(new MockWorldState()) + .SetAgentState(_agentAddress, agentState) + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState( + slotAddress, + new CombinationSlotState( + slotAddress, + GameConfig.RequireClearedStageLevel.CombinationConsumableAction).Serialize()) + .SetLegacyState(GameConfigState.Address, gold.Serialize()); + + foreach (var (key, value) in sheets) + { + _initialState = + _initialState.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + } + + [Fact] + public void Execute() + { + var avatarState = _initialState.GetAvatarState(_avatarAddress); + var row = _tableSheets.ConsumableItemRecipeSheet.Values.First(); + var costActionPoint = row.RequiredActionPoint; + foreach (var materialInfo in row.Materials) + { + var materialRow = _tableSheets.MaterialItemSheet[materialInfo.Id]; + var material = ItemFactory.CreateItem(materialRow, _random); + avatarState.inventory.AddItem(material, materialInfo.Count); + } + + var previousActionPoint = avatarState.actionPoint; + var previousResultConsumableCount = + avatarState.inventory.Equipments.Count(e => e.Id == row.ResultConsumableItemId); + var previousMailCount = avatarState.mailBox.Count; + + avatarState.worldInformation = new WorldInformation( + 0, + _tableSheets.WorldSheet, + GameConfig.RequireClearedStageLevel.CombinationConsumableAction); + + IWorld previousState = _initialState.SetAvatarState(_avatarAddress, avatarState); + + var action = new CombinationConsumable + { + avatarAddress = _avatarAddress, + recipeId = row.Id, + slotIndex = 0, + }; + + var nextState = action.Execute(new ActionContext + { + PreviousState = previousState, + Signer = _agentAddress, + BlockIndex = 1, + RandomSeed = _random.Seed, + }); + + var slotState = nextState.GetCombinationSlotState(_avatarAddress, 0); + Assert.NotNull(slotState.Result); + Assert.NotNull(slotState.Result.itemUsable); + + var consumable = (Consumable)slotState.Result.itemUsable; + Assert.NotNull(consumable); + + var nextAvatarState = nextState.GetAvatarState(_avatarAddress); + Assert.Equal(previousActionPoint - costActionPoint, nextAvatarState.actionPoint); + Assert.Equal(previousMailCount + 1, nextAvatarState.mailBox.Count); + Assert.IsType(nextAvatarState.mailBox.First()); + Assert.Equal( + previousResultConsumableCount + 1, + nextAvatarState.inventory.Consumables.Count(e => e.Id == row.ResultConsumableItemId)); + } + } +} diff --git a/.Lib9c.Tests/Action/CreateAvatarTest.cs b/.Lib9c.Tests/Action/CreateAvatarTest.cs new file mode 100644 index 0000000000..127a3d172e --- /dev/null +++ b/.Lib9c.Tests/Action/CreateAvatarTest.cs @@ -0,0 +1,280 @@ +namespace Lib9c.Tests.Action +{ + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Globalization; + using System.IO; + using System.Linq; + using System.Runtime.Serialization.Formatters.Binary; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Helper; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Nekoyume.TableData; + using Xunit; + using static Lib9c.SerializeKeys; + + public class CreateAvatarTest + { + private readonly Address _agentAddress; + private readonly TableSheets _tableSheets; + + public CreateAvatarTest() + { + _agentAddress = default; + _tableSheets = new TableSheets(TableSheetsImporter.ImportSheets()); + } + + [Theory] + [InlineData(0L)] + [InlineData(7_210_000L)] + [InlineData(7_210_001L)] + public void Execute(long blockIndex) + { + var action = new CreateAvatar() + { + index = 0, + hair = 0, + ear = 0, + lens = 0, + tail = 0, + name = "test", + }; + + var sheets = TableSheetsImporter.ImportSheets(); + var state = new World(new MockWorldState()) + .SetLegacyState( + Addresses.GameConfig, + new GameConfigState(sheets[nameof(GameConfigSheet)]).Serialize() + ); + + foreach (var (key, value) in sheets) + { + state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + Assert.Equal(0 * CrystalCalculator.CRYSTAL, state.GetBalance(_agentAddress, CrystalCalculator.CRYSTAL)); + + var nextState = action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + BlockIndex = blockIndex, + RandomSeed = 0, + }); + + var avatarAddress = _agentAddress.Derive( + string.Format( + CultureInfo.InvariantCulture, + CreateAvatar.DeriveFormat, + 0 + ) + ); + Assert.True(nextState.TryGetAvatarState( + default, + avatarAddress, + out var nextAvatarState) + ); + var agentState = nextState.GetAgentState(default); + Assert.NotNull(agentState); + Assert.True(agentState.avatarAddresses.Any()); + Assert.Equal("test", nextAvatarState.name); + Assert.Equal(200_000 * CrystalCalculator.CRYSTAL, nextState.GetBalance(_agentAddress, CrystalCalculator.CRYSTAL)); + var avatarItemSheet = nextState.GetSheet(); + foreach (var row in avatarItemSheet.Values) + { + Assert.True(nextAvatarState.inventory.HasItem(row.ItemId, row.Count)); + } + + var avatarFavSheet = nextState.GetSheet(); + foreach (var row in avatarFavSheet.Values) + { + var targetAddress = row.Target == CreateAvatarFavSheet.Target.Agent + ? _agentAddress + : avatarAddress; + Assert.Equal(row.Currency * row.Quantity, nextState.GetBalance(targetAddress, row.Currency)); + } + } + + [Theory] + [InlineData("홍길동")] + [InlineData("山田太郎")] + public void ExecuteThrowInvalidNamePatterException(string nickName) + { + var agentAddress = default(Address); + + var action = new CreateAvatar() + { + index = 0, + hair = 0, + ear = 0, + lens = 0, + tail = 0, + name = nickName, + }; + + var state = new World(new MockWorldState()); + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = agentAddress, + BlockIndex = 0, + }) + ); + } + + [Fact] + public void ExecuteThrowInvalidAddressException() + { + var avatarAddress = _agentAddress.Derive( + string.Format( + CultureInfo.InvariantCulture, + CreateAvatar.DeriveFormat, + 0 + ) + ); + + var avatarState = new AvatarState( + avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + new GameConfigState(), + default + ); + + var action = new CreateAvatar() + { + index = 0, + hair = 0, + ear = 0, + lens = 0, + tail = 0, + name = "test", + }; + + var state = new World(new MockWorldState()).SetAvatarState(avatarAddress, avatarState); + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + BlockIndex = 0, + }) + ); + } + + [Theory] + [InlineData(-1)] + [InlineData(3)] + public void ExecuteThrowAvatarIndexOutOfRangeException(int index) + { + var agentState = new AgentState(_agentAddress); + var state = new World(new MockWorldState()).SetAgentState(_agentAddress, agentState); + var action = new CreateAvatar() + { + index = index, + hair = 0, + ear = 0, + lens = 0, + tail = 0, + name = "test", + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + PreviousState = state, + Signer = _agentAddress, + BlockIndex = 0, + }) + ); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void ExecuteThrowAvatarIndexAlreadyUsedException(int index) + { + var agentState = new AgentState(_agentAddress); + var avatarAddress = _agentAddress.Derive( + string.Format( + CultureInfo.InvariantCulture, + CreateAvatar.DeriveFormat, + 0 + ) + ); + agentState.avatarAddresses[index] = avatarAddress; + var state = new World(new MockWorldState()).SetAgentState(_agentAddress, agentState); + + var action = new CreateAvatar() + { + index = index, + hair = 0, + ear = 0, + lens = 0, + tail = 0, + name = "test", + }; + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + BlockIndex = 0, + }) + ); + } + + [Fact] + public void AddItem() + { + var itemSheet = _tableSheets.ItemSheet; + var createAvatarItemSheet = new CreateAvatarItemSheet(); + createAvatarItemSheet.Set(@"item_id,count +10112000,2 +10512000,2 +600201,2 +"); + var avatarState = new AvatarState(default, default, 0L, _tableSheets.GetAvatarSheets(), new GameConfigState(), default, "test"); + CreateAvatar.AddItem(itemSheet, createAvatarItemSheet, avatarState, new TestRandom()); + foreach (var row in createAvatarItemSheet.Values) + { + Assert.True(avatarState.inventory.HasItem(row.ItemId, row.Count)); + } + + Assert.Equal(4, avatarState.inventory.Equipments.Count()); + foreach (var equipment in avatarState.inventory.Equipments) + { + var equipmentRow = _tableSheets.EquipmentItemSheet[equipment.Id]; + Assert.Equal(equipmentRow.Stat, equipment.Stat); + } + } + + [Fact] + public void MintAsset() + { + var createAvatarFavSheet = new CreateAvatarFavSheet(); + createAvatarFavSheet.Set(@"currency,quantity,target +CRYSTAL,200000,Agent +RUNE_GOLDENLEAF,200000,Avatar +"); + var avatarAddress = new PrivateKey().Address; + var agentAddress = new PrivateKey().Address; + var avatarState = new AvatarState(avatarAddress, agentAddress, 0L, _tableSheets.GetAvatarSheets(), new GameConfigState(), default, "test"); + var nextState = CreateAvatar.MintAsset(createAvatarFavSheet, avatarState, new World(new MockWorldState()), new ActionContext()); + foreach (var row in createAvatarFavSheet.Values) + { + var targetAddress = row.Target == CreateAvatarFavSheet.Target.Agent + ? agentAddress + : avatarAddress; + Assert.Equal(row.Currency * row.Quantity, nextState.GetBalance(targetAddress, row.Currency)); + } + } + } +} diff --git a/.Lib9c.Tests/Action/EventDungeonBattleTest.cs b/.Lib9c.Tests/Action/EventDungeonBattleTest.cs new file mode 100644 index 0000000000..92d23c0ced --- /dev/null +++ b/.Lib9c.Tests/Action/EventDungeonBattleTest.cs @@ -0,0 +1,492 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Exceptions; + using Nekoyume.Extensions; + using Nekoyume.Model.Event; + using Nekoyume.Model.Rune; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Nekoyume.TableData; + using Nekoyume.TableData.Event; + using Xunit; + + public class EventDungeonBattleTest + { + private readonly Currency _ncgCurrency; + private readonly TableSheets _tableSheets; + + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + private IWorld _initialStates; + + public EventDungeonBattleTest() + { + _initialStates = new World(new MockWorldState()); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + _ncgCurrency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + _initialStates = _initialStates.SetLegacyState( + GoldCurrencyState.Address, + new GoldCurrencyState(_ncgCurrency).Serialize()); + var sheets = TableSheetsImporter.ImportSheets(); + foreach (var (key, value) in sheets) + { + _initialStates = _initialStates + .SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + _tableSheets = new TableSheets(sheets); + + _agentAddress = new PrivateKey().Address; + _avatarAddress = _agentAddress.Derive("avatar"); + + var agentState = new AgentState(_agentAddress); + agentState.avatarAddresses.Add(0, _avatarAddress); + + var gameConfigState = new GameConfigState(sheets[nameof(GameConfigSheet)]); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + gameConfigState, + new PrivateKey().Address + ) + { + level = 100, + }; + + _initialStates = _initialStates + .SetAgentState(_agentAddress, agentState) + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(gameConfigState.address, gameConfigState.Serialize()); + } + + [Theory] + [InlineData(1001, 10010001, 10010001)] + public void Execute_Success_Within_Event_Period( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) + { + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + var contextBlockIndex = scheduleRow.StartBlockIndex; + var nextStates = Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: contextBlockIndex); + var eventDungeonInfoAddr = + EventDungeonInfo.DeriveAddress(_avatarAddress, eventDungeonId); + var eventDungeonInfo = + new EventDungeonInfo(nextStates.GetLegacyState(eventDungeonInfoAddr)); + Assert.Equal( + scheduleRow.DungeonTicketsMax - 1, + eventDungeonInfo.RemainingTickets); + + contextBlockIndex = scheduleRow.DungeonEndBlockIndex; + nextStates = Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: contextBlockIndex); + eventDungeonInfo = + new EventDungeonInfo(nextStates.GetLegacyState(eventDungeonInfoAddr)); + Assert.Equal( + scheduleRow.DungeonTicketsMax - 1, + eventDungeonInfo.RemainingTickets); + } + + [Theory] + [InlineData(1001, 10010001, 10010001, 0, 0, 0)] + [InlineData(1001, 10010001, 10010001, 1, 1, 1)] + [InlineData(1001, 10010001, 10010001, int.MaxValue, int.MaxValue, int.MaxValue - 1)] + public void Execute_Success_With_Ticket_Purchase( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId, + int dungeonTicketPrice, + int dungeonTicketAdditionalPrice, + int numberOfTicketPurchases) + { + var context = new ActionContext(); + var previousStates = _initialStates; + var scheduleSheet = _tableSheets.EventScheduleSheet; + Assert.True(scheduleSheet.TryGetValue(eventScheduleId, out var scheduleRow)); + var sb = new StringBuilder(); + sb.AppendLine( + "id,_name,start_block_index,dungeon_end_block_index,dungeon_tickets_max,dungeon_tickets_reset_interval_block_range,dungeon_exp_seed_value,recipe_end_block_index,dungeon_ticket_price,dungeon_ticket_additional_price"); + sb.AppendLine( + $"{eventScheduleId}" + + $",\"2022 Summer Event\"" + + $",{scheduleRow.StartBlockIndex}" + + $",{scheduleRow.DungeonEndBlockIndex}" + + $",{scheduleRow.DungeonTicketsMax}" + + $",{scheduleRow.DungeonTicketsResetIntervalBlockRange}" + + $",{dungeonTicketPrice}" + + $",{dungeonTicketAdditionalPrice}" + + $",{scheduleRow.DungeonExpSeedValue}" + + $",{scheduleRow.RecipeEndBlockIndex}"); + previousStates = previousStates.SetLegacyState( + Addresses.GetSheetAddress(), + sb.ToString().Serialize()); + + var eventDungeonInfoAddr = + EventDungeonInfo.DeriveAddress(_avatarAddress, eventDungeonId); + var eventDungeonInfo = new EventDungeonInfo( + remainingTickets: 0, + numberOfTicketPurchases: numberOfTicketPurchases); + previousStates = previousStates.SetLegacyState( + eventDungeonInfoAddr, + eventDungeonInfo.Serialize()); + + Assert.True(previousStates.GetSheet() + .TryGetValue(eventScheduleId, out var newScheduleRow)); + var ncgHas = newScheduleRow.GetDungeonTicketCost( + numberOfTicketPurchases, + _ncgCurrency); + if (ncgHas.Sign > 0) + { + previousStates = previousStates.MintAsset(context, _agentAddress, ncgHas); + } + + var nextStates = Execute( + previousStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + buyTicketIfNeeded: true, + blockIndex: scheduleRow.StartBlockIndex); + var nextEventDungeonInfoList = + (Bencodex.Types.List)nextStates.GetLegacyState(eventDungeonInfoAddr)!; + Assert.Equal( + numberOfTicketPurchases + 1, + nextEventDungeonInfoList[2].ToInteger()); + Assert.True( + nextStates.TryGetGoldBalance( + _agentAddress, + _ncgCurrency, + out FungibleAssetValue balance + ) + ); + Assert.Equal(0 * _ncgCurrency, balance); + } + + [Theory] + [InlineData(10000001, 10010001, 10010001)] + [InlineData(10010001, 10010001, 10010001)] + public void Execute_Throw_InvalidActionFieldException_By_EventScheduleId( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) => + Assert.Throws(() => + Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId)); + + [Theory] + [InlineData(1001, 10010001, 10010001)] + public void Execute_Throw_InvalidActionFieldException_By_ContextBlockIndex( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) + { + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + var contextBlockIndex = scheduleRow.StartBlockIndex - 1; + Assert.Throws(() => + Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: contextBlockIndex)); + contextBlockIndex = scheduleRow.DungeonEndBlockIndex + 1; + Assert.Throws(() => + Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: contextBlockIndex)); + } + + [Theory] + [InlineData(1001, 10020001, 10010001)] + [InlineData(1001, 1001, 10010001)] + public void Execute_Throw_InvalidActionFieldException_By_EventDungeonId( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) + { + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + Assert.Throws(() => + Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: scheduleRow.StartBlockIndex)); + } + + [Theory] + [InlineData(1001, 10010001, 10020001)] + [InlineData(1001, 10010001, 1001)] + public void Execute_Throw_InvalidActionFieldException_By_EventDungeonStageId( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) + { + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + Assert.Throws(() => + Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: scheduleRow.StartBlockIndex)); + } + + [Theory] + [InlineData(1001, 10010001, 10010001)] + public void Execute_Throw_NotEnoughEventDungeonTicketsException( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) + { + var previousStates = _initialStates; + var eventDungeonInfoAddr = + EventDungeonInfo.DeriveAddress(_avatarAddress, eventDungeonId); + var eventDungeonInfo = new EventDungeonInfo(); + previousStates = previousStates + .SetLegacyState(eventDungeonInfoAddr, eventDungeonInfo.Serialize()); + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + Assert.Throws(() => + Execute( + previousStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: scheduleRow.StartBlockIndex)); + } + + [Theory] + [InlineData(1001, 10010001, 10010001, 0)] + [InlineData(1001, 10010001, 10010001, int.MaxValue - 1)] + public void Execute_Throw_InsufficientBalanceException( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId, + int numberOfTicketPurchases) + { + var context = new ActionContext(); + var previousStates = _initialStates; + var eventDungeonInfoAddr = + EventDungeonInfo.DeriveAddress(_avatarAddress, eventDungeonId); + var eventDungeonInfo = new EventDungeonInfo( + remainingTickets: 0, + numberOfTicketPurchases: numberOfTicketPurchases); + previousStates = previousStates + .SetLegacyState(eventDungeonInfoAddr, eventDungeonInfo.Serialize()); + + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + var ncgHas = scheduleRow.GetDungeonTicketCost( + numberOfTicketPurchases, + _ncgCurrency) - 1 * _ncgCurrency; + if (ncgHas.Sign > 0) + { + previousStates = previousStates.MintAsset(context, _agentAddress, ncgHas); + } + + Assert.Throws(() => + Execute( + previousStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + buyTicketIfNeeded: true, + blockIndex: scheduleRow.StartBlockIndex)); + } + + [Theory] + [InlineData(1001, 10010001, 10010002)] + public void Execute_Throw_StageNotClearedException( + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId) + { + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(eventScheduleId, out var scheduleRow)); + Assert.Throws(() => + Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: scheduleRow.StartBlockIndex)); + } + + [Theory] + [InlineData(0, 30001, 1, 30001, typeof(DuplicatedRuneIdException))] + [InlineData(1, 10002, 1, 30001, typeof(DuplicatedRuneSlotIndexException))] + public void Execute_DuplicatedException(int slotIndex, int runeId, int slotIndex2, int runeId2, Type exception) + { + Assert.True(_tableSheets.EventScheduleSheet + .TryGetValue(1001, out var scheduleRow)); + + var context = new ActionContext(); + _initialStates = _initialStates.MintAsset(context, _agentAddress, 99999 * _ncgCurrency); + + var unlockRuneSlot = new UnlockRuneSlot() + { + AvatarAddress = _avatarAddress, + SlotIndex = 1, + }; + + _initialStates = unlockRuneSlot.Execute(new ActionContext + { + BlockIndex = 1, + PreviousState = _initialStates, + Signer = _agentAddress, + RandomSeed = 0, + }); + + Assert.Throws(exception, () => + Execute( + _initialStates, + 1001, + 10010001, + 10010001, + false, + scheduleRow.StartBlockIndex, + slotIndex, + runeId, + slotIndex2, + runeId2)); + } + + [Fact] + public void Execute_V100301() + { + int eventScheduleId = 1001; + int eventDungeonId = 10010001; + int eventDungeonStageId = 10010001; + var csv = $@"id,_name,start_block_index,dungeon_end_block_index,dungeon_tickets_max,dungeon_tickets_reset_interval_block_range,dungeon_ticket_price,dungeon_ticket_additional_price,dungeon_exp_seed_value,recipe_end_block_index + 1001,2022 Summer Event,{ActionObsoleteConfig.V100301ExecutedBlockIndex},{ActionObsoleteConfig.V100301ExecutedBlockIndex + 100},5,7200,5,2,1,5018000"; + _initialStates = + _initialStates.SetLegacyState( + Addresses.GetSheetAddress(), + csv.Serialize()); + var sheet = new EventScheduleSheet(); + sheet.Set(csv); + Assert.True(sheet.TryGetValue(eventScheduleId, out var scheduleRow)); + var contextBlockIndex = scheduleRow.StartBlockIndex; + var nextStates = Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: contextBlockIndex); + var eventDungeonInfoAddr = + EventDungeonInfo.DeriveAddress(_avatarAddress, eventDungeonId); + var eventDungeonInfo = + new EventDungeonInfo(nextStates.GetLegacyState(eventDungeonInfoAddr)); + Assert.Equal( + scheduleRow.DungeonTicketsMax - 1, + eventDungeonInfo.RemainingTickets); + + contextBlockIndex = scheduleRow.DungeonEndBlockIndex; + nextStates = Execute( + _initialStates, + eventScheduleId, + eventDungeonId, + eventDungeonStageId, + blockIndex: contextBlockIndex); + eventDungeonInfo = + new EventDungeonInfo(nextStates.GetLegacyState(eventDungeonInfoAddr)); + Assert.Equal( + scheduleRow.DungeonTicketsMax - 1, + eventDungeonInfo.RemainingTickets); + } + + private IWorld Execute( + IWorld previousStates, + int eventScheduleId, + int eventDungeonId, + int eventDungeonStageId, + bool buyTicketIfNeeded = false, + long blockIndex = 0, + int slotIndex = 0, + int runeId = 10002, + int slotIndex2 = 1, + int runeId2 = 30001) + { + var previousAvatarState = previousStates.GetAvatarState(_avatarAddress); + var equipments = + Doomfist.GetAllParts(_tableSheets, previousAvatarState.level); + foreach (var equipment in equipments) + { + previousAvatarState.inventory.AddItem(equipment, iLock: null); + } + + var action = new EventDungeonBattle + { + AvatarAddress = _avatarAddress, + EventScheduleId = eventScheduleId, + EventDungeonId = eventDungeonId, + EventDungeonStageId = eventDungeonStageId, + Equipments = equipments + .Select(e => e.NonFungibleId) + .ToList(), + Costumes = new List(), + Foods = new List(), + RuneInfos = new List() + { + new RuneSlotInfo(slotIndex, runeId), + new RuneSlotInfo(slotIndex2, runeId2), + }, + BuyTicketIfNeeded = buyTicketIfNeeded, + }; + + var nextStates = action.Execute(new ActionContext + { + PreviousState = previousStates, + Signer = _agentAddress, + RandomSeed = 0, + BlockIndex = blockIndex, + }); + + Assert.True(nextStates.GetSheet().TryGetValue( + eventScheduleId, + out var scheduleRow)); + var nextAvatarState = nextStates.GetAvatarState(_avatarAddress); + var expectExp = scheduleRow.GetStageExp( + eventDungeonStageId.ToEventDungeonStageNumber()); + Assert.Equal( + previousAvatarState.exp + expectExp, + nextAvatarState.exp); + + return nextStates; + } + } +} diff --git a/.Lib9c.Tests/Action/HackAndSlashSweepTest.cs b/.Lib9c.Tests/Action/HackAndSlashSweepTest.cs new file mode 100644 index 0000000000..f14fe1e177 --- /dev/null +++ b/.Lib9c.Tests/Action/HackAndSlashSweepTest.cs @@ -0,0 +1,712 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Bencodex.Types; + using Libplanet.Action; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Extensions; + using Nekoyume.Helper; + using Nekoyume.Model; + using Nekoyume.Model.Item; + using Nekoyume.Model.Rune; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Nekoyume.TableData; + using Xunit; + + public class HackAndSlashSweepTest + { + private readonly Dictionary _sheets; + private readonly TableSheets _tableSheets; + + private readonly Address _agentAddress; + + private readonly Address _avatarAddress; + private readonly AvatarState _avatarState; + + private readonly Address _rankingMapAddress; + + private readonly WeeklyArenaState _weeklyArenaState; + private readonly IWorld _initialState; + private readonly IRandom _random; + + public HackAndSlashSweepTest() + { + _random = new TestRandom(); + _sheets = TableSheetsImporter.ImportSheets(); + _tableSheets = new TableSheets(_sheets); + + var privateKey = new PrivateKey(); + _agentAddress = privateKey.PublicKey.Address; + var agentState = new AgentState(_agentAddress); + + _avatarAddress = _agentAddress.Derive("avatar"); + var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); + _rankingMapAddress = _avatarAddress.Derive("ranking_map"); + _avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress + ) + { + level = 100, + }; + agentState.avatarAddresses.Add(0, _avatarAddress); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + var currency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + var goldCurrencyState = new GoldCurrencyState(currency); + _weeklyArenaState = new WeeklyArenaState(0); + _initialState = new World(new MockWorldState()) + .SetLegacyState(_weeklyArenaState.address, _weeklyArenaState.Serialize()) + .SetAgentState(_agentAddress, agentState) + .SetAvatarState(_avatarAddress, _avatarState) + .SetLegacyState(gameConfigState.address, gameConfigState.Serialize()) + .SetLegacyState(Addresses.GoldCurrency, goldCurrencyState.Serialize()); + + foreach (var (key, value) in _sheets) + { + _initialState = _initialState + .SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + foreach (var address in _avatarState.combinationSlotAddresses) + { + var slotState = new CombinationSlotState( + address, + GameConfig.RequireClearedStageLevel.CombinationEquipmentAction); + _initialState = _initialState.SetLegacyState(address, slotState.Serialize()); + } + } + + public (List Equipments, List Costumes) GetDummyItems(AvatarState avatarState) + { + var equipments = Doomfist.GetAllParts(_tableSheets, avatarState.level) + .Select(e => e.NonFungibleId).ToList(); + var random = new TestRandom(); + var costumes = new List(); + if (avatarState.level >= GameConfig.RequireCharacterLevel.CharacterFullCostumeSlot) + { + var costumeId = _tableSheets + .CostumeItemSheet + .Values + .First(r => r.ItemSubType == ItemSubType.FullCostume) + .Id; + + var costume = (Costume)ItemFactory.CreateItem( + _tableSheets.ItemSheet[costumeId], random); + avatarState.inventory.AddItem(costume); + costumes.Add(costume.ItemId); + } + + return (equipments, costumes); + } + + [Fact] + public void Execute_FailedLoadStateException() + { + var action = new HackAndSlashSweep + { + runeInfos = new List(), + apStoneCount = 1, + avatarAddress = _avatarAddress, + worldId = 1, + stageId = 1, + }; + + IWorld state = new World(new MockWorldState()); + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + + [Theory] + [InlineData(100, 1)] + public void Execute_SheetRowNotFoundException(int worldId, int stageId) + { + var action = new HackAndSlashSweep + { + runeInfos = new List(), + apStoneCount = 1, + avatarAddress = _avatarAddress, + worldId = worldId, + stageId = stageId, + }; + + var state = _initialState.SetLegacyState( + _avatarAddress.Derive("world_ids"), + List.Empty.Add(worldId.Serialize()) + ); + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + + [Theory] + [InlineData(1, 999)] + [InlineData(2, 50)] + public void Execute_SheetRowColumnException(int worldId, int stageId) + { + var action = new HackAndSlashSweep + { + runeInfos = new List(), + apStoneCount = 1, + avatarAddress = _avatarAddress, + worldId = worldId, + stageId = stageId, + }; + + var state = _initialState.SetLegacyState( + _avatarAddress.Derive("world_ids"), + List.Empty.Add(worldId.Serialize()) + ); + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + + [Theory] + [InlineData(1, 48, 1, 50)] + [InlineData(1, 49, 2, 51)] + public void Execute_InvalidStageException(int clearedWorldId, int clearedStageId, int worldId, int stageId) + { + var action = new HackAndSlashSweep + { + runeInfos = new List(), + apStoneCount = 1, + avatarAddress = _avatarAddress, + worldId = worldId, + stageId = stageId, + }; + var worldSheet = _initialState.GetSheet(); + var worldUnlockSheet = _initialState.GetSheet(); + + _avatarState.worldInformation.ClearStage(clearedWorldId, clearedStageId, 1, worldSheet, worldUnlockSheet); + + var state = _initialState + .SetLegacyState( + _avatarAddress.Derive("world_ids"), + List.Empty.Add(worldId.Serialize()) + ) + .SetAvatarState(_avatarAddress, _avatarState); + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + + [Theory] + [InlineData(GameConfig.MimisbrunnrWorldId, 10000001, false)] + [InlineData(GameConfig.MimisbrunnrWorldId, 10000001, true)] + // Unlock CRYSTAL first. + [InlineData(2, 51, false)] + public void Execute_InvalidWorldException(int worldId, int stageId, bool unlockedIdsExist) + { + var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 10000001), + }; + + IWorld state = _initialState.SetAvatarState(_avatarAddress, avatarState); + + if (unlockedIdsExist) + { + state = state.SetLegacyState( + _avatarAddress.Derive("world_ids"), + List.Empty.Add(worldId.Serialize()) + ); + } + + var action = new HackAndSlashSweep + { + runeInfos = new List(), + apStoneCount = 1, + avatarAddress = _avatarAddress, + worldId = worldId, + stageId = stageId, + }; + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + + [Fact] + public void Execute_UsageLimitExceedException() + { + var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + }; + + IWorld state = _initialState.SetAvatarState(_avatarAddress, avatarState); + + var action = new HackAndSlashSweep + { + runeInfos = new List(), + apStoneCount = 99, + avatarAddress = _avatarAddress, + worldId = 1, + stageId = 2, + }; + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + + [Theory] + [InlineData(3, 2)] + [InlineData(7, 5)] + public void Execute_NotEnoughMaterialException(int useApStoneCount, int holdingApStoneCount) + { + var gameConfigState = _initialState.GetGameConfigState(); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + level = 400, + }; + + var row = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.ApStone); + var apStone = ItemFactory.CreateTradableMaterial(row); + avatarState.inventory.AddItem(apStone, holdingApStoneCount); + + IWorld state = _initialState.SetAvatarState(_avatarAddress, avatarState); + + var stageSheet = _initialState.GetSheet(); + var (expectedLevel, expectedExp) = (0, 0L); + if (stageSheet.TryGetValue(2, out var stageRow)) + { + var itemPlayCount = + gameConfigState.ActionPointMax / stageRow.CostAP * useApStoneCount; + var apPlayCount = avatarState.actionPoint / stageRow.CostAP; + var playCount = apPlayCount + itemPlayCount; + (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _tableSheets.CharacterLevelSheet, + 2, + playCount); + + var (equipments, costumes) = GetDummyItems(avatarState); + + var action = new HackAndSlashSweep + { + equipments = equipments, + costumes = costumes, + runeInfos = new List(), + avatarAddress = _avatarAddress, + actionPoint = avatarState.actionPoint, + apStoneCount = useApStoneCount, + worldId = 1, + stageId = 2, + }; + + Assert.Throws(() => action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + } + + [Fact] + public void Execute_NotEnoughActionPointException() + { + var gameConfigState = _initialState.GetGameConfigState(); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + level = 400, + actionPoint = 0, + }; + + IWorld state = _initialState.SetAvatarState(_avatarAddress, avatarState); + + var stageSheet = _initialState.GetSheet(); + var (expectedLevel, expectedExp) = (0, 0L); + if (stageSheet.TryGetValue(2, out var stageRow)) + { + var itemPlayCount = + gameConfigState.ActionPointMax / stageRow.CostAP * 1; + var apPlayCount = avatarState.actionPoint / stageRow.CostAP; + var playCount = apPlayCount + itemPlayCount; + (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _tableSheets.CharacterLevelSheet, + 2, + playCount); + + var (equipments, costumes) = GetDummyItems(avatarState); + var action = new HackAndSlashSweep + { + runeInfos = new List(), + costumes = costumes, + equipments = equipments, + avatarAddress = _avatarAddress, + actionPoint = 999999, + apStoneCount = 0, + worldId = 1, + stageId = 2, + }; + + Assert.Throws(() => + action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + } + + [Fact] + public void Execute_PlayCountIsZeroException() + { + var gameConfigState = _initialState.GetGameConfigState(); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + level = 400, + actionPoint = 0, + }; + + IWorld state = _initialState.SetAvatarState(_avatarAddress, avatarState); + + var stageSheet = _initialState.GetSheet(); + var (expectedLevel, expectedExp) = (0, 0L); + if (stageSheet.TryGetValue(2, out var stageRow)) + { + var itemPlayCount = + gameConfigState.ActionPointMax / stageRow.CostAP * 1; + var apPlayCount = avatarState.actionPoint / stageRow.CostAP; + var playCount = apPlayCount + itemPlayCount; + (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _tableSheets.CharacterLevelSheet, + 2, + playCount); + + var (equipments, costumes) = GetDummyItems(avatarState); + var action = new HackAndSlashSweep + { + costumes = costumes, + equipments = equipments, + runeInfos = new List(), + avatarAddress = _avatarAddress, + actionPoint = 0, + apStoneCount = 0, + worldId = 1, + stageId = 2, + }; + + Assert.Throws(() => + action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + } + + [Fact] + public void Execute_NotEnoughCombatPointException() + { + var gameConfigState = _initialState.GetGameConfigState(); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + actionPoint = 0, + level = 1, + }; + + IWorld state = _initialState.SetAvatarState(_avatarAddress, avatarState); + + var stageSheet = _initialState.GetSheet(); + var (expectedLevel, expectedExp) = (0, 0L); + int stageId = 24; + if (stageSheet.TryGetValue(stageId, out var stageRow)) + { + var itemPlayCount = + gameConfigState.ActionPointMax / stageRow.CostAP * 1; + var apPlayCount = avatarState.actionPoint / stageRow.CostAP; + var playCount = apPlayCount + itemPlayCount; + (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _tableSheets.CharacterLevelSheet, + stageId, + playCount); + + var action = new HackAndSlashSweep + { + costumes = new List(), + equipments = new List(), + runeInfos = new List(), + avatarAddress = _avatarAddress, + actionPoint = avatarState.actionPoint, + apStoneCount = 1, + worldId = 1, + stageId = stageId, + }; + + Assert.Throws(() => + action.Execute(new ActionContext() + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + public void ExecuteWithStake(int stakingLevel) + { + const int worldId = 1; + const int stageId = 1; + var gameConfigState = _initialState.GetGameConfigState(); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + actionPoint = 120, + level = 3, + }; + var itemRow = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.ApStone); + var apStone = ItemFactory.CreateTradableMaterial(itemRow); + avatarState.inventory.AddItem(apStone); + + var stakeStateAddress = StakeState.DeriveAddress(_agentAddress); + var stakeState = new StakeState(stakeStateAddress, 1); + var requiredGold = _tableSheets.StakeRegularRewardSheet.OrderedRows + .FirstOrDefault(r => r.Level == stakingLevel)?.RequiredGold ?? 0; + var context = new ActionContext(); + var state = _initialState + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(stakeStateAddress, stakeState.Serialize()) + .MintAsset(context, stakeStateAddress, requiredGold * _initialState.GetGoldCurrency()); + var stageSheet = _initialState.GetSheet(); + if (stageSheet.TryGetValue(stageId, out var stageRow)) + { + var apSheet = _initialState.GetSheet(); + var costAp = apSheet.GetActionPointByStaking(stageRow.CostAP, 1, stakingLevel); + var itemPlayCount = + gameConfigState.ActionPointMax / costAp * 1; + var apPlayCount = avatarState.actionPoint / costAp; + var playCount = apPlayCount + itemPlayCount; + var (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _initialState.GetSheet(), + stageId, + playCount); + + var action = new HackAndSlashSweep + { + costumes = new List(), + equipments = new List(), + runeInfos = new List(), + avatarAddress = _avatarAddress, + actionPoint = avatarState.actionPoint, + apStoneCount = 1, + worldId = worldId, + stageId = stageId, + }; + + var nextState = action.Execute(new ActionContext + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + }); + var nextAvatar = nextState.GetAvatarState(_avatarAddress); + Assert.Equal(expectedLevel, nextAvatar.level); + Assert.Equal(expectedExp, nextAvatar.exp); + } + else + { + throw new SheetRowNotFoundException(nameof(StageSheet), stageId); + } + } + + [Theory] + [InlineData(0, 30001, 1, 30001, typeof(DuplicatedRuneIdException))] + [InlineData(1, 10002, 1, 30001, typeof(DuplicatedRuneSlotIndexException))] + public void ExecuteDuplicatedException(int slotIndex, int runeId, int slotIndex2, int runeId2, Type exception) + { + var stakingLevel = 1; + const int worldId = 1; + const int stageId = 1; + var gameConfigState = _initialState.GetGameConfigState(); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _initialState.GetAvatarSheets(), + gameConfigState, + _rankingMapAddress) + { + worldInformation = + new WorldInformation(0, _initialState.GetSheet(), 25), + actionPoint = 120, + level = 3, + }; + var itemRow = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.ApStone); + var apStone = ItemFactory.CreateTradableMaterial(itemRow); + avatarState.inventory.AddItem(apStone); + + var stakeStateAddress = StakeState.DeriveAddress(_agentAddress); + var stakeState = new StakeState(stakeStateAddress, 1); + var requiredGold = _tableSheets.StakeRegularRewardSheet.OrderedRows + .FirstOrDefault(r => r.Level == stakingLevel)?.RequiredGold ?? 0; + var context = new ActionContext(); + var state = _initialState + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(stakeStateAddress, stakeState.Serialize()) + .MintAsset(context, stakeStateAddress, requiredGold * _initialState.GetGoldCurrency()); + var stageSheet = _initialState.GetSheet(); + if (stageSheet.TryGetValue(stageId, out var stageRow)) + { + var apSheet = _initialState.GetSheet(); + var costAp = apSheet.GetActionPointByStaking(stageRow.CostAP, 1, stakingLevel); + var itemPlayCount = + gameConfigState.ActionPointMax / costAp * 1; + var apPlayCount = avatarState.actionPoint / costAp; + var playCount = apPlayCount + itemPlayCount; + var (expectedLevel, expectedExp) = avatarState.GetLevelAndExp( + _initialState.GetSheet(), + stageId, + playCount); + + var ncgCurrency = state.GetGoldCurrency(); + state = state.MintAsset(context, _agentAddress, 99999 * ncgCurrency); + + var unlockRuneSlot = new UnlockRuneSlot() + { + AvatarAddress = _avatarAddress, + SlotIndex = 1, + }; + + state = unlockRuneSlot.Execute(new ActionContext + { + BlockIndex = 1, + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + }); + + var action = new HackAndSlashSweep + { + costumes = new List(), + equipments = new List(), + runeInfos = new List() + { + new RuneSlotInfo(slotIndex, runeId), + new RuneSlotInfo(slotIndex2, runeId2), + }, + avatarAddress = _avatarAddress, + actionPoint = avatarState.actionPoint, + apStoneCount = 1, + worldId = worldId, + stageId = stageId, + }; + + Assert.Throws(exception, () => action.Execute(new ActionContext + { + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + })); + } + else + { + throw new SheetRowNotFoundException(nameof(StageSheet), stageId); + } + } + } +} diff --git a/.Lib9c.Tests/Action/ItemEnhancementTest.cs b/.Lib9c.Tests/Action/ItemEnhancementTest.cs new file mode 100644 index 0000000000..901db08f20 --- /dev/null +++ b/.Lib9c.Tests/Action/ItemEnhancementTest.cs @@ -0,0 +1,366 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Linq; + using Bencodex.Types; + using Lib9c.Tests.Fixtures.TableCSV.Cost; + using Lib9c.Tests.Util; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Extensions; + using Nekoyume.Model.Item; + using Nekoyume.Model.Mail; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Xunit; + + public class ItemEnhancementTest + { + private readonly TableSheets _tableSheets; + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + private readonly AvatarState _avatarState; + private readonly Currency _currency; + private IWorld _initialState; + + public ItemEnhancementTest() + { + _initialState = new World(new MockWorldState()); + Dictionary sheets; + (_initialState, sheets) = InitializeUtil.InitializeTableSheets( + _initialState, + sheetsOverride: new Dictionary + { + { + "EnhancementCostSheetV3", + EnhancementCostSheetFixtures.V3 + }, + }); + _tableSheets = new TableSheets(sheets); + foreach (var (key, value) in sheets) + { + _initialState = + _initialState.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + var privateKey = new PrivateKey(); + _agentAddress = privateKey.PublicKey.Address; + var agentState = new AgentState(_agentAddress); + + _avatarAddress = _agentAddress.Derive("avatar"); + _avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + new GameConfigState(), + default + ); + + agentState.avatarAddresses.Add(0, _avatarAddress); + +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + _currency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + var gold = new GoldCurrencyState(_currency); + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0 + )); + + var context = new ActionContext(); + _initialState = _initialState + .SetAgentState(_agentAddress, agentState) + .SetAvatarState(_avatarAddress, _avatarState) + .SetLegacyState(slotAddress, new CombinationSlotState(slotAddress, 0).Serialize()) + .SetLegacyState(GoldCurrencyState.Address, gold.Serialize()) + .MintAsset(context, GoldCurrencyState.Address, gold.Currency * 100_000_000_000) + .TransferAsset( + context, + Addresses.GoldCurrency, + _agentAddress, + gold.Currency * 3_000_000 + ); + + Assert.Equal( + gold.Currency * 99_997_000_000, + _initialState.GetBalance(Addresses.GoldCurrency, gold.Currency) + ); + Assert.Equal( + gold.Currency * 3_000_000, + _initialState.GetBalance(_agentAddress, gold.Currency) + ); + } + + [Theory] + // from 0 to 0 using one level 0 material + [InlineData(0, false, 0, false, 1)] + [InlineData(0, false, 0, true, 1)] + [InlineData(0, true, 0, false, 1)] + [InlineData(0, true, 0, true, 1)] + // from 0 to 1 using two level 0 material + [InlineData(0, false, 0, false, 3)] + [InlineData(0, false, 0, true, 3)] + [InlineData(0, true, 0, false, 3)] + [InlineData(0, true, 0, true, 3)] + // // Duplicated > from 0 to 0 + [InlineData(0, false, 0, false, 3, true)] + [InlineData(0, false, 0, true, 3, true)] + [InlineData(0, true, 0, false, 3, true)] + [InlineData(0, true, 0, true, 3, true)] + // from 0 to N using multiple level 0 materials + [InlineData(0, false, 0, false, 7)] + [InlineData(0, false, 0, false, 31)] + [InlineData(0, false, 0, true, 7)] + [InlineData(0, false, 0, true, 31)] + [InlineData(0, true, 0, false, 7)] + [InlineData(0, true, 0, false, 31)] + [InlineData(0, true, 0, true, 7)] + [InlineData(0, true, 0, true, 31)] + // // Duplicated > from 0 to 0 + [InlineData(0, false, 0, false, 7, true)] + [InlineData(0, false, 0, false, 31, true)] + [InlineData(0, false, 0, true, 7, true)] + [InlineData(0, false, 0, true, 31, true)] + [InlineData(0, true, 0, false, 7, true)] + [InlineData(0, true, 0, false, 31, true)] + [InlineData(0, true, 0, true, 7, true)] + [InlineData(0, true, 0, true, 31, true)] + // from K to K with material(s). Check requiredBlock == 0 + [InlineData(10, false, 0, false, 1)] + [InlineData(10, false, 0, true, 1)] + [InlineData(10, true, 0, false, 1)] + [InlineData(10, true, 0, true, 1)] + // from K to N using one level X material + [InlineData(5, false, 6, false, 1)] + [InlineData(5, false, 6, true, 1)] + [InlineData(5, true, 6, false, 1)] + [InlineData(5, true, 6, true, 1)] + // from K to N using multiple materials + [InlineData(5, false, 4, false, 6)] + [InlineData(5, false, 7, false, 5)] + [InlineData(5, false, 4, true, 6)] + [InlineData(5, false, 7, true, 5)] + [InlineData(5, true, 4, false, 6)] + [InlineData(5, true, 7, false, 5)] + [InlineData(5, true, 4, true, 6)] + [InlineData(5, true, 7, true, 5)] + // // Duplicated: from K to K + [InlineData(5, true, 4, true, 6, true)] + [InlineData(5, true, 7, true, 5, true)] + [InlineData(5, true, 4, false, 6, true)] + [InlineData(5, true, 7, false, 5, true)] + [InlineData(5, false, 4, true, 6, true)] + [InlineData(5, false, 7, true, 5, true)] + [InlineData(5, false, 4, false, 6, true)] + [InlineData(5, false, 7, false, 5, true)] + // from 20 to 21 (just to reach level 21 exp) + [InlineData(20, false, 20, false, 1)] + [InlineData(20, false, 20, true, 1)] + [InlineData(20, true, 20, false, 1)] + [InlineData(20, true, 20, true, 1)] + // from 20 to 21 (over level 21) + [InlineData(20, false, 20, false, 2)] + [InlineData(20, false, 20, true, 2)] + [InlineData(20, true, 20, false, 2)] + [InlineData(20, true, 20, true, 2)] + // from 21 to 21 (no level up) + [InlineData(21, false, 1, false, 1)] + [InlineData(21, false, 21, false, 1)] + [InlineData(21, false, 1, true, 1)] + [InlineData(21, false, 21, true, 1)] + [InlineData(21, true, 1, false, 1)] + [InlineData(21, true, 21, false, 1)] + [InlineData(21, true, 1, true, 1)] + [InlineData(21, true, 21, true, 1)] + public void Execute( + int startLevel, + bool oldStart, + int materialLevel, + bool oldMaterial, + int materialCount, + bool duplicated = false + ) + { + var row = _tableSheets.EquipmentItemSheet.Values.First(r => r.Id == 10110000); + var equipment = (Equipment)ItemFactory.CreateItemUsable(row, default, 0, startLevel); + if (startLevel == 0) + { + equipment.Exp = (long)row.Exp!; + } + else + { + equipment.Exp = _tableSheets.EnhancementCostSheetV3.OrderedList.First(r => + r.ItemSubType == equipment.ItemSubType && r.Grade == equipment.Grade && + r.Level == equipment.level).Exp; + } + + var startExp = equipment.Exp; + if (oldStart) + { + equipment.Exp = 0L; + } + + _avatarState.inventory.AddItem(equipment, count: 1); + + var startRow = _tableSheets.EnhancementCostSheetV3.OrderedList.FirstOrDefault(r => + r.Grade == equipment.Grade && r.ItemSubType == equipment.ItemSubType && + r.Level == startLevel); + var expectedExpIncrement = 0L; + var materialIds = new List(); + var duplicatedGuid = Guid.NewGuid(); + for (var i = 0; i < materialCount; i++) + { + var materialId = duplicated ? duplicatedGuid : Guid.NewGuid(); + materialIds.Add(materialId); + var material = + (Equipment)ItemFactory.CreateItemUsable(row, materialId, 0, materialLevel); + if (materialLevel == 0) + { + material.Exp = (long)row.Exp!; + } + else + { + material.Exp = _tableSheets.EnhancementCostSheetV3.OrderedList.First(r => + r.ItemSubType == material.ItemSubType && r.Grade == material.Grade && + r.Level == material.level).Exp; + } + + if (!(duplicated && i > 0)) + { + expectedExpIncrement += material.Exp; + } + + if (oldMaterial) + { + material.Exp = 0L; + } + + _avatarState.inventory.AddItem(material, count: 1); + } + + var result = new CombinationConsumable5.ResultModel() + { + id = default, + gold = 0, + actionPoint = 0, + recipeId = 1, + materials = new Dictionary(), + itemUsable = equipment, + }; + var preItemUsable = new Equipment((Dictionary)equipment.Serialize()); + + for (var i = 0; i < 100; i++) + { + var mail = new CombinationMail(result, i, default, 0); + _avatarState.Update(mail); + } + + _avatarState.worldInformation.ClearStage( + 1, + 1, + 1, + _tableSheets.WorldSheet, + _tableSheets.WorldUnlockSheet + ); + + var slotAddress = + _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0 + )); + + Assert.Equal(startLevel, equipment.level); + + _initialState = _initialState.SetAvatarState(_avatarAddress, _avatarState); + + var action = new ItemEnhancement + { + itemId = default, + materialIds = materialIds, + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + var nextState = action.Execute(new ActionContext() + { + PreviousState = _initialState, + Signer = _agentAddress, + BlockIndex = 1, + RandomSeed = 0, + }); + + var slotState = nextState.GetCombinationSlotState(_avatarAddress, 0); + var resultEquipment = (Equipment)slotState.Result.itemUsable; + var level = resultEquipment.level; + var nextAvatarState = nextState.GetAvatarState(_avatarAddress); + var expectedTargetRow = _tableSheets.EnhancementCostSheetV3.OrderedList.FirstOrDefault( + r => r.Grade == equipment.Grade && r.ItemSubType == equipment.ItemSubType && + r.Level == level); + var expectedCost = (expectedTargetRow?.Cost ?? 0) - (startRow?.Cost ?? 0); + var expectedBlockIndex = + (expectedTargetRow?.RequiredBlockIndex ?? 0) - (startRow?.RequiredBlockIndex ?? 0); + Assert.Equal(default, resultEquipment.ItemId); + Assert.Equal(startExp + expectedExpIncrement, resultEquipment.Exp); + Assert.Equal( + (3_000_000 - expectedCost) * _currency, + nextState.GetBalance(_agentAddress, _currency) + ); + + var arenaSheet = _tableSheets.ArenaSheet; + var arenaData = arenaSheet.GetRoundByBlockIndex(1); + var feeStoreAddress = + Addresses.GetBlacksmithFeeAddress(arenaData.ChampionshipId, arenaData.Round); + Assert.Equal( + expectedCost * _currency, + nextState.GetBalance(feeStoreAddress, _currency) + ); + Assert.Equal(30, nextAvatarState.mailBox.Count); + + var stateDict = (Dictionary)nextState.GetLegacyState(slotAddress); + var slot = new CombinationSlotState(stateDict); + var slotResult = (ItemEnhancement13.ResultModel)slot.Result; + if (startLevel != level) + { + var baseMinAtk = (decimal)preItemUsable.StatsMap.BaseATK; + var baseMaxAtk = (decimal)preItemUsable.StatsMap.BaseATK; + var extraMinAtk = (decimal)preItemUsable.StatsMap.AdditionalATK; + var extraMaxAtk = (decimal)preItemUsable.StatsMap.AdditionalATK; + + for (var i = startLevel + 1; i <= level; i++) + { + var currentRow = _tableSheets.EnhancementCostSheetV3.OrderedList + .First(x => + x.Grade == 1 && x.ItemSubType == equipment.ItemSubType && x.Level == i); + + baseMinAtk *= currentRow.BaseStatGrowthMin.NormalizeFromTenThousandths() + 1; + baseMaxAtk *= currentRow.BaseStatGrowthMax.NormalizeFromTenThousandths() + 1; + extraMinAtk *= currentRow.ExtraStatGrowthMin.NormalizeFromTenThousandths() + 1; + extraMaxAtk *= currentRow.ExtraStatGrowthMax.NormalizeFromTenThousandths() + 1; + } + + Assert.InRange( + resultEquipment.StatsMap.ATK, + baseMinAtk + extraMinAtk, + baseMaxAtk + extraMaxAtk + 1 + ); + } + + Assert.Equal( + expectedBlockIndex + 1, // +1 for execution + resultEquipment.RequiredBlockIndex + ); + Assert.Equal(preItemUsable.ItemId, slotResult.preItemUsable.ItemId); + Assert.Equal(preItemUsable.ItemId, resultEquipment.ItemId); + Assert.Equal(expectedCost, slotResult.gold); + } + } +} diff --git a/.Lib9c.Tests/Action/RaidTest.cs b/.Lib9c.Tests/Action/RaidTest.cs new file mode 100644 index 0000000000..3a383361a0 --- /dev/null +++ b/.Lib9c.Tests/Action/RaidTest.cs @@ -0,0 +1,626 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Bencodex.Types; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Types.Assets; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Battle; + using Nekoyume.Extensions; + using Nekoyume.Helper; + using Nekoyume.Model.Arena; + using Nekoyume.Model.Rune; + using Nekoyume.Model.Stat; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Nekoyume.TableData; + using Xunit; + using static SerializeKeys; + + public class RaidTest + { + private readonly Dictionary _sheets; + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + private readonly TableSheets _tableSheets; + private readonly Currency _goldCurrency; + + public RaidTest() + { + _sheets = TableSheetsImporter.ImportSheets(); + _tableSheets = new TableSheets(_sheets); + _agentAddress = new PrivateKey().Address; + _avatarAddress = new PrivateKey().Address; +#pragma warning disable CS0618 + // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 + _goldCurrency = Currency.Legacy("NCG", 2, null); +#pragma warning restore CS0618 + } + + [Theory] + // Join new raid. + [InlineData(null, true, true, true, false, 0, 0L, false, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] + [InlineData(null, true, true, true, false, 0, 0L, false, false, 0, false, false, false, 5, true, 0, 10002, 1, 30001)] + // Refill by interval. + [InlineData(null, true, true, false, true, 0, -10368, false, false, 0, false, false, false, 5, true, 0, 10002, 1, 30001)] + // Refill by NCG. + [InlineData(null, true, true, false, true, 0, 200L, true, true, 0, false, false, false, 5, true, 0, 10002, 1, 30001)] + [InlineData(null, true, true, false, true, 0, 200L, true, true, 1, false, false, false, 5, true, 0, 10002, 1, 30001)] + // Boss level up. + [InlineData(null, true, true, false, true, 3, 100L, false, false, 0, true, true, false, 5, true, 0, 10002, 1, 30001)] + // Update RaidRewardInfo. + [InlineData(null, true, true, false, true, 3, 100L, false, false, 0, true, true, true, 5, true, 0, 10002, 1, 30001)] + // Boss skip level up. + [InlineData(null, true, true, false, true, 3, 100L, false, false, 0, true, false, false, 5, true, 0, 10002, 1, 30001)] + // AvatarState null. + [InlineData(typeof(FailedLoadStateException), false, false, false, false, 0, 0L, false, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] + // Insufficient CRYSTAL. + [InlineData(typeof(InsufficientBalanceException), true, true, false, false, 0, 0L, false, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] + // Insufficient NCG. + [InlineData(typeof(InsufficientBalanceException), true, true, false, true, 0, 0L, true, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] + // Wait interval. + [InlineData(typeof(RequiredBlockIntervalException), true, true, false, true, 3, 10L, false, false, 0, false, false, false, 1, false, 0, 10002, 1, 30001)] + // Exceed purchase limit. + [InlineData(typeof(ExceedTicketPurchaseLimitException), true, true, false, true, 0, 100L, true, false, 1_000, false, false, false, 5, false, 0, 10002, 1, 30001)] + // Exceed challenge count. + [InlineData(typeof(ExceedPlayCountException), true, true, false, true, 0, 100L, false, false, 0, false, false, false, 5, false, 0, 10002, 1, 30001)] + [InlineData(typeof(DuplicatedRuneIdException), true, true, false, true, 3, 100L, true, false, 0, false, false, false, 5, false, 0, 30001, 1, 30001)] + [InlineData(typeof(DuplicatedRuneSlotIndexException), true, true, false, true, 3, 100L, true, false, 0, false, false, false, 5, false, 1, 10002, 1, 30001)] + public void Execute( + Type exc, + bool avatarExist, + bool stageCleared, + bool crystalExist, + bool raiderStateExist, + int remainChallengeCount, + long refillBlockIndexOffset, + bool payNcg, + bool ncgExist, + int purchaseCount, + bool kill, + bool levelUp, + bool rewardRecordExist, + long executeOffset, + bool raiderListExist, + int slotIndex, + int runeId, + int slotIndex2, + int runeId2 + ) + { + var blockIndex = _tableSheets.WorldBossListSheet.Values + .OrderBy(x => x.StartedBlockIndex) + .First(x => + { + if (exc == typeof(InsufficientBalanceException)) + { + return ncgExist ? x.TicketPrice > 0 : x.EntranceFee > 0; + } + + return true; + }) + .StartedBlockIndex; + + var action = new Raid + { + AvatarAddress = _avatarAddress, + EquipmentIds = new List(), + CostumeIds = new List(), + FoodIds = new List(), + RuneInfos = new List() + { + new RuneSlotInfo(slotIndex, runeId), + new RuneSlotInfo(slotIndex2, runeId2), + }, + PayNcg = payNcg, + }; + Currency crystal = CrystalCalculator.CRYSTAL; + int raidId = _tableSheets.WorldBossListSheet.FindRaidIdByBlockIndex(blockIndex); + Address raiderAddress = Addresses.GetRaiderAddress(_avatarAddress, raidId); + var goldCurrencyState = new GoldCurrencyState(_goldCurrency); + WorldBossListSheet.Row worldBossRow = _tableSheets.WorldBossListSheet.FindRowByBlockIndex(blockIndex); + var hpSheet = _tableSheets.WorldBossGlobalHpSheet; + Address bossAddress = Addresses.GetWorldBossAddress(raidId); + Address worldBossKillRewardRecordAddress = Addresses.GetWorldBossKillRewardRecordAddress(_avatarAddress, raidId); + Address raiderListAddress = Addresses.GetRaiderListAddress(raidId); + int level = 1; + if (kill & !levelUp) + { + level = hpSheet.OrderedList.Last().Level; + } + + var fee = _tableSheets.WorldBossListSheet[raidId].EntranceFee; + + var context = new ActionContext(); + IWorld state = new World(new MockWorldState()) + .SetLegacyState(goldCurrencyState.address, goldCurrencyState.Serialize()) + .SetAgentState(_agentAddress, new AgentState(_agentAddress)); + + foreach (var (key, value) in _sheets) + { + state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + gameConfigState, + default + ); + + if (avatarExist) + { + var equipments = Doomfist.GetAllParts(_tableSheets, avatarState.level); + foreach (var equipment in equipments) + { + avatarState.inventory.AddItem(equipment); + } + + if (stageCleared) + { + for (int i = 0; i < 50; i++) + { + avatarState.worldInformation.ClearStage(1, i + 1, 0, _tableSheets.WorldSheet, _tableSheets.WorldUnlockSheet); + } + } + + if (crystalExist) + { + var price = _tableSheets.WorldBossListSheet[raidId].EntranceFee; + state = state.MintAsset(context, _agentAddress, price * crystal); + } + + if (raiderStateExist) + { + var raiderState = new RaiderState(); + raiderState.RefillBlockIndex = blockIndex + refillBlockIndexOffset; + raiderState.RemainChallengeCount = remainChallengeCount; + raiderState.TotalScore = 1_000; + raiderState.HighScore = 0; + raiderState.TotalChallengeCount = 1; + raiderState.PurchaseCount = purchaseCount; + raiderState.Cp = 0; + raiderState.Level = 0; + raiderState.IconId = 0; + raiderState.AvatarName = "hash"; + raiderState.AvatarAddress = _avatarAddress; + raiderState.UpdatedBlockIndex = blockIndex; + + state = state.SetLegacyState(raiderAddress, raiderState.Serialize()); + + var raiderList = new List().Add(raiderAddress.Serialize()); + + if (raiderListExist) + { + raiderList = raiderList.Add(new PrivateKey().Address.Serialize()); + } + + state = state.SetLegacyState(raiderListAddress, raiderList); + } + + if (rewardRecordExist) + { + var rewardRecord = new WorldBossKillRewardRecord + { + [0] = false, + }; + state = state.SetLegacyState(worldBossKillRewardRecordAddress, rewardRecord.Serialize()); + } + + if (ncgExist) + { + var row = _tableSheets.WorldBossListSheet.FindRowByBlockIndex(blockIndex); + state = state.MintAsset(context, _agentAddress, (row.TicketPrice + row.AdditionalTicketPrice * purchaseCount) * _goldCurrency); + } + + state = state + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(gameConfigState.address, gameConfigState.Serialize()); + } + + if (kill) + { + var bossState = + new WorldBossState(worldBossRow, _tableSheets.WorldBossGlobalHpSheet[level]) + { + CurrentHp = 0, + Level = level, + }; + state = state.SetLegacyState(bossAddress, bossState.Serialize()); + } + + if (exc is null) + { + var randomSeed = 0; + var ctx = new ActionContext + { + BlockIndex = blockIndex + executeOffset, + PreviousState = state, + RandomSeed = randomSeed, + Signer = _agentAddress, + }; + + var nextState = action.Execute(ctx); + + var random = new TestRandom(randomSeed); + var bossListRow = _tableSheets.WorldBossListSheet.FindRowByBlockIndex(ctx.BlockIndex); + var raidSimulatorSheets = _tableSheets.GetRaidSimulatorSheets(); + var simulator = new RaidSimulator( + bossListRow.BossId, + random, + avatarState, + action.FoodIds, + null, + raidSimulatorSheets, + _tableSheets.CostumeStatSheet, + new List()); + simulator.Simulate(); + var score = simulator.DamageDealt; + + Dictionary rewardMap + = new Dictionary(); + foreach (var reward in simulator.AssetReward) + { + rewardMap[reward.Currency] = reward; + } + + if (rewardRecordExist) + { + var bossRow = raidSimulatorSheets.WorldBossCharacterSheet[bossListRow.BossId]; + Assert.True(state.TryGetLegacyState(bossAddress, out List prevRawBoss)); + var prevBossState = new WorldBossState(prevRawBoss); + int rank = WorldBossHelper.CalculateRank(bossRow, raiderStateExist ? 1_000 : 0); + var rewards = RuneHelper.CalculateReward( + rank, + prevBossState.Id, + _tableSheets.RuneWeightSheet, + _tableSheets.WorldBossKillRewardSheet, + _tableSheets.RuneSheet, + random + ); + + foreach (var reward in rewards) + { + if (!rewardMap.ContainsKey(reward.Currency)) + { + rewardMap[reward.Currency] = reward; + } + else + { + rewardMap[reward.Currency] += reward; + } + } + + foreach (var reward in rewardMap) + { + if (reward.Key.Equals(CrystalCalculator.CRYSTAL)) + { + Assert.Equal(reward.Value, nextState.GetBalance(_agentAddress, reward.Key)); + } + else + { + Assert.Equal(reward.Value, nextState.GetBalance(_avatarAddress, reward.Key)); + } + } + } + + if (rewardMap.ContainsKey(crystal)) + { + Assert.Equal(rewardMap[crystal], nextState.GetBalance(_agentAddress, crystal)); + } + + if (crystalExist) + { + Assert.Equal(fee * crystal, nextState.GetBalance(bossAddress, crystal)); + } + + Assert.True(nextState.TryGetLegacyState(raiderAddress, out List rawRaider)); + var raiderState = new RaiderState(rawRaider); + long expectedTotalScore = raiderStateExist ? 1_000 + score : score; + int expectedRemainChallenge = payNcg ? 0 : 2; + int expectedTotalChallenge = raiderStateExist ? 2 : 1; + + Assert.Equal(score, raiderState.HighScore); + Assert.Equal(expectedTotalScore, raiderState.TotalScore); + Assert.Equal(expectedRemainChallenge, raiderState.RemainChallengeCount); + Assert.Equal(expectedTotalChallenge, raiderState.TotalChallengeCount); + Assert.Equal(1, raiderState.Level); + Assert.Equal(GameConfig.DefaultAvatarArmorId, raiderState.IconId); + Assert.True(raiderState.Cp > 0); + + Assert.True(nextState.TryGetLegacyState(bossAddress, out List rawBoss)); + var bossState = new WorldBossState(rawBoss); + int expectedLevel = level; + if (kill & levelUp) + { + expectedLevel++; + } + + Assert.Equal(expectedLevel, bossState.Level); + Assert.Equal(expectedLevel, raiderState.LatestBossLevel); + if (kill) + { + Assert.Equal(hpSheet[expectedLevel].Hp, bossState.CurrentHp); + } + + if (payNcg) + { + Assert.Equal(0 * _goldCurrency, nextState.GetBalance(_agentAddress, _goldCurrency)); + Assert.Equal(purchaseCount + 1, nextState.GetRaiderState(raiderAddress).PurchaseCount); + } + + Assert.True(nextState.TryGetLegacyState(worldBossKillRewardRecordAddress, out List rawRewardInfo)); + var rewardRecord = new WorldBossKillRewardRecord(rawRewardInfo); + Assert.Contains(expectedLevel, rewardRecord.Keys); + if (rewardRecordExist) + { + Assert.True(rewardRecord[0]); + } + else + { + if (expectedLevel == 1) + { + Assert.False(rewardRecord[1]); + } + else + { + Assert.DoesNotContain(1, rewardRecord.Keys); + } + } + + Assert.True(nextState.TryGetLegacyState(raiderListAddress, out List rawRaiderList)); + List
raiderList = rawRaiderList.ToList(StateExtensions.ToAddress); + + Assert.Contains(raiderAddress, raiderList); + } + else + { + if (exc == typeof(DuplicatedRuneIdException) || exc == typeof(DuplicatedRuneSlotIndexException)) + { + var ncgCurrency = state.GetGoldCurrency(); + state = state.MintAsset(context, _agentAddress, 99999 * ncgCurrency); + + var unlockRuneSlot = new UnlockRuneSlot() + { + AvatarAddress = _avatarAddress, + SlotIndex = 1, + }; + + state = unlockRuneSlot.Execute(new ActionContext + { + BlockIndex = 1, + PreviousState = state, + Signer = _agentAddress, + RandomSeed = 0, + }); + } + + Assert.Throws(exc, () => action.Execute(new ActionContext + { + BlockIndex = blockIndex + executeOffset, + PreviousState = state, + RandomSeed = 0, + Signer = _agentAddress, + })); + } + } + + [Fact] + public void Execute_With_Reward() + { + var action = new Raid + { + AvatarAddress = _avatarAddress, + EquipmentIds = new List(), + CostumeIds = new List(), + FoodIds = new List(), + RuneInfos = new List(), + PayNcg = false, + }; + + var worldBossRow = _tableSheets.WorldBossListSheet.First().Value; + int raidId = worldBossRow.Id; + Address raiderAddress = Addresses.GetRaiderAddress(_avatarAddress, raidId); + var goldCurrencyState = new GoldCurrencyState(_goldCurrency); + Address bossAddress = Addresses.GetWorldBossAddress(raidId); + Address worldBossKillRewardRecordAddress = Addresses.GetWorldBossKillRewardRecordAddress(_avatarAddress, raidId); + + IWorld state = new World(new MockWorldState()) + .SetLegacyState(goldCurrencyState.address, goldCurrencyState.Serialize()) + .SetAgentState(_agentAddress, new AgentState(_agentAddress)); + + foreach (var (key, value) in _sheets) + { + state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + gameConfigState, + default + ); + + for (int i = 0; i < 50; i++) + { + avatarState.worldInformation.ClearStage(1, i + 1, 0, _tableSheets.WorldSheet, _tableSheets.WorldUnlockSheet); + } + + var raiderState = new RaiderState(); + raiderState.RefillBlockIndex = 0; + raiderState.RemainChallengeCount = WorldBossHelper.MaxChallengeCount; + raiderState.TotalScore = 1_000; + raiderState.TotalChallengeCount = 1; + raiderState.PurchaseCount = 0; + raiderState.Cp = 0; + raiderState.Level = 0; + raiderState.IconId = 0; + raiderState.AvatarName = "hash"; + raiderState.AvatarAddress = _avatarAddress; + state = state.SetLegacyState(raiderAddress, raiderState.Serialize()); + + var rewardRecord = new WorldBossKillRewardRecord + { + [1] = false, + }; + state = state.SetLegacyState(worldBossKillRewardRecordAddress, rewardRecord.Serialize()); + + state = state + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(gameConfigState.address, gameConfigState.Serialize()); + + var bossState = + new WorldBossState(worldBossRow, _tableSheets.WorldBossGlobalHpSheet[2]) + { + CurrentHp = 0, + Level = 2, + }; + state = state.SetLegacyState(bossAddress, bossState.Serialize()); + var randomSeed = 0; + var random = new TestRandom(randomSeed); + + var simulator = new RaidSimulator( + worldBossRow.BossId, + random, + avatarState, + action.FoodIds, + null, + _tableSheets.GetRaidSimulatorSheets(), + _tableSheets.CostumeStatSheet, + new List()); + simulator.Simulate(); + + Dictionary rewardMap + = new Dictionary(); + foreach (var reward in simulator.AssetReward) + { + rewardMap[reward.Currency] = reward; + } + + List killRewards = RuneHelper.CalculateReward( + 0, + bossState.Id, + _tableSheets.RuneWeightSheet, + _tableSheets.WorldBossKillRewardSheet, + _tableSheets.RuneSheet, + random + ); + + var nextState = action.Execute(new ActionContext + { + BlockIndex = worldBossRow.StartedBlockIndex + gameConfigState.WorldBossRequiredInterval, + PreviousState = state, + RandomSeed = randomSeed, + Signer = _agentAddress, + }); + + Assert.True(nextState.TryGetLegacyState(raiderAddress, out List rawRaider)); + var nextRaiderState = new RaiderState(rawRaider); + Assert.Equal(simulator.DamageDealt, nextRaiderState.HighScore); + + foreach (var reward in killRewards) + { + if (!rewardMap.ContainsKey(reward.Currency)) + { + rewardMap[reward.Currency] = reward; + } + else + { + rewardMap[reward.Currency] += reward; + } + } + + foreach (var reward in rewardMap) + { + if (reward.Key.Equals(CrystalCalculator.CRYSTAL)) + { + Assert.Equal(reward.Value, nextState.GetBalance(_agentAddress, reward.Key)); + } + else + { + Assert.Equal(reward.Value, nextState.GetBalance(_avatarAddress, reward.Key)); + } + } + + Assert.Equal(1, nextRaiderState.Level); + Assert.Equal(GameConfig.DefaultAvatarArmorId, nextRaiderState.IconId); + Assert.True(nextRaiderState.Cp > 0); + Assert.Equal(3, nextRaiderState.LatestBossLevel); + Assert.True(nextState.TryGetLegacyState(bossAddress, out List rawBoss)); + var nextBossState = new WorldBossState(rawBoss); + Assert.Equal(3, nextBossState.Level); + Assert.True(nextState.TryGetLegacyState(worldBossKillRewardRecordAddress, out List rawRewardInfo)); + var nextRewardInfo = new WorldBossKillRewardRecord(rawRewardInfo); + Assert.True(nextRewardInfo[1]); + } + + [Fact] + public void Execute_With_Free_Crystal_Fee() + { + var action = new Raid + { + AvatarAddress = _avatarAddress, + EquipmentIds = new List(), + CostumeIds = new List(), + FoodIds = new List(), + RuneInfos = new List(), + PayNcg = false, + }; + Currency crystal = CrystalCalculator.CRYSTAL; + + _sheets[nameof(WorldBossListSheet)] = + "id,boss_id,started_block_index,ended_block_index,fee,ticket_price,additional_ticket_price,max_purchase_count\r\n" + + "1,900002,0,100,0,1,1,40"; + + var goldCurrencyState = new GoldCurrencyState(_goldCurrency); + IWorld state = new World(new MockWorldState()) + .SetLegacyState(goldCurrencyState.address, goldCurrencyState.Serialize()) + .SetAgentState(_agentAddress, new AgentState(_agentAddress)); + + foreach (var (key, value) in _sheets) + { + state = state.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + var gameConfigState = new GameConfigState(_sheets[nameof(GameConfigSheet)]); + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + gameConfigState, + default + ); + + for (int i = 0; i < 50; i++) + { + avatarState.worldInformation.ClearStage(1, i + 1, 0, _tableSheets.WorldSheet, _tableSheets.WorldUnlockSheet); + } + + state = state + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(gameConfigState.address, gameConfigState.Serialize()); + + var blockIndex = gameConfigState.WorldBossRequiredInterval; + var randomSeed = 0; + var ctx = new ActionContext + { + BlockIndex = blockIndex, + PreviousState = state, + RandomSeed = randomSeed, + Signer = _agentAddress, + }; + action.Execute(ctx); + } + } +} diff --git a/.Lib9c.Tests/Action/RapidCombinationTest.cs b/.Lib9c.Tests/Action/RapidCombinationTest.cs new file mode 100644 index 0000000000..fa6976dbcf --- /dev/null +++ b/.Lib9c.Tests/Action/RapidCombinationTest.cs @@ -0,0 +1,615 @@ +namespace Lib9c.Tests.Action +{ + using System; + using System.Collections.Generic; + using System.Collections.Immutable; + using System.Globalization; + using System.Linq; + using Bencodex.Types; + using Lib9c.Tests.Fixtures.TableCSV; + using Lib9c.Tests.Fixtures.TableCSV.Item; + using Lib9c.Tests.Util; + using Libplanet.Action; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Helper; + using Nekoyume.Model; + using Nekoyume.Model.Item; + using Nekoyume.Model.Mail; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Nekoyume.TableData; + using Xunit; + using static Lib9c.SerializeKeys; + + public class RapidCombinationTest + { + private readonly IWorld _initialState; + + private readonly TableSheets _tableSheets; + + private readonly Address _agentAddress; + private readonly Address _avatarAddress; + + public RapidCombinationTest() + { + _initialState = new World(new MockWorldState()); + Dictionary sheets; + (_initialState, sheets) = InitializeUtil.InitializeTableSheets( + _initialState, + sheetsOverride: new Dictionary + { + { + "EquipmentItemRecipeSheet", + EquipmentItemRecipeSheetFixtures.Default + }, + { + "EquipmentItemSubRecipeSheet", + EquipmentItemSubRecipeSheetFixtures.V1 + }, + { + "GameConfigSheet", + GameConfigSheetFixtures.Default + }, + }); + _tableSheets = new TableSheets(sheets); + foreach (var (key, value) in sheets) + { + _initialState = + _initialState.SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + _agentAddress = new PrivateKey().Address; + var agentState = new AgentState(_agentAddress); + + _avatarAddress = new PrivateKey().Address; + var avatarState = new AvatarState( + _avatarAddress, + _agentAddress, + 0, + _tableSheets.GetAvatarSheets(), + new GameConfigState(), + default + ); + + agentState.avatarAddresses[0] = _avatarAddress; + + _initialState = _initialState + .SetLegacyState(Addresses.GameConfig, new GameConfigState(sheets[nameof(GameConfigSheet)]).Serialize()) + .SetAgentState(_agentAddress, agentState) + .SetAvatarState(_avatarAddress, avatarState); + } + + [Fact] + public void Execute() + { + const int slotStateUnlockStage = 1; + + var avatarState = _initialState.GetAvatarState(_avatarAddress); + avatarState.worldInformation = new WorldInformation( + 0, + _initialState.GetSheet(), + slotStateUnlockStage); + + var row = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.Hourglass); + avatarState.inventory.AddItem(ItemFactory.CreateMaterial(row), 83); + avatarState.inventory.AddItem(ItemFactory.CreateTradableMaterial(row), 100); + Assert.True(avatarState.inventory.HasFungibleItem(row.ItemId, 0, 183)); + + var firstEquipmentRow = _tableSheets.EquipmentItemSheet.First; + Assert.NotNull(firstEquipmentRow); + + var gameConfigState = _initialState.GetGameConfigState(); + var requiredBlockIndex = gameConfigState.HourglassPerBlock * 200; + var equipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + requiredBlockIndex); + avatarState.inventory.AddItem(equipment); + + var result = new CombinationConsumable5.ResultModel + { + actionPoint = 0, + gold = 0, + materials = new Dictionary(), + itemUsable = equipment, + recipeId = 0, + itemType = ItemType.Equipment, + }; + + var mail = new CombinationMail(result, 0, default, requiredBlockIndex); + result.id = mail.id; + avatarState.Update2(mail); + + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, slotStateUnlockStage); + slotState.Update(result, 0, requiredBlockIndex); + + var tempState = _initialState + .SetLegacyState(slotAddress, slotState.Serialize()) + .SetAvatarState(_avatarAddress, avatarState); + + var action = new RapidCombination + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + var nextState = action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = 51, + }); + + var nextAvatarState = nextState.GetAvatarState(_avatarAddress); + var item = nextAvatarState.inventory.Equipments.First(); + + Assert.Empty(nextAvatarState.inventory.Materials.Select(r => r.ItemSubType == ItemSubType.Hourglass)); + Assert.Equal(equipment.ItemId, item.ItemId); + Assert.Equal(51, item.RequiredBlockIndex); + } + + [Fact] + public void Execute_Throw_CombinationSlotResultNullException() + { + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, 0); + slotState.Update(null, 0, 0); + + var tempState = _initialState + .SetLegacyState(slotAddress, slotState.Serialize()); + + var action = new RapidCombination + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = 1, + })); + } + + [Theory] + [InlineData(0, 0)] + [InlineData(10, 100)] + public void Execute_Throw_RequiredBlockIndexException(int itemRequiredBlockIndex, int contextBlockIndex) + { + const int avatarClearedStage = 1; + + var avatarState = _initialState.GetAvatarState(_avatarAddress); + avatarState.worldInformation = new WorldInformation( + 0, + _initialState.GetSheet(), + avatarClearedStage); + + var firstEquipmentRow = _tableSheets.EquipmentItemSheet.First; + Assert.NotNull(firstEquipmentRow); + + var equipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + itemRequiredBlockIndex); + + var result = new CombinationConsumable5.ResultModel + { + actionPoint = 0, + gold = 0, + materials = new Dictionary(), + itemUsable = equipment, + recipeId = 0, + itemType = ItemType.Equipment, + }; + + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, avatarClearedStage); + slotState.Update(result, 0, 0); + + var tempState = _initialState + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(slotAddress, slotState.Serialize()); + + var action = new RapidCombination + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = contextBlockIndex, + })); + } + + [Theory] + [InlineData(0, 0, 0, 40)] + [InlineData(0, 1, 2, 40)] + [InlineData(22, 0, 0, 40)] + [InlineData(0, 22, 0, 40)] + [InlineData(0, 22, 2, 40)] + [InlineData(2, 10, 2, 40)] + public void Execute_Throw_NotEnoughMaterialException(int materialCount, int tradableCount, long blockIndex, int requiredCount) + { + const int slotStateUnlockStage = 1; + + var avatarState = _initialState.GetAvatarState(_avatarAddress); + avatarState.worldInformation = new WorldInformation( + 0, + _initialState.GetSheet(), + slotStateUnlockStage); + + var row = _tableSheets.MaterialItemSheet.Values.First(r => r.ItemSubType == ItemSubType.Hourglass); + avatarState.inventory.AddItem(ItemFactory.CreateMaterial(row), count: materialCount); + if (tradableCount > 0) + { + var material = ItemFactory.CreateTradableMaterial(row); + material.RequiredBlockIndex = blockIndex; + avatarState.inventory.AddItem(material, count: tradableCount); + } + + var firstEquipmentRow = _tableSheets.EquipmentItemSheet.First; + Assert.NotNull(firstEquipmentRow); + + var gameConfigState = _initialState.GetGameConfigState(); + var requiredBlockIndex = gameConfigState.HourglassPerBlock * requiredCount; + var equipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + requiredBlockIndex); + avatarState.inventory.AddItem(equipment); + + var result = new CombinationConsumable5.ResultModel + { + actionPoint = 0, + gold = 0, + materials = new Dictionary(), + itemUsable = equipment, + recipeId = 0, + itemType = ItemType.Equipment, + }; + + var mail = new CombinationMail(result, 0, default, requiredBlockIndex); + result.id = mail.id; + avatarState.Update2(mail); + + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, slotStateUnlockStage); + slotState.Update(result, 0, 0); + + var tempState = _initialState + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(slotAddress, slotState.Serialize()); + + var action = new RapidCombination + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = 51, + })); + } + + [Theory] + [InlineData(null)] + [InlineData(1)] + public void ResultModelDeterministic(int? subRecipeId) + { + var row = _tableSheets.MaterialItemSheet.Values.First(); + var row2 = _tableSheets.MaterialItemSheet.Values.Last(); + + Assert.True(row.Id < row2.Id); + + var material = ItemFactory.CreateMaterial(row); + var material2 = ItemFactory.CreateMaterial(row2); + + var itemUsable = ItemFactory.CreateItemUsable(_tableSheets.EquipmentItemSheet.Values.First(), default, 0); + var r = new CombinationConsumable5.ResultModel + { + id = default, + gold = 0, + actionPoint = 0, + recipeId = 1, + subRecipeId = subRecipeId, + materials = new Dictionary + { + [material] = 1, + [material2] = 1, + }, + itemUsable = itemUsable, + }; + var result = new RapidCombination0.ResultModel((Dictionary)r.Serialize()) + { + cost = new Dictionary + { + [material] = 1, + [material2] = 1, + }, + }; + + var r2 = new CombinationConsumable5.ResultModel + { + id = default, + gold = 0, + actionPoint = 0, + recipeId = 1, + subRecipeId = subRecipeId, + materials = new Dictionary + { + [material2] = 1, + [material] = 1, + }, + itemUsable = itemUsable, + }; + + var result2 = new RapidCombination0.ResultModel((Dictionary)r2.Serialize()) + { + cost = new Dictionary + { + [material2] = 1, + [material] = 1, + }, + }; + + Assert.Equal(result.Serialize(), result2.Serialize()); + } + + [Fact] + public void Execute_Throw_RequiredAppraiseBlockException() + { + const int slotStateUnlockStage = 1; + + var avatarState = _initialState.GetAvatarState(_avatarAddress); + avatarState.worldInformation = new WorldInformation( + 0, + _initialState.GetSheet(), + slotStateUnlockStage); + + var row = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.Hourglass); + avatarState.inventory.AddItem(ItemFactory.CreateMaterial(row), count: 22); + + var firstEquipmentRow = _tableSheets.EquipmentItemSheet.First; + Assert.NotNull(firstEquipmentRow); + + var gameConfigState = _initialState.GetGameConfigState(); + var requiredBlockIndex = gameConfigState.HourglassPerBlock * 40; + var equipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + requiredBlockIndex); + avatarState.inventory.AddItem(equipment); + + var result = new CombinationConsumable5.ResultModel + { + actionPoint = 0, + gold = 0, + materials = new Dictionary(), + itemUsable = equipment, + recipeId = 0, + itemType = ItemType.Equipment, + }; + + var mail = new CombinationMail(result, 0, default, requiredBlockIndex); + result.id = mail.id; + avatarState.Update(mail); + + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, slotStateUnlockStage); + slotState.Update(result, 0, 0); + + var tempState = _initialState + .SetAvatarState(_avatarAddress, avatarState) + .SetLegacyState(slotAddress, slotState.Serialize()); + + var action = new RapidCombination + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + Assert.Throws(() => action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = 1, + })); + } + + [Theory] + [InlineData(7)] + [InlineData(9)] + [InlineData(10)] + [InlineData(11)] + public void Execute_NotThrow_InvalidOperationException_When_TargetSlotCreatedBy( + int itemEnhancementResultModelNumber) + { + const int slotStateUnlockStage = 1; + + var avatarState = _initialState.GetAvatarState(_avatarAddress); + avatarState.worldInformation = new WorldInformation( + 0, + _initialState.GetSheet(), + slotStateUnlockStage); + + var row = _tableSheets.MaterialItemSheet.Values.First(r => + r.ItemSubType == ItemSubType.Hourglass); + avatarState.inventory.AddItem(ItemFactory.CreateMaterial(row), 83); + avatarState.inventory.AddItem(ItemFactory.CreateTradableMaterial(row), 100); + Assert.True(avatarState.inventory.HasFungibleItem(row.ItemId, 0, 183)); + + var firstEquipmentRow = _tableSheets.EquipmentItemSheet + .OrderedList.First(e => e.Grade >= 1); + Assert.NotNull(firstEquipmentRow); + + var gameConfigState = _initialState.GetGameConfigState(); + var requiredBlockIndex = gameConfigState.HourglassPerBlock * 200; + var equipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + requiredBlockIndex); + var materialEquipment = (Equipment)ItemFactory.CreateItemUsable( + firstEquipmentRow, + Guid.NewGuid(), + requiredBlockIndex); + avatarState.inventory.AddItem(equipment); + avatarState.inventory.AddItem(materialEquipment); + + AttachmentActionResult resultModel = null; + var random = new TestRandom(); + var mailId = random.GenerateRandomGuid(); + var preItemUsable = new Equipment((Dictionary)equipment.Serialize()); + switch (itemEnhancementResultModelNumber) + { + case 7: + { + equipment = ItemEnhancement7.UpgradeEquipment(equipment); + resultModel = new ItemEnhancement7.ResultModel + { + id = mailId, + itemUsable = equipment, + materialItemIdList = new[] { materialEquipment.NonFungibleId }, + }; + + break; + } + + case 9: + { + Assert.True(ItemEnhancement9.TryGetRow( + equipment, + _tableSheets.EnhancementCostSheetV2, + out var costRow)); + var equipmentResult = ItemEnhancement9.GetEnhancementResult(costRow, random); + equipment.LevelUp( + random, + costRow, + equipmentResult == ItemEnhancement9.EnhancementResult.GreatSuccess); + resultModel = new ItemEnhancement9.ResultModel + { + id = mailId, + preItemUsable = preItemUsable, + itemUsable = equipment, + materialItemIdList = new[] { materialEquipment.NonFungibleId }, + gold = 0, + actionPoint = 0, + enhancementResult = ItemEnhancement9.EnhancementResult.GreatSuccess, + }; + + break; + } + + case 10: + { + Assert.True(ItemEnhancement10.TryGetRow( + equipment, + _tableSheets.EnhancementCostSheetV2, + out var costRow)); + var equipmentResult = ItemEnhancement10.GetEnhancementResult(costRow, random); + equipment.LevelUp( + random, + costRow, + equipmentResult == ItemEnhancement10.EnhancementResult.GreatSuccess); + resultModel = new ItemEnhancement10.ResultModel + { + id = mailId, + preItemUsable = preItemUsable, + itemUsable = equipment, + materialItemIdList = new[] { materialEquipment.NonFungibleId }, + gold = 0, + actionPoint = 0, + enhancementResult = ItemEnhancement10.EnhancementResult.GreatSuccess, + }; + + break; + } + + case 11: + { + Assert.True(ItemEnhancement11.TryGetRow( + equipment, + _tableSheets.EnhancementCostSheetV2, + out var costRow)); + var equipmentResult = ItemEnhancement11.GetEnhancementResult(costRow, random); + equipment.LevelUp( + random, + costRow, + equipmentResult == ItemEnhancement11.EnhancementResult.GreatSuccess); + resultModel = new ItemEnhancement11.ResultModel + { + id = mailId, + preItemUsable = preItemUsable, + itemUsable = equipment, + materialItemIdList = new[] { materialEquipment.NonFungibleId }, + gold = 0, + actionPoint = 0, + enhancementResult = ItemEnhancement11.EnhancementResult.GreatSuccess, + CRYSTAL = 0 * CrystalCalculator.CRYSTAL, + }; + + break; + } + + default: + break; + } + + // NOTE: Do not update `mail`, because this test assumes that the `mail` was removed. + { + // var mail = new ItemEnhanceMail(resultModel, 0, random.GenerateRandomGuid(), requiredBlockIndex); + // avatarState.Update(mail); + } + + var slotAddress = _avatarAddress.Derive(string.Format( + CultureInfo.InvariantCulture, + CombinationSlotState.DeriveFormat, + 0)); + var slotState = new CombinationSlotState(slotAddress, slotStateUnlockStage); + slotState.Update(resultModel, 0, requiredBlockIndex); + + var tempState = _initialState.SetLegacyState(slotAddress, slotState.Serialize()) + .SetAvatarState(_avatarAddress, avatarState); + + var action = new RapidCombination + { + avatarAddress = _avatarAddress, + slotIndex = 0, + }; + + action.Execute(new ActionContext + { + PreviousState = tempState, + Signer = _agentAddress, + BlockIndex = 51, + }); + } + } +} diff --git a/.Lib9c.Tests/Model/Skill/Arena/ArenaCombatTest.cs b/.Lib9c.Tests/Model/Skill/Arena/ArenaCombatTest.cs index 97307544db..1cee8a57dd 100644 --- a/.Lib9c.Tests/Model/Skill/Arena/ArenaCombatTest.cs +++ b/.Lib9c.Tests/Model/Skill/Arena/ArenaCombatTest.cs @@ -6,6 +6,7 @@ namespace Lib9c.Tests.Model.Skill.Arena using Nekoyume.Arena; using Nekoyume.Model; using Nekoyume.Model.Buff; + using Nekoyume.Model.Skill; using Nekoyume.Model.Skill.Arena; using Nekoyume.Model.Stat; using Nekoyume.Model.State; @@ -13,8 +14,6 @@ namespace Lib9c.Tests.Model.Skill.Arena public class ArenaCombatTest { - private const int ActionBuffId = 708000; // Dispel with duration - private readonly TableSheets _tableSheets; private readonly AvatarState _avatar1; private readonly AvatarState _avatar2; @@ -48,7 +47,7 @@ public ArenaCombatTest() [Theory] [InlineData(700009, new[] { 600001 })] - [InlineData(700010, new[] { 600001, 704000 })] + [InlineData(700009, new[] { 600001, 704000 })] public void DispelOnUse(int dispelId, int[] debuffIdList) { var arenaSheets = _tableSheets.GetArenaSimulatorSheets(); @@ -122,7 +121,7 @@ public void DispelOnDuration_Block() ); // Use Dispel first - var dispel = _tableSheets.ActionBuffSheet.Values.First(bf => bf.Id == ActionBuffId); + var dispel = _tableSheets.ActionBuffSheet.Values.First(bf => bf.ActionBuffType == ActionBuffType.Dispel); challenger.AddBuff(BuffFactory.GetActionBuff(challenger.Stats, dispel)); Assert.Single(challenger.Buffs); @@ -170,7 +169,7 @@ public void DispelOnDuration_Affect() ); // Use Dispel first - var dispel = _tableSheets.ActionBuffSheet.Values.First(bf => bf.Id == ActionBuffId); + var dispel = _tableSheets.ActionBuffSheet.Values.First(bf => bf.ActionBuffType == ActionBuffType.Dispel); challenger.AddBuff(BuffFactory.GetActionBuff(challenger.Stats, dispel)); Assert.Single(challenger.Buffs); diff --git a/.Lib9c.Tests/Model/Skill/Arena/ArenaShatterStrikeTest.cs b/.Lib9c.Tests/Model/Skill/Arena/ArenaShatterStrikeTest.cs index 4aaec7502b..c616f3e393 100644 --- a/.Lib9c.Tests/Model/Skill/Arena/ArenaShatterStrikeTest.cs +++ b/.Lib9c.Tests/Model/Skill/Arena/ArenaShatterStrikeTest.cs @@ -73,7 +73,7 @@ public void Use(int ratioBp) new List() ); - var skillRow = _tableSheets.SkillSheet.OrderedList.First(s => s.Id == 700011); + var skillRow = _tableSheets.SkillSheet.OrderedList.First(s => s.Id == 700010); var shatterStrike = new ArenaShatterStrike(skillRow, 0, 0, ratioBp, StatType.NONE); var used = shatterStrike.Use(challenger, enemy, simulator.Turn, new List()); Assert.Single(used.SkillInfos); diff --git a/.Lib9c.Tests/Model/Skill/CombatTest.cs b/.Lib9c.Tests/Model/Skill/CombatTest.cs index 652f03b4b2..b01d7fce99 100644 --- a/.Lib9c.Tests/Model/Skill/CombatTest.cs +++ b/.Lib9c.Tests/Model/Skill/CombatTest.cs @@ -139,7 +139,7 @@ public void Bleed() [Theory] [InlineData(700009, new[] { 600001 })] - [InlineData(700010, new[] { 600001, 704000 })] + [InlineData(700009, new[] { 600001, 704000 })] public void DispelOnUse(int dispelId, int[] debuffIdList) { var actionBuffSheet = _tableSheets.ActionBuffSheet; @@ -182,11 +182,10 @@ public void DispelOnUse(int dispelId, int[] debuffIdList) [Fact] public void DispelOnDuration_Block() { - const int actionBuffId = 708000; // Dispel with duration var actionBuffSheet = _tableSheets.ActionBuffSheet; // Use Dispel first - var dispel = actionBuffSheet.Values.First(bf => bf.Id == actionBuffId); + var dispel = actionBuffSheet.Values.First(bf => bf.ActionBuffType == ActionBuffType.Dispel); _player.AddBuff(BuffFactory.GetActionBuff(_player.Stats, dispel)); Assert.Single(_player.Buffs); @@ -217,11 +216,10 @@ public void DispelOnDuration_Block() [Fact] public void DispelOnDuration_Affect() { - const int actionBuffId = 708000; // Dispel with duration var actionBuffSheet = _tableSheets.ActionBuffSheet; // Use Dispel first - var dispel = actionBuffSheet.Values.First(bf => bf.Id == actionBuffId); + var dispel = actionBuffSheet.Values.First(bf => bf.ActionBuffType == ActionBuffType.Dispel); _player.AddBuff(BuffFactory.GetActionBuff(_player.Stats, dispel)); Assert.Single(_player.Buffs); diff --git a/.Lib9c.Tests/Model/Skill/ShatterStrikeTest.cs b/.Lib9c.Tests/Model/Skill/ShatterStrikeTest.cs index 190ce2540c..142a08efbb 100644 --- a/.Lib9c.Tests/Model/Skill/ShatterStrikeTest.cs +++ b/.Lib9c.Tests/Model/Skill/ShatterStrikeTest.cs @@ -30,7 +30,7 @@ public class ShatterStrikeTest public void Use(int ratioBp, bool copyCharacter) { Assert.True( - _tableSheets.SkillSheet.TryGetValue(700011, out var skillRow) + _tableSheets.SkillSheet.TryGetValue(700010, out var skillRow) ); // 700011 is ShatterStrike var shatterStrike = new ShatterStrike(skillRow, 0, 0, ratioBp, StatType.NONE); diff --git a/.Libplanet b/.Libplanet index 1016fbce88..96970ec83a 160000 --- a/.Libplanet +++ b/.Libplanet @@ -1 +1 @@ -Subproject commit 1016fbce882309452a45eda1a19c9a8b213801a5 +Subproject commit 96970ec83a91f1590560b82834f8cb64518754a8 diff --git a/Lib9c.DPoS.Tests/ActionContext.cs b/Lib9c.DPoS.Tests/ActionContext.cs new file mode 100644 index 0000000000..7bb511d6f9 --- /dev/null +++ b/Lib9c.DPoS.Tests/ActionContext.cs @@ -0,0 +1,60 @@ +#nullable disable + +using System.Security.Cryptography; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Types.Blocks; +using Libplanet.Types.Tx; + +namespace Lib9c.DPoS.Tests +{ + public class ActionContext : IActionContext + { + private long _gasUsed; + + private IRandom _random = null; + + public BlockHash? GenesisHash { get; set; } + + public Address Signer { get; set; } + + public TxId? TxId { get; set; } + + public Address Miner { get; set; } + + public BlockHash BlockHash { get; set; } + + public long BlockIndex { get; set; } + + 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 void UseGas(long gas) + { + _gasUsed += gas; + } + + public IRandom GetRandom() => _random ?? new TestRandom(RandomSeed); + + public long GasUsed() => _gasUsed; + + public long GasLimit() => 0; + + // FIXME: Temporary measure to allow inheriting already mutated IRandom. + public void SetRandom(IRandom random) + { + _random = random; + } + } +} diff --git a/Lib9c.DPoS.Tests/Control/DelegateCtrlTest.cs b/Lib9c.DPoS.Tests/Control/DelegateCtrlTest.cs new file mode 100644 index 0000000000..d0fff28aaf --- /dev/null +++ b/Lib9c.DPoS.Tests/Control/DelegateCtrlTest.cs @@ -0,0 +1,193 @@ +using System.Collections.Immutable; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; +using Xunit; + +namespace Lib9c.DPoS.Tests.Control +{ + 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.ConsensusToken * 50); + Assert.Throws( + () => _states = DelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.ConsensusToken * 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.ConsensusToken * 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.ConsensusToken * 0, + _states.GetBalance(_operatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.ConsensusToken * 0, + _states.GetBalance(_delegatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.Share * 0, + _states.GetBalance(_operatorAddress, Asset.Share)); + Assert.Equal( + Asset.Share * 0, + _states.GetBalance(_delegatorAddress, Asset.Share)); + Assert.Equal( + Asset.ConsensusToken * (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)); + Assert.Equal( + ValidatorCtrl.GetValidator(_states, _validatorAddress)!.DelegatorShares, + _states.GetBalance( + Delegation.DeriveAddress(_operatorAddress, _validatorAddress), Asset.Share) + + _states.GetBalance( + Delegation.DeriveAddress(_delegatorAddress, _validatorAddress), Asset.Share)); + } + + 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.DPoS.Tests/Control/RedelegateCtrlTest.cs b/Lib9c.DPoS.Tests/Control/RedelegateCtrlTest.cs new file mode 100644 index 0000000000..862f2c0575 --- /dev/null +++ b/Lib9c.DPoS.Tests/Control/RedelegateCtrlTest.cs @@ -0,0 +1,375 @@ +using System.Collections.Immutable; +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; +using Xunit; + +namespace Lib9c.DPoS.Tests.Control +{ + 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.ConsensusToken * 50); + Assert.Throws( + () => _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + _dstValidatorAddress, + Asset.ConsensusToken * 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, + Asset.Share * 10, + _nativeTokens)); + Assert.Throws( + () => _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + CreateAddress(), + Asset.Share * 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, + Asset.Share * 1, + _nativeTokens); + } + + Assert.Throws( + () => _states = RedelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _srcValidatorAddress, + _dstValidatorAddress, + Asset.Share * 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, + Asset.Share * 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.ConsensusToken * 0, + _states.GetBalance(_srcOperatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.ConsensusToken * 0, + _states.GetBalance(_dstOperatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.ConsensusToken * 0, + _states.GetBalance(_delegatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.Share * 0, + _states.GetBalance(_srcOperatorAddress, Asset.Share)); + Assert.Equal( + Asset.Share * 0, + _states.GetBalance(_dstOperatorAddress, Asset.Share)); + Assert.Equal( + Asset.Share * 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)); + Assert.Equal( + ValidatorCtrl.GetValidator(_states, _srcValidatorAddress)!.DelegatorShares, + _states.GetBalance( + Delegation.DeriveAddress( + _srcOperatorAddress, _srcValidatorAddress), Asset.Share) + + _states.GetBalance( + Delegation.DeriveAddress( + _delegatorAddress, _srcValidatorAddress), Asset.Share)); + Assert.Equal( + ValidatorCtrl.GetValidator(_states, _dstValidatorAddress)!.DelegatorShares, + _states.GetBalance( + Delegation.DeriveAddress( + _dstOperatorAddress, _dstValidatorAddress), Asset.Share) + + _states.GetBalance( + Delegation.DeriveAddress( + _delegatorAddress, _dstValidatorAddress), Asset.Share)); + RedelegationEntry entry = new RedelegationEntry( + _states.GetDPoSState( + RedelegateCtrl.GetRedelegation(_states, _redelegationAddress)! + .RedelegationEntryAddresses[0])!); + Assert.Equal( + Asset.ConsensusToken * (selfDelegateAmount + delegateAmount) + - entry.UnbondingConsensusToken, + _states.GetBalance(_srcValidatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.ConsensusToken * 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.DPoS.Tests/Control/UndelegateCtrlTest.cs b/Lib9c.DPoS.Tests/Control/UndelegateCtrlTest.cs new file mode 100644 index 0000000000..4bfb53041a --- /dev/null +++ b/Lib9c.DPoS.Tests/Control/UndelegateCtrlTest.cs @@ -0,0 +1,393 @@ +using System.Collections.Immutable; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; +using Xunit; + +namespace Lib9c.DPoS.Tests.Control +{ + 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.ConsensusToken * 50); + Assert.Throws( + () => _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.ConsensusToken * 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(), + Asset.Share * 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, + Asset.Share * 1, + _nativeTokens); + } + + Assert.Throws( + () => _states = UndelegateCtrl.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _delegatorAddress, + _validatorAddress, + Asset.Share * 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, + Asset.Share * 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, + Asset.Share * 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, + Asset.Share * undelegateAmount, + _nativeTokens); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.ConsensusToken * (selfDelegateAmount + delegateAmount - undelegateAmount), + _states.GetBalance(_validatorAddress, Asset.ConsensusToken)); + _states = UndelegateCtrl.Cancel( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 2, + }, + _undelegationAddress, + Asset.ConsensusToken * cancelAmount, + _nativeTokens); + Assert.Equal( + Asset.GovernanceToken * (delegatorMintAmount - delegateAmount), + _states.GetBalance(_delegatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.ConsensusToken + * (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.ConsensusToken + * (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.ConsensusToken + * (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, + Asset.Share * undelegateAmount, + _nativeTokens); + Assert.Equal( + Asset.GovernanceToken * 0, + _states.GetBalance(_validatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.ConsensusToken * 0, + _states.GetBalance(_operatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.ConsensusToken * 0, + _states.GetBalance(_delegatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.Share * 0, + _states.GetBalance(_operatorAddress, Asset.Share)); + Assert.Equal( + Asset.Share * 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)); + Assert.Equal( + ValidatorCtrl.GetValidator(_states, _validatorAddress)!.DelegatorShares, + _states.GetBalance( + Delegation.DeriveAddress( + _operatorAddress, _validatorAddress), Asset.Share) + + _states.GetBalance( + Delegation.DeriveAddress( + _delegatorAddress, _validatorAddress), Asset.Share)); + Assert.Equal( + Asset.ConsensusToken * (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.DPoS.Tests/Control/ValidatorCtrlTest.cs b/Lib9c.DPoS.Tests/Control/ValidatorCtrlTest.cs new file mode 100644 index 0000000000..6a5a548024 --- /dev/null +++ b/Lib9c.DPoS.Tests/Control/ValidatorCtrlTest.cs @@ -0,0 +1,124 @@ +using System.Collections.Immutable; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; +using Xunit; + +namespace Lib9c.DPoS.Tests.Control +{ + public class ValidatorCtrlTest : PoSTest + { + private readonly PublicKey _operatorPublicKey; + private readonly Address _operatorAddress; + private readonly Address _validatorAddress; + private readonly ImmutableHashSet _nativeTokens; + 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.ConsensusToken * 50); + Assert.Throws( + () => _states = ValidatorCtrl.Create( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _operatorAddress, + _operatorPublicKey, + Asset.ConsensusToken * 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.ConsensusToken * selfDelegateAmount, + _states.GetBalance(_validatorAddress, Asset.ConsensusToken)); + Assert.Equal( + Asset.GovernanceToken * (mintAmount - selfDelegateAmount), + _states.GetBalance(_operatorAddress, Asset.GovernanceToken)); + Assert.Equal( + Asset.Share * selfDelegateAmount, + _states.GetBalance( + Delegation.DeriveAddress(_operatorAddress, _validatorAddress), Asset.Share)); + Assert.Equal( + Asset.Share * selfDelegateAmount, + ValidatorCtrl.GetValidator(_states, _validatorAddress)!.DelegatorShares); + } + } +} diff --git a/Lib9c.DPoS.Tests/Control/ValidatorPowerIndexCtrlTest.cs b/Lib9c.DPoS.Tests/Control/ValidatorPowerIndexCtrlTest.cs new file mode 100644 index 0000000000..8929a66683 --- /dev/null +++ b/Lib9c.DPoS.Tests/Control/ValidatorPowerIndexCtrlTest.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; +using Xunit; + +namespace Lib9c.DPoS.Tests.Control +{ + 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.ConsensusToken * 10); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[1], + Asset.ConsensusToken * 30); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[2], + Asset.ConsensusToken * 50); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[3], + Asset.ConsensusToken * 40); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[4], + Asset.ConsensusToken * 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.ConsensusToken * 60, index[0].ConsensusToken); + Assert.Equal(ValidatorAddresses[3], index[1].ValidatorAddress); + Assert.Equal(Asset.ConsensusToken * 50, index[1].ConsensusToken); + Assert.Equal(ValidatorAddresses[1], index[2].ValidatorAddress); + Assert.Equal(Asset.ConsensusToken * 40, index[2].ConsensusToken); + Assert.Equal(ValidatorAddresses[4], index[3].ValidatorAddress); + Assert.Equal(Asset.ConsensusToken * 30, index[3].ConsensusToken); + Assert.Equal(ValidatorAddresses[0], index[4].ValidatorAddress); + Assert.Equal(Asset.ConsensusToken * 20, index[4].ConsensusToken); + } + + [Fact] + public void SortingTestSameToken() + { + (_states, _) = ValidatorPowerIndexCtrl.FetchValidatorPowerIndex(_states); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[0], + Asset.ConsensusToken * 10); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[1], + Asset.ConsensusToken * 10); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[2], + Asset.ConsensusToken * 10); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[3], + Asset.ConsensusToken * 10); + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ValidatorAddresses[4], + Asset.ConsensusToken * 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.DPoS.Tests/Control/ValidatorSetCtrlTest.cs b/Lib9c.DPoS.Tests/Control/ValidatorSetCtrlTest.cs new file mode 100644 index 0000000000..e8a9055f80 --- /dev/null +++ b/Lib9c.DPoS.Tests/Control/ValidatorSetCtrlTest.cs @@ -0,0 +1,221 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; +using Xunit; + +namespace Lib9c.DPoS.Tests.Control +{ + 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( + Asset.Share * (5 + 1 + 300), + _states.GetBalance(delegationAddressB, Asset.Share)); + Assert.Equal( + Asset.ConsensusToken * (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( + Asset.Share * 0, + _states.GetBalance(delegationAddressB, Asset.Share)); + Assert.Equal( + Asset.ConsensusToken * 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.DPoS.Tests/DistributeTest.cs b/Lib9c.DPoS.Tests/DistributeTest.cs new file mode 100644 index 0000000000..9c50484e87 --- /dev/null +++ b/Lib9c.DPoS.Tests/DistributeTest.cs @@ -0,0 +1,259 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Libplanet.Types.Consensus; +using Nekoyume.Module; +using Xunit; +using Validator = Lib9c.DPoS.Model.Validator; + +namespace Lib9c.DPoS.Tests +{ + 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, + }); + + (_states, _) = ValidatorSetCtrl.FetchBondedValidatorSet(_states); + + List votes = new List() + { + new VoteMetadata( + default, + default, + default, + default, + OperatorPrivateKeys[3].PublicKey, + VoteFlag.PreCommit).Sign(OperatorPrivateKeys[3]), + new VoteMetadata( + default, + default, + default, + default, + OperatorPrivateKeys[5].PublicKey, + VoteFlag.PreCommit).Sign(OperatorPrivateKeys[5]), + }; + FungibleAssetValue blockReward = Asset.ConsensusToken * 50; + _states = _states.MintAsset( + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + ReservedAddress.RewardPool, + blockReward); + _states = AllocateReward.Execute( + _states, + new ActionContext + { + PreviousState = _states, + BlockIndex = 1, + }, + _nativeTokens, + votes, + OperatorAddresses[3]); + + var (baseProposerReward, _) + = (blockReward * AllocateReward.BaseProposerRewardNumer) + .DivRem(AllocateReward.BaseProposerRewardDenom); + var (bonusProposerReward, _) + = (blockReward * (205 + 307) + * AllocateReward.BonusProposerRewardNumer) + .DivRem((100 + (101 + 200) * 50 - 101 - 102 + 204 + 306) + * AllocateReward.BonusProposerRewardDenom); + 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.CommissionNumer) + .DivRem(Validator.CommissionDenom); + var (validatorRewardB, _) + = (validatorRewardSum * 307) + .DivRem(100 + (101 + 200) * 50 - 101 - 102 + 204 + 306); + var (commissionB, _) + = (validatorRewardB * Validator.CommissionNumer) + .DivRem(Validator.CommissionDenom); + + Assert.Equal( + Asset.ConsensusToken * 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.ConsensusToken * 205, + _states.GetBalance(validatorAddressA, Asset.ConsensusToken)); + + Assert.Equal( + Asset.ConsensusToken * 307, + _states.GetBalance(validatorAddressB, Asset.ConsensusToken)); + + Assert.Equal( + proposerReward + commissionA, + _states.GetBalance( + AllocateReward.RewardAddress(OperatorAddresses[3]), Asset.ConsensusToken)); + + Assert.Equal( + commissionB, + _states.GetBalance( + AllocateReward.RewardAddress(OperatorAddresses[5]), Asset.ConsensusToken)); + + Address delegationAddressA + = Delegation.DeriveAddress(DelegatorAddress, validatorAddressA); + + Assert.Equal( + Asset.ConsensusToken * 0, + _states.GetBalance( + AllocateReward.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( + AllocateReward.RewardAddress(DelegatorAddress), Asset.ConsensusToken)); + } + } +} diff --git a/Lib9c.DPoS.Tests/Lib9c.DPoS.Tests.csproj b/Lib9c.DPoS.Tests/Lib9c.DPoS.Tests.csproj new file mode 100644 index 0000000000..db2468e974 --- /dev/null +++ b/Lib9c.DPoS.Tests/Lib9c.DPoS.Tests.csproj @@ -0,0 +1,34 @@ + + + + net6.0 + 9.0 + false + false + true + true + $(NoWarn);CS0162;CS8032;CS0618;CS0612;SYSLIB0011 + enable + .\Lib9c.Tests.ruleset + Debug;Release;DevEx + AnyCPU + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/Lib9c.DPoS.Tests/Model/DelegationTest.cs b/Lib9c.DPoS.Tests/Model/DelegationTest.cs new file mode 100644 index 0000000000..5dea9496ab --- /dev/null +++ b/Lib9c.DPoS.Tests/Model/DelegationTest.cs @@ -0,0 +1,23 @@ +using Lib9c.DPoS.Model; +using Xunit; + +namespace Lib9c.DPoS.Tests.Model +{ + 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.DPoS.Tests/Model/RedelegationEntryTest.cs b/Lib9c.DPoS.Tests/Model/RedelegationEntryTest.cs new file mode 100644 index 0000000000..cb6ae49b25 --- /dev/null +++ b/Lib9c.DPoS.Tests/Model/RedelegationEntryTest.cs @@ -0,0 +1,48 @@ +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Xunit; + +namespace Lib9c.DPoS.Tests.Model +{ + public class RedelegationEntryTest : PoSTest + { + private readonly RedelegationEntry _redelegationEntry; + + public RedelegationEntryTest() + { + _redelegationEntry = new RedelegationEntry( + CreateAddress(), + Asset.Share * 1, + Asset.ConsensusToken * 1, + Asset.Share * 1, + 1, + 1); + } + + [Fact] + public void InvalidUnbondingConsensusToken() + { + Assert.Throws( + () => _redelegationEntry.RedelegatingShare = Asset.GovernanceToken * 1); + Assert.Throws( + () => _redelegationEntry.RedelegatingShare = Asset.ConsensusToken * 1); + Assert.Throws( + () => _redelegationEntry.UnbondingConsensusToken = Asset.GovernanceToken * 1); + Assert.Throws( + () => _redelegationEntry.UnbondingConsensusToken = Asset.Share * 1); + Assert.Throws( + () => _redelegationEntry.IssuedShare = Asset.GovernanceToken * 1); + Assert.Throws( + () => _redelegationEntry.IssuedShare = Asset.ConsensusToken * 1); + } + + [Fact] + public void MarshallingTest() + { + RedelegationEntry newRedelegationEntry + = new RedelegationEntry(_redelegationEntry.Serialize()); + Assert.Equal(_redelegationEntry, newRedelegationEntry); + } + } +} diff --git a/Lib9c.DPoS.Tests/Model/RedelegationTest.cs b/Lib9c.DPoS.Tests/Model/RedelegationTest.cs new file mode 100644 index 0000000000..21cee77b7c --- /dev/null +++ b/Lib9c.DPoS.Tests/Model/RedelegationTest.cs @@ -0,0 +1,24 @@ +using Lib9c.DPoS.Model; +using Xunit; + +namespace Lib9c.DPoS.Tests.Model +{ + 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.DPoS.Tests/Model/UndelegationEntryTest.cs b/Lib9c.DPoS.Tests/Model/UndelegationEntryTest.cs new file mode 100644 index 0000000000..16d5124510 --- /dev/null +++ b/Lib9c.DPoS.Tests/Model/UndelegationEntryTest.cs @@ -0,0 +1,35 @@ +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Xunit; + +namespace Lib9c.DPoS.Tests.Model +{ + public class UndelegationEntryTest : PoSTest + { + private readonly UndelegationEntry _undelegationEntry; + + public UndelegationEntryTest() + { + _undelegationEntry = new UndelegationEntry( + CreateAddress(), Asset.ConsensusToken * 1, 1, 1); + } + + [Fact] + public void InvalidUnbondingConsensusToken() + { + Assert.Throws( + () => _undelegationEntry.UnbondingConsensusToken = Asset.GovernanceToken * 1); + Assert.Throws( + () => _undelegationEntry.UnbondingConsensusToken = Asset.Share * 1); + } + + [Fact] + public void MarshallingTest() + { + UndelegationEntry newUndelegationEntry + = new UndelegationEntry(_undelegationEntry.Serialize()); + Assert.Equal(_undelegationEntry, newUndelegationEntry); + } + } +} diff --git a/Lib9c.DPoS.Tests/Model/UndelegationTest.cs b/Lib9c.DPoS.Tests/Model/UndelegationTest.cs new file mode 100644 index 0000000000..0806c58cf4 --- /dev/null +++ b/Lib9c.DPoS.Tests/Model/UndelegationTest.cs @@ -0,0 +1,23 @@ +using Lib9c.DPoS.Model; +using Xunit; + +namespace Lib9c.DPoS.Tests.Model +{ + 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.DPoS.Tests/Model/ValidatorPowerIndexTest.cs b/Lib9c.DPoS.Tests/Model/ValidatorPowerIndexTest.cs new file mode 100644 index 0000000000..acdd7a4983 --- /dev/null +++ b/Lib9c.DPoS.Tests/Model/ValidatorPowerIndexTest.cs @@ -0,0 +1,23 @@ +using Lib9c.DPoS.Model; +using Xunit; + +namespace Lib9c.DPoS.Tests.Model +{ + 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.DPoS.Tests/Model/ValidatorPowerTest.cs b/Lib9c.DPoS.Tests/Model/ValidatorPowerTest.cs new file mode 100644 index 0000000000..0149f97279 --- /dev/null +++ b/Lib9c.DPoS.Tests/Model/ValidatorPowerTest.cs @@ -0,0 +1,38 @@ +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Crypto; +using Xunit; + +namespace Lib9c.DPoS.Tests.Model +{ + public class ValidatorPowerTest : PoSTest + { + private readonly ValidatorPower _validatorPower; + + public ValidatorPowerTest() + { + _validatorPower = new ValidatorPower( + CreateAddress(), + new PrivateKey().PublicKey, + Asset.ConsensusToken * 10); + } + + [Fact] + public void InvalidUnbondingConsensusToken() + { + Assert.Throws( + () => _validatorPower.ConsensusToken = Asset.GovernanceToken * 1); + Assert.Throws( + () => _validatorPower.ConsensusToken = Asset.Share * 1); + } + + [Fact] + public void MarshallingTest() + { + ValidatorPower newValidatorPower = new ValidatorPower( + _validatorPower.Serialize()); + Assert.Equal(_validatorPower, newValidatorPower); + } + } +} diff --git a/Lib9c.DPoS.Tests/Model/ValidatorSetTest.cs b/Lib9c.DPoS.Tests/Model/ValidatorSetTest.cs new file mode 100644 index 0000000000..189a89996f --- /dev/null +++ b/Lib9c.DPoS.Tests/Model/ValidatorSetTest.cs @@ -0,0 +1,25 @@ +using Lib9c.DPoS.Model; +using Xunit; + +namespace Lib9c.DPoS.Tests.Model +{ + 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.DPoS.Tests/Model/ValidatorTest.cs b/Lib9c.DPoS.Tests/Model/ValidatorTest.cs new file mode 100644 index 0000000000..d700513315 --- /dev/null +++ b/Lib9c.DPoS.Tests/Model/ValidatorTest.cs @@ -0,0 +1,34 @@ +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Crypto; +using Xunit; + +namespace Lib9c.DPoS.Tests.Model +{ + 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.ConsensusToken * 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.DPoS.Tests/PoSTest.cs b/Lib9c.DPoS.Tests/PoSTest.cs new file mode 100644 index 0000000000..1c1c1f1387 --- /dev/null +++ b/Lib9c.DPoS.Tests/PoSTest.cs @@ -0,0 +1,20 @@ +using Lib9c.Tests.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Tests +{ + public class PoSTest + { + protected static IWorld InitializeStates() + { + return new World(new MockWorldState()); + } + + protected static Address CreateAddress() + { + PrivateKey privateKey = new PrivateKey(); + return privateKey.Address; + } + } +} diff --git a/Lib9c.DPoS.Tests/TestRandom.cs b/Lib9c.DPoS.Tests/TestRandom.cs new file mode 100644 index 0000000000..361c45c03b --- /dev/null +++ b/Lib9c.DPoS.Tests/TestRandom.cs @@ -0,0 +1,41 @@ +using Libplanet.Action; + +namespace Lib9c.DPoS.Tests +{ + public class TestRandom : IRandom + { + private readonly System.Random _random; + + public TestRandom(int seed = default) + { + _random = new System.Random(seed); + } + + public int Seed => 0; + + public int Next() + { + return _random.Next(); + } + + public int Next(int maxValue) + { + return _random.Next(maxValue); + } + + public int Next(int minValue, int maxValue) + { + return _random.Next(minValue, maxValue); + } + + public void NextBytes(byte[] buffer) + { + _random.NextBytes(buffer); + } + + public double NextDouble() + { + return _random.NextDouble(); + } + } +} diff --git a/Lib9c.DPoS.Tests/ValidatorPowerComparerTest.cs b/Lib9c.DPoS.Tests/ValidatorPowerComparerTest.cs new file mode 100644 index 0000000000..e38e666f3b --- /dev/null +++ b/Lib9c.DPoS.Tests/ValidatorPowerComparerTest.cs @@ -0,0 +1,39 @@ +using System; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Crypto; +using Xunit; + +namespace Lib9c.DPoS.Tests +{ + 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.ConsensusToken * 10); + ValidatorPower validatorPowerB = new ValidatorPower( + publicKeyB.Address, publicKeyB, Asset.ConsensusToken * 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.ConsensusToken * 10); + ValidatorPower validatorPowerB = new ValidatorPower( + publicKeyB.Address, publicKeyB, Asset.ConsensusToken * 10); + int sign = -((IComparable
)publicKeyA.Address) + .CompareTo(publicKeyB.Address); + Assert.True(((IComparable)validatorPowerA) + .CompareTo(validatorPowerB) == sign); + } + } +} diff --git a/Lib9c.DPoS/Action/DPoSModule.cs b/Lib9c.DPoS/Action/DPoSModule.cs new file mode 100644 index 0000000000..c6f7e5e9ea --- /dev/null +++ b/Lib9c.DPoS/Action/DPoSModule.cs @@ -0,0 +1,22 @@ +using Bencodex.Types; +using Lib9c.DPoS.Misc; +using Libplanet.Action.State; +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Action +{ + public static class DPoSModule + { + public static IValue? GetDPoSState(this IWorld world, Address address) + { + return world.GetAccount(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); + } + } +} diff --git a/Lib9c.DPoS/Action/PoSAction.cs b/Lib9c.DPoS/Action/PoSAction.cs new file mode 100644 index 0000000000..67980d39f1 --- /dev/null +++ b/Lib9c.DPoS/Action/PoSAction.cs @@ -0,0 +1,65 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Action +{ + /// + /// A block action for DPoS that updates . + /// + public sealed class PoSAction : IAction + { + /// + /// Creates a new instance of . + /// + public PoSAction() + { + } + + /// + public IValue PlainValue => new Bencodex.Types.Boolean(true); + + /// + public void LoadPlainValue(IValue plainValue) + { + // Method intentionally left empty. + } + + /// + public IWorld Execute(IActionContext context) + { + IActionContext ctx = context; + var states = ctx.PreviousState; + + // if (ctx.Rehearsal) + // Rehearsal mode is not implemented + states = ValidatorSetCtrl.Update(states, ctx); + var nativeTokens = ImmutableHashSet.Create( + Asset.GovernanceToken, Asset.ConsensusToken, Asset.Share); + + states = AllocateReward.Execute( + states, + ctx, + nativeTokens, + ctx.LastCommit?.Votes, + ctx.Miner); + + // Endblock, Update ValidatorSet + var bondedSet = ValidatorSetCtrl.FetchBondedValidatorSet(states).Item2.Set; + foreach (var validator in bondedSet) + { + states = states.SetValidator( + new Libplanet.Types.Consensus.Validator( + validator.OperatorPublicKey, + validator.ConsensusToken.RawValue)); + } + + return states; + } + } +} diff --git a/Lib9c.DPoS/Action/Sys/CancelUndelegation.cs b/Lib9c.DPoS/Action/Sys/CancelUndelegation.cs new file mode 100644 index 0000000000..8ad2470ba5 --- /dev/null +++ b/Lib9c.DPoS/Action/Sys/CancelUndelegation.cs @@ -0,0 +1,81 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Lib9c.DPoS.Util; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Action.Sys +{ + /// + /// A system action for DPoS that cancel specified + /// of tokens to a given . + /// + public sealed class CancelUndelegation : IAction + { + /// + /// 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; + } + + internal 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 IValue PlainValue => Bencodex.Types.Dictionary.Empty + .Add("validator", Validator.Serialize()) + .Add("amount", Amount.Serialize()); + + /// + public void LoadPlainValue(IValue plainValue) + { + var dict = (Bencodex.Types.Dictionary)plainValue; + Validator = dict["validator"].ToAddress(); + Amount = dict["amount"].ToFungibleAssetValue(); + } + + /// + public IWorld Execute(IActionContext context) + { + 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.DPoS/Action/Sys/Delegate.cs b/Lib9c.DPoS/Action/Sys/Delegate.cs new file mode 100644 index 0000000000..feaa0d0790 --- /dev/null +++ b/Lib9c.DPoS/Action/Sys/Delegate.cs @@ -0,0 +1,77 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Util; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Action.Sys +{ + /// + /// A system action for DPoS that specified + /// of tokens to a given . + /// + public sealed class Delegate : IAction + { + /// + /// 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; + } + + internal 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 IValue PlainValue => Bencodex.Types.Dictionary.Empty + .Add("validator", Validator.Serialize()) + .Add("amount", Amount.Serialize()); + + /// + public void LoadPlainValue(IValue plainValue) + { + var dict = (Bencodex.Types.Dictionary)plainValue; + Validator = dict["validator"].ToAddress(); + Amount = dict["amount"].ToFungibleAssetValue(); + } + + /// + public IWorld Execute(IActionContext context) + { + 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.DPoS/Action/Sys/PromoteValidator.cs b/Lib9c.DPoS/Action/Sys/PromoteValidator.cs new file mode 100644 index 0000000000..ab1b26061e --- /dev/null +++ b/Lib9c.DPoS/Action/Sys/PromoteValidator.cs @@ -0,0 +1,86 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Util; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Action.Sys +{ + /// + /// A system action for DPoS that promotes non-validator node to a validator. + /// + public sealed class PromoteValidator : IAction + { + /// + /// 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; + } + + internal 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 IValue PlainValue => Bencodex.Types.Dictionary.Empty + .Add("validator", Validator.Serialize()) + .Add("amount", Amount.Serialize()); + + /// + public void LoadPlainValue(IValue plainValue) + { + var dict = (Bencodex.Types.Dictionary)plainValue; + Validator = dict["validator"].ToPublicKey(); + Amount = dict["amount"].ToFungibleAssetValue(); + } + + /// + public IWorld Execute(IActionContext context) + { + 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.DPoS/Action/Sys/Redelegate.cs b/Lib9c.DPoS/Action/Sys/Redelegate.cs new file mode 100644 index 0000000000..32e4685238 --- /dev/null +++ b/Lib9c.DPoS/Action/Sys/Redelegate.cs @@ -0,0 +1,89 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Util; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Action.Sys +{ + /// + /// A system action for DPoS that specified + /// of shared tokens to from . + /// + public sealed class Redelegate : IAction + { + /// + /// 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; + } + + internal 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 IValue PlainValue => Bencodex.Types.Dictionary.Empty + .Add("src", SrcValidator.Serialize()) + .Add("dst", DstValidator.Serialize()) + .Add("amount", ShareAmount.Serialize()); + + /// + public void LoadPlainValue(IValue plainValue) + { + var dict = (Bencodex.Types.Dictionary)plainValue; + SrcValidator = dict["src"].ToAddress(); + DstValidator = dict["dst"].ToAddress(); + ShareAmount = dict["amount"].ToFungibleAssetValue(); + } + + /// + public IWorld Execute(IActionContext context) + { + 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.DPoS/Action/Sys/Undelegate.cs b/Lib9c.DPoS/Action/Sys/Undelegate.cs new file mode 100644 index 0000000000..11627cec4e --- /dev/null +++ b/Lib9c.DPoS/Action/Sys/Undelegate.cs @@ -0,0 +1,80 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Util; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Action.Sys +{ + /// + /// A system action for DPoS that cancels specified + /// of shared tokens to a given . + /// + public sealed class Undelegate : IAction + { + /// + /// 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; + } + + internal 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 IValue PlainValue => Bencodex.Types.Dictionary.Empty + .Add("validator", Validator.Serialize()) + .Add("amount", ShareAmount.Serialize()); + + /// + public void LoadPlainValue(IValue plainValue) + { + var dict = (Bencodex.Types.Dictionary)plainValue; + Validator = dict["validator"].ToAddress(); + ShareAmount = dict["amount"].ToFungibleAssetValue(); + } + + /// + public IWorld Execute(IActionContext context) + { + 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.DPoS/Action/Sys/WithdrawDelegator.cs b/Lib9c.DPoS/Action/Sys/WithdrawDelegator.cs new file mode 100644 index 0000000000..7ef60cec9d --- /dev/null +++ b/Lib9c.DPoS/Action/Sys/WithdrawDelegator.cs @@ -0,0 +1,82 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Lib9c.DPoS.Util; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Action.Sys +{ + /// + /// A system action for DPoS that withdraws reward tokens from given . + /// + public sealed class WithdrawDelegator : IAction + { + /// + /// Creates a new instance of action. + /// + /// The of the validator + /// from which to withdraw the tokens. + public WithdrawDelegator(Address validator) + { + Validator = validator; + } + + internal WithdrawDelegator() + { + // Used only for deserialization. See also class Libplanet.Action.Sys.Registry. + } + + /// + /// The of the validator to withdraw. + /// + public Address Validator { get; set; } + + /// + public IValue PlainValue => Validator.Serialize(); + + /// + public void LoadPlainValue(IValue plainValue) + { + Validator = plainValue.ToAddress(); + } + + /// + public IWorld Execute(IActionContext context) + { + 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)); + + foreach (Currency nativeToken in nativeTokens) + { + FungibleAssetValue reward = states.GetBalance( + AllocateReward.RewardAddress(ctx.Signer), nativeToken); + if (reward.Sign > 0) + { + states = states.TransferAsset( + ctx, + AllocateReward.RewardAddress(ctx.Signer), + ctx.Signer, + reward); + } + } + + return states; + } + } +} diff --git a/Lib9c.DPoS/Action/Sys/WithdrawValidator.cs b/Lib9c.DPoS/Action/Sys/WithdrawValidator.cs new file mode 100644 index 0000000000..1b90360fb3 --- /dev/null +++ b/Lib9c.DPoS/Action/Sys/WithdrawValidator.cs @@ -0,0 +1,61 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Control; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Types.Assets; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Action.Sys +{ + /// + /// A system action for DPoS that withdraws commission tokens from . + /// + public sealed class WithdrawValidator : IAction + { + /// + /// Creates a new instance of action. + /// + public WithdrawValidator() + { + } + + /// + public IValue PlainValue => Bencodex.Types.Dictionary.Empty; + + /// + public void LoadPlainValue(IValue plainValue) + { + // Method intentionally left empty. + } + + /// + public IWorld Execute(IActionContext context) + { + 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 + foreach (Currency nativeToken in nativeTokens) + { + FungibleAssetValue reward = states.GetBalance( + AllocateReward.RewardAddress(ctx.Signer), nativeToken); + if (reward.Sign > 0) + { + states = states.TransferAsset( + ctx, + AllocateReward.RewardAddress(ctx.Signer), + ctx.Signer, + reward); + } + } + + return states; + } + } +} diff --git a/Lib9c.DPoS/AssemblyInfo.cs b/Lib9c.DPoS/AssemblyInfo.cs new file mode 100644 index 0000000000..bb400a3aee --- /dev/null +++ b/Lib9c.DPoS/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Lib9c.DPoS.Tests")] diff --git a/Lib9c.DPoS/Control/AllocateReward.cs b/Lib9c.DPoS/Control/AllocateReward.cs new file mode 100644 index 0000000000..6cc0039330 --- /dev/null +++ b/Lib9c.DPoS/Control/AllocateReward.cs @@ -0,0 +1,184 @@ +using System.Collections.Immutable; +using System.Numerics; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Lib9c.DPoS.Util; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.PoS; +using Libplanet.Types.Assets; +using Libplanet.Types.Consensus; +using Nekoyume.Module; +using Validator = Lib9c.DPoS.Model.Validator; +using ValidatorSet = Lib9c.DPoS.Model.ValidatorSet; + +namespace Lib9c.DPoS.Control +{ + public static class AllocateReward + { + public static BigInteger BaseProposerRewardNumer => 1; + + public static BigInteger BaseProposerRewardDenom => 100; + + public static BigInteger BonusProposerRewardNumer => 4; + + public static BigInteger BonusProposerRewardDenom => 100; + + public static Address RewardAddress(Address holderAddress) + { + return holderAddress.Derive("RewardAddress"); + } + + internal static IWorld Execute( + IWorld states, + IActionContext ctx, + IImmutableSet? nativeTokens, + IEnumerable? votes, + Address miner) + { + ValidatorSet bondedValidatorSet; + (states, bondedValidatorSet) = ValidatorSetCtrl.FetchBondedValidatorSet(states); + + if (nativeTokens is null) + { + throw new NullNativeTokensException(); + } + + foreach (Currency nativeToken in nativeTokens) + { + if (votes is { } lastVotes) + { + states = DistributeProposerReward( + states, ctx, nativeToken, miner, bondedValidatorSet, lastVotes); + + // TODO: Check if this is correct? + states = DistributeValidatorReward( + states, ctx, nativeToken, bondedValidatorSet, votes); + } + + 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, + Address proposer, + ValidatorSet bondedValidatorSet, + IEnumerable votes) + { + FungibleAssetValue blockReward = states.GetBalance( + ReservedAddress.RewardPool, nativeToken); + + if (blockReward.Sign <= 0) + { + return states; + } + + ImmutableDictionary bondedValidatorDict + = bondedValidatorSet.Set.ToImmutableDictionary( + bondedValidator => bondedValidator.OperatorPublicKey); + + FungibleAssetValue votePowerNumer + = votes.Aggregate( + Asset.ConsensusToken * 0, (total, next) + => total + bondedValidatorDict[next.ValidatorPublicKey].ConsensusToken); + + FungibleAssetValue votePowerDenom + = bondedValidatorSet.TotalConsensusToken; + + var (baseProposerReward, _) + = (blockReward * BaseProposerRewardNumer).DivRem(BaseProposerRewardDenom); + var (bonusProposerReward, _) + = (blockReward * votePowerNumer.RawValue * BonusProposerRewardNumer) + .DivRem(votePowerDenom.RawValue * BonusProposerRewardDenom); + FungibleAssetValue proposerReward = baseProposerReward + bonusProposerReward; + + states = states.TransferAsset( + ctx, ReservedAddress.RewardPool, RewardAddress(proposer), proposerReward); + + return states; + } + + internal static IWorld DistributeValidatorReward( + IWorld states, + IActionContext ctx, + Currency nativeToken, + ValidatorSet bondedValidatorSet, + IEnumerable votes) + { + long blockHeight = ctx.BlockIndex; + FungibleAssetValue validatorRewardSum = states.GetBalance( + ReservedAddress.RewardPool, nativeToken); + + if (validatorRewardSum.Sign <= 0) + { + return states; + } + + ImmutableDictionary bondedValidatorDict + = bondedValidatorSet.Set.ToImmutableDictionary( + bondedValidator => bondedValidator.OperatorPublicKey); + + foreach (Vote vote in votes) + { + if (vote.Flag == VoteFlag.Null || vote.Flag == VoteFlag.Unknown) + { + continue; + } + + ValidatorPower bondedValidator = bondedValidatorDict[vote.ValidatorPublicKey]; + + FungibleAssetValue powerNumer + = bondedValidator.ConsensusToken; + + FungibleAssetValue powerDenom + = bondedValidatorSet.TotalConsensusToken; + + var (validatorReward, _) + = (validatorRewardSum * powerNumer.RawValue) + .DivRem(powerDenom.RawValue); + var (commission, _) + = (validatorReward * Validator.CommissionNumer) + .DivRem(Validator.CommissionDenom); + + FungibleAssetValue delegationRewardSum = validatorReward - commission; + + states = states.TransferAsset( + ctx, + ReservedAddress.RewardPool, + RewardAddress(vote.ValidatorPublicKey.Address), + commission); + + states = states.TransferAsset( + ctx, + ReservedAddress.RewardPool, + ValidatorRewards.DeriveAddress(bondedValidator.ValidatorAddress, nativeToken), + delegationRewardSum); + + states = ValidatorRewardsCtrl.Add( + states, + bondedValidator.ValidatorAddress, + nativeToken, + blockHeight, + delegationRewardSum); + } + + return states; + } + } +} diff --git a/Lib9c.DPoS/Control/Bond.cs b/Lib9c.DPoS/Control/Bond.cs new file mode 100644 index 0000000000..cbf2b2bbf9 --- /dev/null +++ b/Lib9c.DPoS/Control/Bond.cs @@ -0,0 +1,147 @@ +using System.Collections.Immutable; +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; + +namespace Lib9c.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 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); + } + + return (states, issuedShare); + } + + internal static (IWorld, FungibleAssetValue) Cancel( + IWorld states, + IActionContext ctx, + FungibleAssetValue share, + Address validatorAddress, + Address delegationAddress, + IImmutableSet nativeTokens) + { + long blockHeight = ctx.BlockIndex; + + // Currency check + if (!share.Currency.Equals(Asset.Share)) + { + throw new 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); + } + + return (states, unbondingConsensusToken); + } + } +} diff --git a/Lib9c.DPoS/Control/DelegateCtrl.cs b/Lib9c.DPoS/Control/DelegateCtrl.cs new file mode 100644 index 0000000000..791bff0be3 --- /dev/null +++ b/Lib9c.DPoS/Control/DelegateCtrl.cs @@ -0,0 +1,155 @@ +using System.Collections.Immutable; +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Control +{ + internal static class DelegateCtrl + { + internal static Delegation? GetDelegation( + IWorld states, + Address delegationAddress) + { + if (states.GetDPoSState(delegationAddress) is { } value) + { + return new Delegation(value); + } + + return null; + } + + 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 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); + + 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, + AllocateReward.RewardAddress(delegation.DelegatorAddress), + reward); + } + } + + delegation.LatestDistributeHeight = blockHeight; + + states = states.SetDPoSState(delegation.Address, delegation.Serialize()); + + return states; + } + } +} diff --git a/Lib9c.DPoS/Control/RedelegateCtrl.cs b/Lib9c.DPoS/Control/RedelegateCtrl.cs new file mode 100644 index 0000000000..ae1eaca279 --- /dev/null +++ b/Lib9c.DPoS/Control/RedelegateCtrl.cs @@ -0,0 +1,225 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Control +{ + internal static class RedelegateCtrl + { + internal static Redelegation? GetRedelegation( + IWorld states, + Address redelegationAddress) + { + if (states.GetDPoSState(redelegationAddress) is { } value) + { + return new Redelegation(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 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(); + 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); + } + } + + 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.DPoS/Control/UnbondingSetCtrl.cs b/Lib9c.DPoS/Control/UnbondingSetCtrl.cs new file mode 100644 index 0000000000..aea9974758 --- /dev/null +++ b/Lib9c.DPoS/Control/UnbondingSetCtrl.cs @@ -0,0 +1,129 @@ +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Control +{ + internal static class UnbondingSetCtrl + { + 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.DPoS/Control/UndelegateCtrl.cs b/Lib9c.DPoS/Control/UndelegateCtrl.cs new file mode 100644 index 0000000000..782144ef2e --- /dev/null +++ b/Lib9c.DPoS/Control/UndelegateCtrl.cs @@ -0,0 +1,311 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Control +{ + internal static class UndelegateCtrl + { + internal static Undelegation? GetUndelegation( + IWorld state, + Address undelegationAddress) + { + if (state.GetDPoSState(undelegationAddress) is { } value) + { + return new Undelegation(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 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 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(); + 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); + } + } + + (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 + 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); + } + } + + 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.DPoS/Control/ValidatorCtrl.cs b/Lib9c.DPoS/Control/ValidatorCtrl.cs new file mode 100644 index 0000000000..3fb6f4189a --- /dev/null +++ b/Lib9c.DPoS/Control/ValidatorCtrl.cs @@ -0,0 +1,263 @@ +using System.Collections.Immutable; +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Control +{ + internal static class ValidatorCtrl + { + internal static Validator? GetValidator( + IWorld 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()); + } + + 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 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); + + // Does not save current instance, since it's done on delegation + return states; + } + + internal static FungibleAssetValue? ShareFromConsensusToken( + IWorld 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 new FungibleAssetValue( + Asset.Share, consensusToken.MajorUnit, consensusToken.MinorUnit); + } + + 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) + { + 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; + } + } +} diff --git a/Lib9c.DPoS/Control/ValidatorDelegationSetCtrl.cs b/Lib9c.DPoS/Control/ValidatorDelegationSetCtrl.cs new file mode 100644 index 0000000000..967dda206e --- /dev/null +++ b/Lib9c.DPoS/Control/ValidatorDelegationSetCtrl.cs @@ -0,0 +1,59 @@ +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Control +{ + internal static class ValidatorDelegationSetCtrl + { + 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.DPoS/Control/ValidatorPowerIndexCtrl.cs b/Lib9c.DPoS/Control/ValidatorPowerIndexCtrl.cs new file mode 100644 index 0000000000..fee7a3640c --- /dev/null +++ b/Lib9c.DPoS/Control/ValidatorPowerIndexCtrl.cs @@ -0,0 +1,69 @@ +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Control +{ + internal static class ValidatorPowerIndexCtrl + { + 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; + } + } +} diff --git a/Lib9c.DPoS/Control/ValidatorRewardsCtrl.cs b/Lib9c.DPoS/Control/ValidatorRewardsCtrl.cs new file mode 100644 index 0000000000..ef001fef83 --- /dev/null +++ b/Lib9c.DPoS/Control/ValidatorRewardsCtrl.cs @@ -0,0 +1,85 @@ +using System.Collections.Immutable; +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Model; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Control +{ + internal static class ValidatorRewardsCtrl + { + internal static ValidatorRewards? GetValidatorRewards( + IWorld 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.DPoS/Control/ValidatorSetCtrl.cs b/Lib9c.DPoS/Control/ValidatorSetCtrl.cs new file mode 100644 index 0000000000..8230662c8f --- /dev/null +++ b/Lib9c.DPoS/Control/ValidatorSetCtrl.cs @@ -0,0 +1,127 @@ +using Lib9c.DPoS.Action; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Model; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Nekoyume.Module; + +namespace Lib9c.DPoS.Control +{ + internal static class ValidatorSetCtrl + { + 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.DPoS/Exception/DuplicatedValidatorException.cs b/Lib9c.DPoS/Exception/DuplicatedValidatorException.cs new file mode 100644 index 0000000000..4ece4e6807 --- /dev/null +++ b/Lib9c.DPoS/Exception/DuplicatedValidatorException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Exception +{ + public class DuplicatedValidatorException : System.Exception + { + public DuplicatedValidatorException(Address address) + : base($"Validator {address} is duplicated") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/InsufficientFungibleAssetValueException.cs b/Lib9c.DPoS/Exception/InsufficientFungibleAssetValueException.cs new file mode 100644 index 0000000000..e5f717dde8 --- /dev/null +++ b/Lib9c.DPoS/Exception/InsufficientFungibleAssetValueException.cs @@ -0,0 +1,13 @@ +using Libplanet.Types.Assets; + +namespace Lib9c.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.DPoS/Exception/InvalidCurrencyException.cs b/Lib9c.DPoS/Exception/InvalidCurrencyException.cs new file mode 100644 index 0000000000..47a667ceed --- /dev/null +++ b/Lib9c.DPoS/Exception/InvalidCurrencyException.cs @@ -0,0 +1,12 @@ +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Exception +{ + public class InvalidCurrencyException : System.Exception + { + public InvalidCurrencyException(Currency expected, Currency actual) + : base($"Expected {expected}, found {actual}") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/InvalidExchangeRateException.cs b/Lib9c.DPoS/Exception/InvalidExchangeRateException.cs new file mode 100644 index 0000000000..bd460b7aea --- /dev/null +++ b/Lib9c.DPoS/Exception/InvalidExchangeRateException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Exception +{ + public class InvalidExchangeRateException : System.Exception + { + public InvalidExchangeRateException(Address address) + : base($"Exchange of Validator {address} is invalid") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/JailedValidatorException.cs b/Lib9c.DPoS/Exception/JailedValidatorException.cs new file mode 100644 index 0000000000..5e520580f7 --- /dev/null +++ b/Lib9c.DPoS/Exception/JailedValidatorException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Exception +{ + public class JailedValidatorException : System.Exception + { + public JailedValidatorException(Address address) + : base($"Validator {address} is jailed") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/MaximumRedelegationEntriesException.cs b/Lib9c.DPoS/Exception/MaximumRedelegationEntriesException.cs new file mode 100644 index 0000000000..3d71822f31 --- /dev/null +++ b/Lib9c.DPoS/Exception/MaximumRedelegationEntriesException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Lib9c.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.DPoS/Exception/MaximumUndelegationEntriesException.cs b/Lib9c.DPoS/Exception/MaximumUndelegationEntriesException.cs new file mode 100644 index 0000000000..99599eaa17 --- /dev/null +++ b/Lib9c.DPoS/Exception/MaximumUndelegationEntriesException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Lib9c.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.DPoS/Exception/NullDelegationException.cs b/Lib9c.DPoS/Exception/NullDelegationException.cs new file mode 100644 index 0000000000..f076dfa044 --- /dev/null +++ b/Lib9c.DPoS/Exception/NullDelegationException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Exception +{ + public class NullDelegationException : System.Exception + { + public NullDelegationException(Address address) + : base($"Delegation {address} not found") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/NullNativeTokensException.cs b/Lib9c.DPoS/Exception/NullNativeTokensException.cs new file mode 100644 index 0000000000..e7b42cb56e --- /dev/null +++ b/Lib9c.DPoS/Exception/NullNativeTokensException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Libplanet.PoS +{ + public class NullNativeTokensException : Exception + { + public NullNativeTokensException() + : base($"At least one native token have to be set on block policy") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/NullRedelegationException.cs b/Lib9c.DPoS/Exception/NullRedelegationException.cs new file mode 100644 index 0000000000..cc531abaa9 --- /dev/null +++ b/Lib9c.DPoS/Exception/NullRedelegationException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Exception +{ + public class NullRedelegationException : System.Exception + { + public NullRedelegationException(Address address) + : base($"Redelegation {address} not found") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/NullUndelegationException.cs b/Lib9c.DPoS/Exception/NullUndelegationException.cs new file mode 100644 index 0000000000..28f9d86a21 --- /dev/null +++ b/Lib9c.DPoS/Exception/NullUndelegationException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Exception +{ + public class NullUndelegationException : System.Exception + { + public NullUndelegationException(Address address) + : base($"Undelegation {address} not found") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/NullValidatorException.cs b/Lib9c.DPoS/Exception/NullValidatorException.cs new file mode 100644 index 0000000000..3d75440413 --- /dev/null +++ b/Lib9c.DPoS/Exception/NullValidatorException.cs @@ -0,0 +1,12 @@ +using Libplanet.Crypto; + +namespace Lib9c.DPoS.Exception +{ + public class NullValidatorException : System.Exception + { + public NullValidatorException(Address address) + : base($"Validator {address} not found") + { + } + } +} diff --git a/Lib9c.DPoS/Exception/PostmatureUndelegationEntryException.cs b/Lib9c.DPoS/Exception/PostmatureUndelegationEntryException.cs new file mode 100644 index 0000000000..eed386cbef --- /dev/null +++ b/Lib9c.DPoS/Exception/PostmatureUndelegationEntryException.cs @@ -0,0 +1,14 @@ +using Libplanet.Crypto; + +namespace Lib9c.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.DPoS/Exception/PublicKeyAddressMatchingException.cs b/Lib9c.DPoS/Exception/PublicKeyAddressMatchingException.cs new file mode 100644 index 0000000000..ece95fd0a2 --- /dev/null +++ b/Lib9c.DPoS/Exception/PublicKeyAddressMatchingException.cs @@ -0,0 +1,13 @@ +using Libplanet.Crypto; + +namespace Lib9c.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.DPoS/Lib9c.DPoS.csproj b/Lib9c.DPoS/Lib9c.DPoS.csproj new file mode 100644 index 0000000000..3a4eb76f25 --- /dev/null +++ b/Lib9c.DPoS/Lib9c.DPoS.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + enable + enable + + + + + + + + + diff --git a/Lib9c.DPoS/Misc/Asset.cs b/Lib9c.DPoS/Misc/Asset.cs new file mode 100644 index 0000000000..bc26e822ec --- /dev/null +++ b/Lib9c.DPoS/Misc/Asset.cs @@ -0,0 +1,28 @@ +using Libplanet.Types.Assets; + +namespace Lib9c.DPoS.Misc +{ + public struct Asset + { + public static readonly Currency GovernanceToken = + Currency.Legacy("NCG", 2, null); + + public static readonly Currency ConsensusToken = + Currency.Uncapped("ConsensusToken", 18, minters: null); + + public static readonly Currency Share = + Currency.Uncapped("Share", 18, minters: null); + + public static FungibleAssetValue ConsensusFromGovernance(FungibleAssetValue governanceToken) + { + return new FungibleAssetValue( + ConsensusToken, governanceToken.MajorUnit, governanceToken.MinorUnit); + } + + public static FungibleAssetValue GovernanceFromConsensus(FungibleAssetValue consensusToken) + { + return new FungibleAssetValue( + GovernanceToken, consensusToken.MajorUnit, consensusToken.MinorUnit); + } + } +} diff --git a/Lib9c.DPoS/Misc/ReservedAddress.cs b/Lib9c.DPoS/Misc/ReservedAddress.cs new file mode 100644 index 0000000000..69b0032e68 --- /dev/null +++ b/Lib9c.DPoS/Misc/ReservedAddress.cs @@ -0,0 +1,40 @@ +using Libplanet.Crypto; + +namespace Lib9c.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"); + } +} diff --git a/Lib9c.DPoS/Model/BondingStatus.cs b/Lib9c.DPoS/Model/BondingStatus.cs new file mode 100644 index 0000000000..3980e5eef6 --- /dev/null +++ b/Lib9c.DPoS/Model/BondingStatus.cs @@ -0,0 +1,21 @@ +namespace Lib9c.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.DPoS/Model/Delegation.cs b/Lib9c.DPoS/Model/Delegation.cs new file mode 100644 index 0000000000..d5b938eb42 --- /dev/null +++ b/Lib9c.DPoS/Model/Delegation.cs @@ -0,0 +1,88 @@ +using Bencodex.Types; +using Lib9c.DPoS.Util; +using Libplanet.Common; +using Libplanet.Crypto; + +namespace Lib9c.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 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 delegatorAddress + .Derive(validatorAddress.ToByteArray()) + .Derive("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.DPoS/Model/Redelegation.cs b/Lib9c.DPoS/Model/Redelegation.cs new file mode 100644 index 0000000000..7c03d75e5d --- /dev/null +++ b/Lib9c.DPoS/Model/Redelegation.cs @@ -0,0 +1,137 @@ +using Bencodex.Types; +using Lib9c.DPoS.Util; +using Libplanet.Common; +using Libplanet.Crypto; + +namespace Lib9c.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 delegatorAddress + .Derive(srcValidatorAddress.ToByteArray()) + .Derive(dstValidatorAddress.ToByteArray()) + .Derive("Redelegation"); + } + + public IValue Serialize() + { + List serializedRedelegationEntryAddresses = List.Empty; + foreach ( + KeyValuePair redelegationEntryAddressKV + in RedelegationEntryAddresses) + { + serializedRedelegationEntryAddresses = + serializedRedelegationEntryAddresses.Add( + List.Empty + .Add(redelegationEntryAddressKV.Key.Serialize()) + .Add(redelegationEntryAddressKV.Value.Serialize())); + } + + 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 && + RedelegationEntryAddresses.SequenceEqual(other.RedelegationEntryAddresses); + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(Address.ToByteArray()); + } + } +} diff --git a/Lib9c.DPoS/Model/RedelegationEntry.cs b/Lib9c.DPoS/Model/RedelegationEntry.cs new file mode 100644 index 0000000000..256f9d979c --- /dev/null +++ b/Lib9c.DPoS/Model/RedelegationEntry.cs @@ -0,0 +1,147 @@ +using Bencodex.Types; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Util; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.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; + UnbondingConsensusToken = unbondingConsensusToken; + IssuedShare = issuedShare; + Index = index; + CompletionBlockHeight = blockHeight + UnbondingSet.Period; + } + + public RedelegationEntry(IValue serialized) + { + List serializedList = (List)serialized; + Address = serializedList[0].ToAddress(); + RedelegationAddress = serializedList[1].ToAddress(); + RedelegatingShare = serializedList[2].ToFungibleAssetValue(); + UnbondingConsensusToken = serializedList[3].ToFungibleAssetValue(); + IssuedShare = serializedList[4].ToFungibleAssetValue(); + Index = serializedList[5].ToLong(); + CompletionBlockHeight = serializedList[6].ToLong(); + } + + public Address Address { get; set; } + + public Address RedelegationAddress { get; set; } + + public FungibleAssetValue RedelegatingShare + { + get => _redelegatingShare; + set + { + if (!value.Currency.Equals(Asset.Share)) + { + throw new InvalidCurrencyException(Asset.Share, value.Currency); + } + + _redelegatingShare = value; + } + } + + public FungibleAssetValue UnbondingConsensusToken + { + get => _unbondingConsensusToken; + set + { + if (!value.Currency.Equals(Asset.ConsensusToken)) + { + throw new InvalidCurrencyException(Asset.ConsensusToken, value.Currency); + } + + _unbondingConsensusToken = value; + } + } + + public FungibleAssetValue IssuedShare + { + get => _issuedShare; + set + { + if (!value.Currency.Equals(Asset.Share)) + { + throw new InvalidCurrencyException(Asset.Share, value.Currency); + } + + _issuedShare = value; + } + } + + public long Index { 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 redelegationAddress.Derive($"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(UnbondingConsensusToken.Serialize()) + .Add(IssuedShare.Serialize()) + .Add(Index.Serialize()) + .Add(CompletionBlockHeight.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) && + UnbondingConsensusToken.Equals(other.UnbondingConsensusToken) && + IssuedShare.Equals(other.IssuedShare) && + Index == other.Index && + CompletionBlockHeight == other.CompletionBlockHeight; + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(Address.ToByteArray()); + } + } +} diff --git a/Lib9c.DPoS/Model/UnbondingSet.cs b/Lib9c.DPoS/Model/UnbondingSet.cs new file mode 100644 index 0000000000..41d7d01a9a --- /dev/null +++ b/Lib9c.DPoS/Model/UnbondingSet.cs @@ -0,0 +1,59 @@ +using Bencodex.Types; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Util; +using Libplanet.Crypto; + +namespace Lib9c.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.DPoS/Model/Undelegation.cs b/Lib9c.DPoS/Model/Undelegation.cs new file mode 100644 index 0000000000..678658557c --- /dev/null +++ b/Lib9c.DPoS/Model/Undelegation.cs @@ -0,0 +1,125 @@ +using Bencodex.Types; +using Lib9c.DPoS.Util; +using Libplanet.Common; +using Libplanet.Crypto; + +namespace Lib9c.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 delegatorAddress + .Derive(validatorAddress.ToByteArray()) + .Derive("Undelegation"); + } + + public IValue Serialize() + { + List serializedUndelegationEntryAddresses = List.Empty; + foreach ( + KeyValuePair undelegationEntryAddressKV + in UndelegationEntryAddresses) + { + serializedUndelegationEntryAddresses = + serializedUndelegationEntryAddresses.Add( + List.Empty + .Add(undelegationEntryAddressKV.Key.Serialize()) + .Add(undelegationEntryAddressKV.Value.Serialize())); + } + + 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 && + UndelegationEntryAddresses.SequenceEqual(other.UndelegationEntryAddresses); + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(Address.ToByteArray()); + } + } +} diff --git a/Lib9c.DPoS/Model/UndelegationEntry.cs b/Lib9c.DPoS/Model/UndelegationEntry.cs new file mode 100644 index 0000000000..34b13f0867 --- /dev/null +++ b/Lib9c.DPoS/Model/UndelegationEntry.cs @@ -0,0 +1,107 @@ +using Bencodex.Types; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Util; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.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; + UnbondingConsensusToken = unbondingConsensusToken; + Index = index; + CompletionBlockHeight = blockHeight + UnbondingSet.Period; + } + + public UndelegationEntry(IValue serialized) + { + List serializedList = (List)serialized; + Address = serializedList[0].ToAddress(); + UndelegationAddress = serializedList[1].ToAddress(); + UnbondingConsensusToken = serializedList[2].ToFungibleAssetValue(); + Index = serializedList[3].ToLong(); + CompletionBlockHeight = serializedList[4].ToLong(); + } + + public Address Address { get; set; } + + public Address UndelegationAddress { get; set; } + + public FungibleAssetValue UnbondingConsensusToken + { + get => _unbondingConsensusToken; + set + { + if (!value.Currency.Equals(Asset.ConsensusToken)) + { + throw new InvalidCurrencyException(Asset.ConsensusToken, value.Currency); + } + + _unbondingConsensusToken = value; + } + } + + public long Index { 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 undelegationAddress.Derive($"UndelegationEntry{index}"); + } + + public bool IsMatured(long blockHeight) => blockHeight >= CompletionBlockHeight; + + public IValue Serialize() + { + return List.Empty + .Add(Address.Serialize()) + .Add(UndelegationAddress.Serialize()) + .Add(UnbondingConsensusToken.Serialize()) + .Add(Index.Serialize()) + .Add(CompletionBlockHeight.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) && + UnbondingConsensusToken.Equals(other.UnbondingConsensusToken) && + Index == other.Index && + CompletionBlockHeight == other.CompletionBlockHeight; + } + + public override int GetHashCode() + { + return ByteUtil.CalculateHashCode(Address.ToByteArray()); + } + } +} diff --git a/Lib9c.DPoS/Model/Validator.cs b/Lib9c.DPoS/Model/Validator.cs new file mode 100644 index 0000000000..da84e75b83 --- /dev/null +++ b/Lib9c.DPoS/Model/Validator.cs @@ -0,0 +1,134 @@ +using System.Numerics; +using Bencodex.Types; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Util; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.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 * 1; + + public static BigInteger CommissionNumer => 1; + + public static BigInteger CommissionDenom => 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 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 operatorAddress.Derive("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.DPoS/Model/ValidatorDelegationSet.cs b/Lib9c.DPoS/Model/ValidatorDelegationSet.cs new file mode 100644 index 0000000000..f24c77654a --- /dev/null +++ b/Lib9c.DPoS/Model/ValidatorDelegationSet.cs @@ -0,0 +1,63 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Util; +using Libplanet.Crypto; + +namespace Lib9c.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 validatorAddress.Derive("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.DPoS/Model/ValidatorPower.cs b/Lib9c.DPoS/Model/ValidatorPower.cs new file mode 100644 index 0000000000..d1b06da2f3 --- /dev/null +++ b/Lib9c.DPoS/Model/ValidatorPower.cs @@ -0,0 +1,111 @@ +using Bencodex.Types; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Misc; +using Lib9c.DPoS.Util; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.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 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) + { + 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.DPoS/Model/ValidatorPowerIndex.cs b/Lib9c.DPoS/Model/ValidatorPowerIndex.cs new file mode 100644 index 0000000000..7c646ef1fd --- /dev/null +++ b/Lib9c.DPoS/Model/ValidatorPowerIndex.cs @@ -0,0 +1,36 @@ +using Bencodex.Types; +using Lib9c.DPoS.Misc; +using Libplanet.Crypto; + +namespace Lib9c.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; + + public List
ValidatorAddresses + => Index.Select(key => key.ValidatorAddress).ToList(); + + public IValue Serialize() + => new List(Index.Select(consensusPowerKey => consensusPowerKey.Serialize())); + } +} diff --git a/Lib9c.DPoS/Model/ValidatorRewards.cs b/Lib9c.DPoS/Model/ValidatorRewards.cs new file mode 100644 index 0000000000..8707f5c852 --- /dev/null +++ b/Lib9c.DPoS/Model/ValidatorRewards.cs @@ -0,0 +1,87 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Exception; +using Lib9c.DPoS.Util; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.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 validatorAddress.Derive("ValidatorRewardsAddress").Derive(currency.Ticker); + } + + public void Add(long blockHeight, FungibleAssetValue reward) + { + if (!reward.Currency.Equals(Currency)) + { + throw new 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.DPoS/Model/ValidatorSet.cs b/Lib9c.DPoS/Model/ValidatorSet.cs new file mode 100644 index 0000000000..dd211f039d --- /dev/null +++ b/Lib9c.DPoS/Model/ValidatorSet.cs @@ -0,0 +1,58 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Lib9c.DPoS.Misc; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.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.DPoS/Util/AddressHelper.cs b/Lib9c.DPoS/Util/AddressHelper.cs new file mode 100644 index 0000000000..210e27144b --- /dev/null +++ b/Lib9c.DPoS/Util/AddressHelper.cs @@ -0,0 +1,25 @@ +using System.Security.Cryptography; +using System.Text; +using Libplanet.Crypto; + +namespace Lib9c.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.DPoS/Util/MarshalHelper.cs b/Lib9c.DPoS/Util/MarshalHelper.cs new file mode 100644 index 0000000000..f084ab3d0d --- /dev/null +++ b/Lib9c.DPoS/Util/MarshalHelper.cs @@ -0,0 +1,82 @@ +using System.Collections.Immutable; +using System.Globalization; +using System.Numerics; +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.Types.Assets; + +namespace Lib9c.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.MessagePack/Action/NCActionEvaluation.cs b/Lib9c.MessagePack/Action/NCActionEvaluation.cs index 35f2b94a68..f977a3489d 100644 --- a/Lib9c.MessagePack/Action/NCActionEvaluation.cs +++ b/Lib9c.MessagePack/Action/NCActionEvaluation.cs @@ -49,7 +49,9 @@ public struct NCActionEvaluation [Key(8)] [MessagePackFormatter(typeof(TxIdFormatter))] + #pragma warning disable MsgPack003 public TxId? TxId { get; set; } + #pragma warning restore MsgPack003 [SerializationConstructor] public NCActionEvaluation( diff --git a/Lib9c.sln b/Lib9c.sln index 79f6d8b418..35a141ef77 100644 --- a/Lib9c.sln +++ b/Lib9c.sln @@ -70,6 +70,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lib9c.Plugin", ".Lib9c.Plug EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lib9c.Plugin.Shared", ".Lib9c.Plugin.Shared\Lib9c.Plugin.Shared.csproj", "{76F6C25E-94D2-4EA9-B88D-0249F44D1D16}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lib9c.DPoS", "Lib9c.DPoS\Lib9c.DPoS.csproj", "{23848A39-37DD-4B73-A108-1915E10FEE50}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lib9c.DPoS.Tests", "Lib9c.DPoS.Tests\Lib9c.DPoS.Tests.csproj", "{447DEF1C-01B0-4FC7-8444-B9DB5C26AFB7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -196,6 +200,14 @@ Global {76F6C25E-94D2-4EA9-B88D-0249F44D1D16}.Debug|Any CPU.Build.0 = Debug|Any CPU {76F6C25E-94D2-4EA9-B88D-0249F44D1D16}.Release|Any CPU.ActiveCfg = Release|Any CPU {76F6C25E-94D2-4EA9-B88D-0249F44D1D16}.Release|Any CPU.Build.0 = Release|Any CPU + {23848A39-37DD-4B73-A108-1915E10FEE50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23848A39-37DD-4B73-A108-1915E10FEE50}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23848A39-37DD-4B73-A108-1915E10FEE50}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23848A39-37DD-4B73-A108-1915E10FEE50}.Release|Any CPU.Build.0 = Release|Any CPU + {447DEF1C-01B0-4FC7-8444-B9DB5C26AFB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {447DEF1C-01B0-4FC7-8444-B9DB5C26AFB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {447DEF1C-01B0-4FC7-8444-B9DB5C26AFB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {447DEF1C-01B0-4FC7-8444-B9DB5C26AFB7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Lib9c/Model/State/DeletedAvatarState.cs b/Lib9c/Model/State/DeletedAvatarState.cs deleted file mode 100644 index dc5328e46b..0000000000 --- a/Lib9c/Model/State/DeletedAvatarState.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Bencodex.Types; - -namespace Nekoyume.Model.State -{ - [Serializable] - public class DeletedAvatarState : AvatarState - { - public long deletedAt; - - public DeletedAvatarState(AvatarState avatarState, long blockIndex) - : base(avatarState) - { - deletedAt = blockIndex; - } - - public DeletedAvatarState(Dictionary serialized) - : base(serialized) - { - deletedAt = serialized["deletedAt"].ToLong(); - } - - public override IValue Serialize() => -#pragma warning disable LAA1002 - new Dictionary(new Dictionary - { - [(Text) "deletedAt"] = deletedAt.Serialize(), - }.Union((Dictionary) base.Serialize())); -#pragma warning restore LAA1002 - } -}