From 88039b3d48acd948e4a4572e1e4c247159a40e59 Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Fri, 26 Apr 2024 11:07:24 +0900 Subject: [PATCH 1/8] Initial implementation of CurrencyAccount with MintAsset() and BurnAsset() --- Libplanet.Action/State/CurrencyAccount.cs | 320 +++++++++++++++++++++ Libplanet.Action/State/IWorldExtensions.cs | 153 +++------- 2 files changed, 362 insertions(+), 111 deletions(-) create mode 100644 Libplanet.Action/State/CurrencyAccount.cs diff --git a/Libplanet.Action/State/CurrencyAccount.cs b/Libplanet.Action/State/CurrencyAccount.cs new file mode 100644 index 00000000000..76a70869434 --- /dev/null +++ b/Libplanet.Action/State/CurrencyAccount.cs @@ -0,0 +1,320 @@ +using System; +using System.Numerics; +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.Store.Trie; +using Libplanet.Types.Assets; +using Libplanet.Types.Blocks; + +namespace Libplanet.Action.State +{ + /// + /// A special "account" for managing starting with + /// . + /// + public class CurrencyAccount + { + /// + /// The location within the account where + /// the total supply of the currency gets stored. + /// + public static readonly Address TotalSupplyAddress = + new Address("1000000000000000000000000000000000000000"); + + public CurrencyAccount(ITrie trie, int worldVersion, Currency currency) + { + Trie = trie; + WorldVersion = worldVersion; + Currency = currency; + } + + public ITrie Trie { get; } + + public int WorldVersion { get; } + + public Currency Currency { get; } + + public FungibleAssetValue GetBalance(Address address, Currency currency) + { + if (!Currency.Equals(currency)) + { + throw new ArgumentException(); + } + + return FungibleAssetValue.FromRawValue( + Currency, + WorldVersion >= BlockMetadata.CurrencyAccountProtocolVersion + ? GetRawBalanceV7(address) + : GetRawBalanceV0(address)); + } + + public FungibleAssetValue GetTotalSupply(Currency currency) + { + if (!Currency.Equals(currency)) + { + throw new ArgumentException(); + } + + return FungibleAssetValue.FromRawValue( + Currency, + WorldVersion >= BlockMetadata.CurrencyAccountProtocolVersion + ? GetRawTotalSupplyV7() + : GetRawTotalSupplyV0()); + } + + public CurrencyAccount MintAsset( + IActionContext context, + Address recipient, + FungibleAssetValue value) + { + if (!Currency.Equals(value.Currency)) + { + throw new ArgumentException(); + } + + if (value.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(value), + "The value to mint has to be greater than zero."); + } + + if (!Currency.AllowsToMint(context.Signer)) + { + throw new CurrencyPermissionException( + $"The account {context.Signer} has no permission to mint currency {Currency}.", + context.Signer, + Currency); + } + + return WorldVersion >= BlockMetadata.CurrencyAccountProtocolVersion + ? MintAssetV7(context, recipient, value) + : MintAssetV0(context, recipient, value); + } + + public CurrencyAccount BurnAsset( + IActionContext context, + Address owner, + FungibleAssetValue value) + { + if (!Currency.Equals(value.Currency)) + { + throw new ArgumentException(); + } + + if (value.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(value), + "The value to burn has to be greater than zero."); + } + + if (!Currency.AllowsToMint(context.Signer)) + { + throw new CurrencyPermissionException( + $"The account {context.Signer} has no permission to burn currency {Currency}.", + context.Signer, + Currency); + } + + return WorldVersion >= BlockMetadata.CurrencyAccountProtocolVersion + ? BurnAssetV7(context, owner, value) + : BurnAssetV0(context, owner, value); + + } + + public IAccount AsAccount() + { + return new Account(new AccountState(Trie)); + } + + private CurrencyAccount MintAssetV0( + IActionContext context, + Address recipient, + FungibleAssetValue value) + { + ValidateMinter(context.Signer); + CurrencyAccount currencyAccount = this; + BigInteger prevAmount = currencyAccount.GetRawBalanceV0(recipient); + BigInteger newAmount = prevAmount + value.RawValue; + currencyAccount = currencyAccount.WriteBalanceV0(recipient, newAmount); + + if (Currency.TotalSupplyTrackable) + { + // NOTE: This should never throw an exception. + BigInteger prevTotalSupply = currencyAccount.GetRawTotalSupplyV0(); + currencyAccount = + currencyAccount.WriteTotalSupplyV0(prevTotalSupply + value.RawValue); + } + + return currencyAccount; + } + + private CurrencyAccount MintAssetV7( + IActionContext context, + Address recipient, + FungibleAssetValue value) + { + ValidateMinter(context.Signer); + CurrencyAccount currencyAccount = this; + BigInteger prevAmount = currencyAccount.GetRawBalanceV7(recipient); + BigInteger newAmount = prevAmount + value.RawValue; + currencyAccount = + currencyAccount.WriteBalanceV7(recipient, newAmount); + BigInteger prevTotalSupply = currencyAccount.GetRawTotalSupplyV7(); + currencyAccount = + currencyAccount.WriteTotalSupplyV7(prevTotalSupply + value.RawValue); + + return currencyAccount; + } + + private CurrencyAccount BurnAssetV0( + IActionContext context, + Address owner, + FungibleAssetValue value) + { + ValidateMinter(context.Signer); + CurrencyAccount currencyAccount = this; + BigInteger prevAmount = currencyAccount.GetRawBalanceV0(owner); + BigInteger newAmount = prevAmount - value.RawValue; + currencyAccount = currencyAccount.WriteBalanceV0(owner, newAmount); + + if (Currency.TotalSupplyTrackable) + { + // NOTE: This should never throw an exception. + BigInteger prevTotalSupply = currencyAccount.GetRawTotalSupplyV0(); + currencyAccount = + currencyAccount.WriteTotalSupplyV0(prevTotalSupply - value.RawValue); + } + + return currencyAccount; + } + + private CurrencyAccount BurnAssetV7( + IActionContext context, + Address owner, + FungibleAssetValue value) + { + ValidateMinter(context.Signer); + CurrencyAccount currencyAccount = this; + BigInteger prevAmount = currencyAccount.GetRawBalanceV7(owner); + BigInteger newAmount = prevAmount - value.RawValue; + currencyAccount = currencyAccount.WriteBalanceV7(owner, newAmount); + BigInteger prevTotalSupply = currencyAccount.GetRawTotalSupplyV7(); + currencyAccount = currencyAccount.WriteTotalSupplyV7(prevTotalSupply - value.RawValue); + + return currencyAccount; + } + + private CurrencyAccount WriteBalanceV0( + Address address, + BigInteger value) + { + ValidateBalance(value); + return new CurrencyAccount( + Trie.Set(KeyConverters.ToFungibleAssetKey(address, Currency), new Integer(value)), + WorldVersion, + Currency); + } + + private CurrencyAccount WriteBalanceV7(Address address, BigInteger value) + { + ValidateBalance(value); + return new CurrencyAccount( + Trie.Set(KeyConverters.ToStateKey(address), new Integer(value)), + WorldVersion, + Currency); + } + + private CurrencyAccount WriteTotalSupplyV0(BigInteger value) + { + ValidateTotalSupply(value); + return new CurrencyAccount( + Trie.Set(KeyConverters.ToTotalSupplyKey(Currency), new Integer(value)), + WorldVersion, + Currency); + } + + private CurrencyAccount WriteTotalSupplyV7(BigInteger value) + { + ValidateTotalSupply(value); + return new CurrencyAccount( + Trie.Set( + KeyConverters.ToStateKey(CurrencyAccount.TotalSupplyAddress), + new Integer(value)), + WorldVersion, + Currency); + } + + private BigInteger GetRawBalanceV0(Address address) + { + return Trie.Get( + KeyConverters.ToFungibleAssetKey(address, Currency)) is Integer i + ? i.Value + : BigInteger.Zero; + } + + private BigInteger GetRawBalanceV7(Address address) + { + return Trie.Get(KeyConverters.ToStateKey(address)) is Integer i + ? i.Value + : BigInteger.Zero; + } + + private BigInteger GetRawTotalSupplyV0() + { + if (!Currency.TotalSupplyTrackable) + { + throw TotalSupplyNotTrackableException.WithDefaultMessage(Currency); + } + + return Trie.Get(KeyConverters.ToTotalSupplyKey(Currency)) is Integer i + ? i.Value + : BigInteger.Zero; + } + + private BigInteger GetRawTotalSupplyV7() + { + return Trie.Get(KeyConverters.ToStateKey(TotalSupplyAddress)) is Integer i + ? i.Value + : BigInteger.Zero; + } + + private void ValidateMinter(Address signer) + { + if (!Currency.AllowsToMint(signer)) + { + // CurrencyPermission + throw new ArgumentException(); + } + } + + private void ValidateTotalSupply(BigInteger rawAmount) + { + if (Currency.MaximumSupply is { } maximumSupply && + maximumSupply.RawValue < rawAmount) + { + // SupplyOverflow + throw new SupplyOverflowException( + "Some message", + FungibleAssetValue.FromRawValue(Currency, rawAmount)); + } + else if (rawAmount < BigInteger.Zero) + { + throw new ArgumentException(); + } + } + + private void ValidateBalance(BigInteger rawAmount) + { + if (rawAmount < 0) + { + // InsufficientBalance + throw new InsufficientBalanceException( + "Some message", + new PrivateKey().Address, + FungibleAssetValue.FromRawValue(Currency, rawAmount)); + } + } + } +} diff --git a/Libplanet.Action/State/IWorldExtensions.cs b/Libplanet.Action/State/IWorldExtensions.cs index 30a198da1f6..43eca52d03c 100644 --- a/Libplanet.Action/State/IWorldExtensions.cs +++ b/Libplanet.Action/State/IWorldExtensions.cs @@ -26,14 +26,8 @@ public static class IWorldExtensions public static FungibleAssetValue GetBalance( this IWorldState worldState, Address address, - Currency currency) - { - IAccountState account = worldState.GetAccountState(ReservedAddresses.LegacyAccount); - IValue? value = account.Trie.Get(ToFungibleAssetKey(address, currency)); - return value is Integer i - ? FungibleAssetValue.FromRawValue(currency, i) - : currency * 0; - } + Currency currency) => + worldState.GetCurrencyAccount(currency).GetBalance(address, currency); /// /// Mints the fungible asset (i.e., in-game monetary), @@ -60,52 +54,9 @@ public static IWorld MintAsset( this IWorld world, IActionContext context, Address recipient, - FungibleAssetValue value) - { - if (value.Sign <= 0) - { - throw new ArgumentOutOfRangeException( - nameof(value), - "The value to mint has to be greater than zero." - ); - } - - Currency currency = value.Currency; - if (!currency.AllowsToMint(context.Signer)) - { - throw new CurrencyPermissionException( - $"The account {context.Signer} has no permission to mint currency {currency}.", - context.Signer, - currency - ); - } - - FungibleAssetValue balance = GetBalance(world, recipient, currency); - BigInteger rawBalance = (balance + value).RawValue; - - if (currency.TotalSupplyTrackable) - { - var currentTotalSupply = GetTotalSupply(world, currency); - if (currency.MaximumSupply < currentTotalSupply + value) - { - var msg = $"The amount {value} attempted to be minted added to the current" - + $" total supply of {currentTotalSupply} exceeds the" - + $" maximum allowed supply of {currency.MaximumSupply}."; - throw new SupplyOverflowException(msg, value); - } - - return UpdateFungibleAssets( - world, - recipient, - currency, - rawBalance, - (currentTotalSupply + value).RawValue); - } - else - { - return UpdateFungibleAssets(world, recipient, currency, rawBalance); - } - } + FungibleAssetValue value) => + world.SetCurrencyAccount( + world.GetCurrencyAccount(value.Currency).MintAsset(context, recipient, value)); /// /// Burns the fungible asset (i.e., in-game monetary) from @@ -130,50 +81,9 @@ public static IWorld BurnAsset( this IWorld world, IActionContext context, Address owner, - FungibleAssetValue value) - { - string msg; - if (value.Sign <= 0) - { - throw new ArgumentOutOfRangeException( - nameof(value), - "The value to burn has to be greater than zero." - ); - } - - Currency currency = value.Currency; - if (!currency.AllowsToMint(context.Signer)) - { - msg = $"The account {context.Signer} has no permission to burn assets of " + - $"the currency {currency}."; - throw new CurrencyPermissionException(msg, context.Signer, currency); - } - - FungibleAssetValue balance = world.GetBalance(owner, currency); - - if (balance < value) - { - msg = $"The account {owner}'s balance of {currency} is insufficient to burn: " + - $"{balance} < {value}."; - throw new InsufficientBalanceException(msg, owner, balance); - } - - BigInteger rawBalance = (balance - value).RawValue; - if (currency.TotalSupplyTrackable) - { - var currentTotalSupply = world.GetTotalSupply(currency); - return UpdateFungibleAssets( - world, - owner, - currency, - rawBalance, - (currentTotalSupply - value).RawValue); - } - else - { - return UpdateFungibleAssets(world, owner, currency, rawBalance); - } - } + FungibleAssetValue value) => + world.SetCurrencyAccount( + world.GetCurrencyAccount(value.Currency).BurnAsset(context, owner, value)); /// /// Transfers the fungible asset (i.e., in-game monetary) @@ -224,19 +134,8 @@ public static IWorld TransferAsset( [Pure] public static FungibleAssetValue GetTotalSupply( this IWorldState worldState, - Currency currency) - { - if (!currency.TotalSupplyTrackable) - { - throw TotalSupplyNotTrackableException.WithDefaultMessage(currency); - } - - IAccountState account = worldState.GetAccountState(ReservedAddresses.LegacyAccount); - IValue? value = account.Trie.Get(ToTotalSupplyKey(currency)); - return value is Integer i - ? FungibleAssetValue.FromRawValue(currency, i) - : currency * 0; - } + Currency currency) => + worldState.GetCurrencyAccount(currency).GetTotalSupply(currency); /// /// Returns the validator set. @@ -270,6 +169,38 @@ internal static ValidatorSetAccount GetValidatorSetAccount(this IWorldState worl worldState.GetAccountState(ReservedAddresses.LegacyAccount).Trie, worldState.Version); + [Pure] + internal static CurrencyAccount GetCurrencyAccount( + this IWorldState worldState, + Currency currency) => + worldState.Version >= BlockMetadata.CurrencyAccountProtocolVersion + ? new CurrencyAccount( + worldState.GetAccountState(new Address(currency.Hash.ByteArray)).Trie, + worldState.Version, + currency) + : new CurrencyAccount( + worldState.GetAccountState(ReservedAddresses.LegacyAccount).Trie, + worldState.Version, + currency); + + [Pure] + internal static IWorld SetCurrencyAccount( + this IWorld world, + CurrencyAccount currencyAccount) => + world.Version == currencyAccount.WorldVersion + ? world.Version >= BlockMetadata.CurrencyAccountProtocolVersion + ? world.SetAccount( + new Address(currencyAccount.Currency.Hash.ByteArray), + currencyAccount.AsAccount()) + : world.SetAccount( + ReservedAddresses.LegacyAccount, + currencyAccount.AsAccount()) + : throw new ArgumentException( + $"Given {nameof(currencyAccount)} must have the same version as " + + $"the version of the world {world.Version}: " + + $"{currencyAccount.WorldVersion}", + nameof(currencyAccount)); + [Pure] internal static IWorld SetValidatorSetAccount( this IWorld world, From 93804fa8f2e3d1491488f1db6c5d2362316c36f4 Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Mon, 29 Apr 2024 14:13:59 +0900 Subject: [PATCH 2/8] Added TransferAsset() to CurrencyAccount --- Libplanet.Action/State/CurrencyAccount.cs | 77 ++++++++++++++++++ Libplanet.Action/State/IWorldExtensions.cs | 92 +--------------------- 2 files changed, 81 insertions(+), 88 deletions(-) diff --git a/Libplanet.Action/State/CurrencyAccount.cs b/Libplanet.Action/State/CurrencyAccount.cs index 76a70869434..34bc5ff4c42 100644 --- a/Libplanet.Action/State/CurrencyAccount.cs +++ b/Libplanet.Action/State/CurrencyAccount.cs @@ -120,7 +120,29 @@ public CurrencyAccount BurnAsset( return WorldVersion >= BlockMetadata.CurrencyAccountProtocolVersion ? BurnAssetV7(context, owner, value) : BurnAssetV0(context, owner, value); + } + + public CurrencyAccount TransferAsset( + IActionContext context, + Address sender, + Address recipient, + FungibleAssetValue value) + { + if (!Currency.Equals(value.Currency)) + { + throw new ArgumentException(); + } + if (value.Sign <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(value), + "The value to transfer has to be greater than zero."); + } + + return WorldVersion >= BlockMetadata.CurrencyAccountProtocolVersion + ? TransferAssetV7(context, sender, recipient, value) + : TransferAssetV0(context, sender, recipient, value); } public IAccount AsAccount() @@ -206,6 +228,61 @@ private CurrencyAccount BurnAssetV7( return currencyAccount; } + private CurrencyAccount TransferAssetV7( + IActionContext context, + Address sender, + Address recipient, + FungibleAssetValue value) + { + CurrencyAccount currencyAccount = this; + + BigInteger senderBalance = currencyAccount.GetRawBalanceV7(sender); + currencyAccount = currencyAccount.WriteBalanceV7( + sender, + senderBalance - value.RawValue); + BigInteger recipientBalance = currencyAccount.GetRawBalanceV7(recipient); + currencyAccount = currencyAccount.WriteBalanceV7( + recipient, + recipientBalance + value.RawValue); + return currencyAccount; + } + + private CurrencyAccount TransferAssetV0( + IActionContext context, + Address sender, + Address recipient, + FungibleAssetValue value) + { + CurrencyAccount currencyAccount = this; + + // NOTE: For backward compatibility with the bugged behavior before + // protocol version 1. + if (context.BlockProtocolVersion == 0) + { + BigInteger senderBalance = currencyAccount.GetRawBalanceV0(sender); + BigInteger recipientBalance = currencyAccount.GetRawBalanceV0(recipient); + currencyAccount = currencyAccount.WriteBalanceV0( + sender, + senderBalance - value.RawValue); + currencyAccount = currencyAccount.WriteBalanceV0( + recipient, + recipientBalance + value.RawValue); + } + else + { + BigInteger senderBalance = currencyAccount.GetRawBalanceV0(sender); + currencyAccount = currencyAccount.WriteBalanceV0( + sender, + senderBalance - value.RawValue); + BigInteger recipientBalance = currencyAccount.GetRawBalanceV0(recipient); + currencyAccount = currencyAccount.WriteBalanceV0( + recipient, + recipientBalance + value.RawValue); + } + + return currencyAccount; + } + private CurrencyAccount WriteBalanceV0( Address address, BigInteger value) diff --git a/Libplanet.Action/State/IWorldExtensions.cs b/Libplanet.Action/State/IWorldExtensions.cs index 43eca52d03c..47844fa399c 100644 --- a/Libplanet.Action/State/IWorldExtensions.cs +++ b/Libplanet.Action/State/IWorldExtensions.cs @@ -117,9 +117,10 @@ public static IWorld TransferAsset( Address sender, Address recipient, FungibleAssetValue value) => - context.BlockProtocolVersion >= BlockMetadata.TransferFixProtocolVersion - ? TransferAssetV1(world, sender, recipient, value) - : TransferAssetV0(world, sender, recipient, value); + world.SetCurrencyAccount( + world + .GetCurrencyAccount(value.Currency) + .TransferAsset(context, sender, recipient, value)); /// /// Returns the total supply of a . @@ -218,90 +219,5 @@ internal static IWorld SetValidatorSetAccount( $"the version of the world {world.Version}: " + $"{validatorSetAccount.WorldVersion}", nameof(validatorSetAccount)); - - [Pure] - private static IWorld TransferAssetV0( - IWorld world, - Address sender, - Address recipient, - FungibleAssetValue value) - { - if (value.Sign <= 0) - { - throw new ArgumentOutOfRangeException( - nameof(value), - "The value to transfer has to be greater than zero." - ); - } - - Currency currency = value.Currency; - FungibleAssetValue senderBalance = world.GetBalance(sender, currency); - FungibleAssetValue recipientBalance = world.GetBalance(recipient, currency); - - if (senderBalance < value) - { - var msg = $"The account {sender}'s balance of {currency} is insufficient to " + - $"transfer: {senderBalance} < {value}."; - throw new InsufficientBalanceException(msg, sender, senderBalance); - } - - IWorld intermediate = UpdateFungibleAssets( - world, sender, currency, (senderBalance - value).RawValue); - return UpdateFungibleAssets( - intermediate, recipient, currency, (recipientBalance + value).RawValue); - } - - [Pure] - private static IWorld TransferAssetV1( - IWorld world, - Address sender, - Address recipient, - FungibleAssetValue value) - { - if (value.Sign <= 0) - { - throw new ArgumentOutOfRangeException( - nameof(value), - "The value to transfer has to be greater than zero." - ); - } - - Currency currency = value.Currency; - FungibleAssetValue senderBalance = world.GetBalance(sender, currency); - - if (senderBalance < value) - { - var msg = $"The account {sender}'s balance of {currency} is insufficient to " + - $"transfer: {senderBalance} < {value}."; - throw new InsufficientBalanceException(msg, sender, senderBalance); - } - - BigInteger senderRawBalance = (senderBalance - value).RawValue; - IWorld intermediate = UpdateFungibleAssets(world, sender, currency, senderRawBalance); - FungibleAssetValue recipientBalance = intermediate.GetBalance(recipient, currency); - BigInteger recipientRawBalance = (recipientBalance + value).RawValue; - - return UpdateFungibleAssets(intermediate, recipient, currency, recipientRawBalance); - } - - [Pure] - private static IWorld UpdateFungibleAssets( - IWorld world, - Address address, - Currency currency, - BigInteger amount, - BigInteger? supplyAmount = null) - { - IAccount account = supplyAmount is { } sa - ? new Account(new AccountState( - world.GetAccount(ReservedAddresses.LegacyAccount).Trie - .Set(ToFungibleAssetKey(address, currency), new Integer(amount)) - .Set(ToTotalSupplyKey(currency), new Integer(sa)))) - : new Account(new AccountState( - world.GetAccount(ReservedAddresses.LegacyAccount).Trie - .Set(ToFungibleAssetKey(address, currency), new Integer(amount)))); - - return world.SetAccount(ReservedAddresses.LegacyAccount, account); - } } } From 5980a822bfb089e71d5f48c89fe7192134f2770d Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Fri, 3 May 2024 10:53:35 +0900 Subject: [PATCH 3/8] Added migration to CurrencyAccountProtocolVersion --- .../State/IStateStoreExtensions.cs | 102 ++++++++++++++++++ Libplanet.Mocks/MockWorldState.cs | 43 ++++++-- Libplanet.Tests/Action/WorldTest.cs | 2 +- Libplanet.Tests/Action/WorldV7Test.cs | 16 +++ 4 files changed, 152 insertions(+), 11 deletions(-) create mode 100644 Libplanet.Tests/Action/WorldV7Test.cs diff --git a/Libplanet.Action/State/IStateStoreExtensions.cs b/Libplanet.Action/State/IStateStoreExtensions.cs index 99463205349..5b2ff4658f2 100644 --- a/Libplanet.Action/State/IStateStoreExtensions.cs +++ b/Libplanet.Action/State/IStateStoreExtensions.cs @@ -1,7 +1,12 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; using System.Security.Cryptography; +using System.Text; using Bencodex.Types; using Libplanet.Common; +using Libplanet.Crypto; using Libplanet.Store; using Libplanet.Store.Trie; using Libplanet.Types.Blocks; @@ -152,6 +157,103 @@ internal static IWorld MigrateWorld( } } + // Migrate up to BlockMetadata.ValidatorSetAccountProtocolVersion + // if conditions are met. + if (targetVersion >= BlockMetadata.CurrencyAccountProtocolVersion && + world.Version < BlockMetadata.CurrencyAccountProtocolVersion) + { + var worldTrie = world.Trie; + worldTrie = worldTrie.SetMetadata( + new TrieMetadata(BlockMetadata.CurrencyAccountProtocolVersion)); + worldTrie = stateStore.Commit(worldTrie); + world = new World(new WorldBaseState(worldTrie, stateStore)); + + // Remove all total supply tracking. + const int totalSupplyKeyLength = 42; + var subRootPath = new KeyBytes(Encoding.ASCII.GetBytes("__")); + var legacyAccountTrie = + world.GetAccount(ReservedAddresses.LegacyAccount).Trie; + var tempTrie = (MerkleTrie)legacyAccountTrie.Set(subRootPath, Null.Value); + foreach (var pair in tempTrie.IterateSubTrieValues(subRootPath)) + { + if (pair.Path.Length == totalSupplyKeyLength) + { + legacyAccountTrie = legacyAccountTrie.Remove(pair.Path); + } + } + + legacyAccountTrie = stateStore.Commit(legacyAccountTrie); + worldTrie = worldTrie.Set( + KeyConverters.ToStateKey(ReservedAddresses.LegacyAccount), + new Binary(legacyAccountTrie.Hash.ByteArray)); + worldTrie = stateStore.Commit(worldTrie); + world = new World(new WorldBaseState(worldTrie, stateStore)); + + // Remove all fungible assets + const int fungibleAssetKeyLength = 82; + subRootPath = new KeyBytes(Encoding.ASCII.GetBytes("_")); + tempTrie = (MerkleTrie)legacyAccountTrie.Set(subRootPath, Null.Value); + byte[] addressBytesBuffer = new byte[40]; + byte[] currencyBytesBuffer = new byte[40]; + List<(KeyBytes Address, KeyBytes Currency, Integer Amount)> favs = + new List<(KeyBytes Address, KeyBytes Currency, Integer Amount)>(); + foreach (var pair in tempTrie.IterateSubTrieValues(subRootPath)) + { + if (pair.Path.Length == fungibleAssetKeyLength) + { + legacyAccountTrie = legacyAccountTrie.Remove(pair.Path); + pair.Path.ByteArray.CopyTo(1, addressBytesBuffer, 0, 40); + pair.Path.ByteArray.CopyTo(42, currencyBytesBuffer, 0, 40); + favs.Add(( + new KeyBytes(addressBytesBuffer), + new KeyBytes(currencyBytesBuffer), + (Integer)pair.Value)); + } + } + + legacyAccountTrie = stateStore.Commit(legacyAccountTrie); + worldTrie = worldTrie.Set( + KeyConverters.ToStateKey(ReservedAddresses.LegacyAccount), + new Binary(legacyAccountTrie.Hash.ByteArray)); + worldTrie = stateStore.Commit(worldTrie); + world = new World(new WorldBaseState(worldTrie, stateStore)); + + // Add in fungible assets to new accounts. + KeyBytes totalSupplyKeyBytes = + KeyConverters.ToStateKey(CurrencyAccount.TotalSupplyAddress); + var grouped = favs.GroupBy(fav => fav.Currency); + foreach (var group in grouped) + { + var currencyAccountTrie = world.Trie.Get(group.Key) is Binary hash + ? stateStore.GetStateRoot(new HashDigest(hash)) + : stateStore.GetStateRoot(null); + foreach (var fav in group) + { + Integer balance = fav.Amount; + Integer prevTotalSupply = + currencyAccountTrie.Get(totalSupplyKeyBytes) is Integer i + ? i + : new Integer(0); + Integer newTotalSupply = new Integer(prevTotalSupply.Value + balance.Value); + currencyAccountTrie = + currencyAccountTrie.Set( + fav.Address, + balance); + currencyAccountTrie = + currencyAccountTrie.Set( + totalSupplyKeyBytes, + newTotalSupply); + } + + currencyAccountTrie = stateStore.Commit(currencyAccountTrie); + worldTrie = worldTrie.Set( + group.Key, + new Binary(currencyAccountTrie.Hash.ByteArray)); + worldTrie = stateStore.Commit(worldTrie); + world = new World(new WorldBaseState(worldTrie, stateStore)); + } + } + // Migrate up to target version if conditions are met. if (targetVersion >= BlockMetadata.WorldStateProtocolVersion && world.Version < targetVersion) diff --git a/Libplanet.Mocks/MockWorldState.cs b/Libplanet.Mocks/MockWorldState.cs index 0f7a0cf8695..9764d4ba7cd 100644 --- a/Libplanet.Mocks/MockWorldState.cs +++ b/Libplanet.Mocks/MockWorldState.cs @@ -170,26 +170,49 @@ public MockWorldState SetBalance(Address address, FungibleAssetValue value) => /// public MockWorldState SetBalance(Address address, Currency currency, Integer rawValue) { - ITrie trie = GetAccountState(ReservedAddresses.LegacyAccount).Trie; - - if (currency.TotalSupplyTrackable) + if (Version >= BlockMetadata.CurrencyAccountProtocolVersion) { - Integer balance = trie.Get(ToFungibleAssetKey(address, currency)) is Integer b + Address accountAddress = new Address(currency.Hash.ByteArray); + KeyBytes balanceKey = ToStateKey(address); + KeyBytes totalSupplyKey = ToStateKey(CurrencyAccount.TotalSupplyAddress); + + ITrie trie = GetAccountState(accountAddress).Trie; + Integer balance = trie.Get(balanceKey) is Integer b ? b : new Integer(0); - Integer totalSupply = trie.Get(ToTotalSupplyKey(currency)) is Integer t + Integer totalSupply = trie.Get(totalSupplyKey) is Integer t ? t : new Integer(0); + trie = trie.Set( - ToTotalSupplyKey(currency), + totalSupplyKey, new Integer(totalSupply.Value - balance.Value + rawValue.Value)); + trie = trie.Set(balanceKey, rawValue); + return SetAccount(accountAddress, new Account(new AccountState(trie))); } + else + { + Address accountAddress = ReservedAddresses.LegacyAccount; + KeyBytes balanceKey = ToFungibleAssetKey(address, currency); + KeyBytes totalSupplyKey = ToTotalSupplyKey(currency); - trie = trie.Set( - ToFungibleAssetKey(address, currency), - rawValue); + ITrie trie = GetAccountState(accountAddress).Trie; + if (currency.TotalSupplyTrackable) + { + Integer balance = trie.Get(balanceKey) is Integer b + ? b + : new Integer(0); + Integer totalSupply = trie.Get(totalSupplyKey) is Integer t + ? t + : new Integer(0); + trie = trie.Set( + totalSupplyKey, + new Integer(totalSupply.Value - balance.Value + rawValue.Value)); + } - return SetAccount(ReservedAddresses.LegacyAccount, new AccountState(trie)); + trie = trie.Set(balanceKey, rawValue); + return SetAccount(accountAddress, new AccountState(trie)); + } } public MockWorldState SetValidatorSet(ValidatorSet validatorSet) diff --git a/Libplanet.Tests/Action/WorldTest.cs b/Libplanet.Tests/Action/WorldTest.cs index f3deb0fbb14..5a2e283f623 100644 --- a/Libplanet.Tests/Action/WorldTest.cs +++ b/Libplanet.Tests/Action/WorldTest.cs @@ -474,7 +474,7 @@ public void TotalSupplyTracking() // While not specifically tied to a protocol version, // Total supply tracking was introduced while the block protocol version // was at 4. - if (ProtocolVersion > BlockMetadata.PBFTProtocolVersion) + if (ProtocolVersion >= BlockMetadata.PBFTProtocolVersion) { IWorld world = _initWorld; IActionContext context = _initContext; diff --git a/Libplanet.Tests/Action/WorldV7Test.cs b/Libplanet.Tests/Action/WorldV7Test.cs new file mode 100644 index 00000000000..3be033f4cbf --- /dev/null +++ b/Libplanet.Tests/Action/WorldV7Test.cs @@ -0,0 +1,16 @@ +using Libplanet.Types.Blocks; +using Xunit.Abstractions; + +namespace Libplanet.Tests.Action +{ + public class WorldV7Test : WorldTest + { + public WorldV7Test(ITestOutputHelper output) + : base(output) + { + } + + public override int ProtocolVersion { get; } = + BlockMetadata.CurrencyAccountProtocolVersion; + } +} From 6dc60ac70a67153b9eb6a3310447f6572fe2135e Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Tue, 7 May 2024 16:18:05 +0900 Subject: [PATCH 4/8] Added FAVs to migration test and updated world test --- .../Action/ActionEvaluatorTest.Migration.cs | 189 +++++++++++++++++- Libplanet.Tests/Action/WorldTest.cs | 26 ++- 2 files changed, 207 insertions(+), 8 deletions(-) diff --git a/Libplanet.Tests/Action/ActionEvaluatorTest.Migration.cs b/Libplanet.Tests/Action/ActionEvaluatorTest.Migration.cs index af91fc8e4e6..7d788e440a0 100644 --- a/Libplanet.Tests/Action/ActionEvaluatorTest.Migration.cs +++ b/Libplanet.Tests/Action/ActionEvaluatorTest.Migration.cs @@ -8,8 +8,10 @@ using Libplanet.Blockchain.Policies; using Libplanet.Common; using Libplanet.Crypto; +using Libplanet.Mocks; using Libplanet.Store; using Libplanet.Store.Trie; +using Libplanet.Types.Assets; using Libplanet.Types.Blocks; using Libplanet.Types.Consensus; using Libplanet.Types.Tx; @@ -22,13 +24,17 @@ namespace Libplanet.Tests.Action public partial class ActionEvaluatorTest { [Fact] - public void MigrateWorldWithValidatorSet() + public void MigrateWorld() { var stateStore = new TrieStateStore(new MemoryKeyValueStore()); var actionEvaluator = new ActionEvaluator( _ => null, stateStore, new SingleActionLoader(typeof(DumbAction))); +#pragma warning disable CS0618 + var legacyCurrency = Currency.Legacy("LEG", 2, null); +#pragma warning restore CS0618 + var modernCurrency = Currency.Uncapped("MOD", 4, null); var address = new PrivateKey().Address; var value = new Text("Foo"); @@ -38,11 +44,17 @@ public void MigrateWorldWithValidatorSet() .Select(_ => new Validator(new PrivateKey().PublicKey, 1)) .ToList()); - var trie0 = stateStore.GetStateRoot(null); + // Throwaway addresses are used to differentiate balance and total supply. + var world0 = new World(MockWorldState.CreateLegacy(stateStore) + .SetBalance(address, legacyCurrency, 123) + .SetBalance(address, modernCurrency, 456) + .SetBalance(new PrivateKey().Address, legacyCurrency, 1000) + .SetBalance(new PrivateKey().Address, modernCurrency, 2000) + .SetValidatorSet(validatorSet)); + var trie0 = world0.Trie; trie0 = trie0.Set(KeyConverters.ToStateKey(address), value); - trie0 = trie0.Set(KeyConverters.ValidatorSetKey, validatorSet.Bencoded); trie0 = stateStore.Commit(trie0); - var world0 = new World(new WorldBaseState(trie0, stateStore)); + world0 = new World(new WorldBaseState(trie0, stateStore)); var world4 = stateStore.MigrateWorld( world0, BlockMetadata.PBFTProtocolVersion); @@ -59,6 +71,27 @@ public void MigrateWorldWithValidatorSet() Assert.Equal( validatorSet, world4.GetValidatorSet()); + Assert.Equal( + new Integer(123), + world4.Trie.Get(KeyConverters.ToFungibleAssetKey(address, legacyCurrency))); + Assert.Equal( + new Integer(456), + world4.Trie.Get(KeyConverters.ToFungibleAssetKey(address, modernCurrency))); + Assert.Equal( + FungibleAssetValue.FromRawValue(legacyCurrency, 123), + world4.GetBalance(address, legacyCurrency)); + Assert.Equal( + FungibleAssetValue.FromRawValue(modernCurrency, 456), + world4.GetBalance(address, modernCurrency)); + Assert.Null(world4.Trie.Get(KeyConverters.ToTotalSupplyKey(legacyCurrency))); + Assert.Equal( + new Integer(2456), + world4.Trie.Get(KeyConverters.ToTotalSupplyKey(modernCurrency))); + Assert.Throws(() => + world4.GetTotalSupply(legacyCurrency)); + Assert.Equal( + FungibleAssetValue.FromRawValue(modernCurrency, 2456), + world4.GetTotalSupply(modernCurrency)); var world5 = stateStore.MigrateWorld( world0, BlockMetadata.WorldStateProtocolVersion); @@ -77,6 +110,40 @@ public void MigrateWorldWithValidatorSet() .Trie .Get(KeyConverters.ValidatorSetKey)); Assert.Equal(validatorSet, world5.GetValidatorSet()); + Assert.Equal( + new Integer(123), + world5 + .GetAccount(ReservedAddresses.LegacyAccount) + .Trie + .Get(KeyConverters.ToFungibleAssetKey(address, legacyCurrency))); + Assert.Equal( + new Integer(456), + world5 + .GetAccount(ReservedAddresses.LegacyAccount) + .Trie + .Get(KeyConverters.ToFungibleAssetKey(address, modernCurrency))); + Assert.Equal( + FungibleAssetValue.FromRawValue(legacyCurrency, 123), + world5.GetBalance(address, legacyCurrency)); + Assert.Equal( + FungibleAssetValue.FromRawValue(modernCurrency, 456), + world5.GetBalance(address, modernCurrency)); + Assert.Null( + world5 + .GetAccount(ReservedAddresses.LegacyAccount) + .Trie + .Get(KeyConverters.ToTotalSupplyKey(legacyCurrency))); + Assert.Equal( + new Integer(2456), + world5 + .GetAccount(ReservedAddresses.LegacyAccount) + .Trie + .Get(KeyConverters.ToTotalSupplyKey(modernCurrency))); + Assert.Throws(() => + world5.GetTotalSupply(legacyCurrency)); + Assert.Equal( + FungibleAssetValue.FromRawValue(modernCurrency, 2456), + world5.GetTotalSupply(modernCurrency)); var world6 = stateStore.MigrateWorld( world0, BlockMetadata.ValidatorSetAccountProtocolVersion); @@ -101,6 +168,120 @@ public void MigrateWorldWithValidatorSet() .Trie .Get(KeyConverters.ToStateKey(ValidatorSetAccount.ValidatorSetAddress))); Assert.Equal(validatorSet, world6.GetValidatorSet()); + Assert.Equal( + new Integer(123), + world6 + .GetAccount(ReservedAddresses.LegacyAccount) + .Trie + .Get(KeyConverters.ToFungibleAssetKey(address, legacyCurrency))); + Assert.Equal( + new Integer(456), + world6 + .GetAccount(ReservedAddresses.LegacyAccount) + .Trie + .Get(KeyConverters.ToFungibleAssetKey(address, modernCurrency))); + Assert.Equal( + FungibleAssetValue.FromRawValue(legacyCurrency, 123), + world6.GetBalance(address, legacyCurrency)); + Assert.Equal( + FungibleAssetValue.FromRawValue(modernCurrency, 456), + world6.GetBalance(address, modernCurrency)); + Assert.Null( + world6 + .GetAccount(ReservedAddresses.LegacyAccount) + .Trie + .Get(KeyConverters.ToTotalSupplyKey(legacyCurrency))); + Assert.Equal( + new Integer(2456), + world6 + .GetAccount(ReservedAddresses.LegacyAccount) + .Trie + .Get(KeyConverters.ToTotalSupplyKey(modernCurrency))); + Assert.Throws(() => + world6.GetTotalSupply(legacyCurrency)); + Assert.Equal( + FungibleAssetValue.FromRawValue(modernCurrency, 2456), + world6.GetTotalSupply(modernCurrency)); + + var world7 = stateStore.MigrateWorld( + world0, BlockMetadata.CurrencyAccountProtocolVersion); + Assert.True(world7.Trie.Recorded); + Assert.Equal(7, world7.Version); + Assert.NotEqual(world0.Trie.Hash, world7.Trie.Hash); + Assert.NotEqual(world6.Trie.Hash, world7.Trie.Hash); + Assert.Null(world7.Trie.Get(KeyConverters.ToStateKey(address))); + Assert.Equal( + value, + world7.GetAccount(ReservedAddresses.LegacyAccount).GetState(address)); + Assert.Null(world7.Trie.Get(KeyConverters.ValidatorSetKey)); + Assert.Null( + world7 + .GetAccount(ReservedAddresses.LegacyAccount) + .Trie + .Get(KeyConverters.ValidatorSetKey)); + Assert.Equal( + validatorSet.Bencoded, + world7 + .GetAccount(ReservedAddresses.ValidatorSetAccount) + .Trie + .Get(KeyConverters.ToStateKey(ValidatorSetAccount.ValidatorSetAddress))); + Assert.Equal(validatorSet, world7.GetValidatorSet()); + Assert.Null( + world7 + .GetAccount(ReservedAddresses.LegacyAccount) + .Trie + .Get(KeyConverters.ToFungibleAssetKey(address, legacyCurrency))); + Assert.Equal( + new Integer(123), + world7 + .GetAccount(new Address(legacyCurrency.Hash.ByteArray)) + .Trie + .Get(KeyConverters.ToStateKey(address))); + Assert.Null( + world7 + .GetAccount(ReservedAddresses.LegacyAccount) + .Trie + .Get(KeyConverters.ToFungibleAssetKey(address, modernCurrency))); + Assert.Equal( + new Integer(456), + world7 + .GetAccount(new Address(modernCurrency.Hash.ByteArray)) + .Trie + .Get(KeyConverters.ToStateKey(address))); + Assert.Equal( + FungibleAssetValue.FromRawValue(legacyCurrency, 123), + world7.GetBalance(address, legacyCurrency)); + Assert.Equal( + FungibleAssetValue.FromRawValue(modernCurrency, 456), + world7.GetBalance(address, modernCurrency)); + Assert.Null( + world7 + .GetAccount(ReservedAddresses.LegacyAccount) + .Trie + .Get(KeyConverters.ToTotalSupplyKey(legacyCurrency))); + Assert.Equal( + new Integer(1123), + world7 + .GetAccount(new Address(legacyCurrency.Hash.ByteArray)) + .Trie + .Get(KeyConverters.ToStateKey(CurrencyAccount.TotalSupplyAddress))); + Assert.Null( + world7 + .GetAccount(ReservedAddresses.LegacyAccount) + .Trie + .Get(KeyConverters.ToTotalSupplyKey(modernCurrency))); + Assert.Equal( + new Integer(2456), + world7 + .GetAccount(new Address(modernCurrency.Hash.ByteArray)) + .Trie + .Get(KeyConverters.ToStateKey(CurrencyAccount.TotalSupplyAddress))); + Assert.Equal( + FungibleAssetValue.FromRawValue(legacyCurrency, 1123), + world7.GetTotalSupply(legacyCurrency)); + Assert.Equal( + FungibleAssetValue.FromRawValue(modernCurrency, 2456), + world7.GetTotalSupply(modernCurrency)); } [Fact] diff --git a/Libplanet.Tests/Action/WorldTest.cs b/Libplanet.Tests/Action/WorldTest.cs index 5a2e283f623..e9c5f91c56a 100644 --- a/Libplanet.Tests/Action/WorldTest.cs +++ b/Libplanet.Tests/Action/WorldTest.cs @@ -479,16 +479,34 @@ public void TotalSupplyTracking() IWorld world = _initWorld; IActionContext context = _initContext; - Assert.Throws(() => - world.GetTotalSupply(_currencies[0])); + if (ProtocolVersion >= BlockMetadata.CurrencyAccountProtocolVersion) + { + Assert.Equal( + Value(0, 5), + world.GetTotalSupply(_currencies[0])); + } + else + { + Assert.Throws(() => + world.GetTotalSupply(_currencies[0])); + } Assert.Equal( Value(4, 5), _initWorld.GetTotalSupply(_currencies[4])); world = world.MintAsset(context, _addr[0], Value(0, 10)); - Assert.Throws(() => - world.GetTotalSupply(_currencies[0])); + if (ProtocolVersion >= BlockMetadata.CurrencyAccountProtocolVersion) + { + Assert.Equal( + Value(0, 15), + world.GetTotalSupply(_currencies[0])); + } + else + { + Assert.Throws(() => + world.GetTotalSupply(_currencies[0])); + } world = world.MintAsset(context, _addr[0], Value(4, 10)); Assert.Equal( From b1433c92408979c415f8951cbe46df6e063828f4 Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Wed, 8 May 2024 15:54:45 +0900 Subject: [PATCH 5/8] Overhauled CurrencyAccount --- Libplanet.Action/State/CurrencyAccount.cs | 355 +++++++++++----------- 1 file changed, 179 insertions(+), 176 deletions(-) diff --git a/Libplanet.Action/State/CurrencyAccount.cs b/Libplanet.Action/State/CurrencyAccount.cs index 34bc5ff4c42..d092dad71c4 100644 --- a/Libplanet.Action/State/CurrencyAccount.cs +++ b/Libplanet.Action/State/CurrencyAccount.cs @@ -36,30 +36,28 @@ public CurrencyAccount(ITrie trie, int worldVersion, Currency currency) public FungibleAssetValue GetBalance(Address address, Currency currency) { - if (!Currency.Equals(currency)) - { - throw new ArgumentException(); - } - + CheckCurrency(currency); +#pragma warning disable SA1118 // The parameter spans multiple lines return FungibleAssetValue.FromRawValue( Currency, WorldVersion >= BlockMetadata.CurrencyAccountProtocolVersion ? GetRawBalanceV7(address) : GetRawBalanceV0(address)); +#pragma warning restore SA1118 } public FungibleAssetValue GetTotalSupply(Currency currency) { - if (!Currency.Equals(currency)) - { - throw new ArgumentException(); - } - + CheckCurrency(currency); +#pragma warning disable SA1118 // The parameter spans multiple lines return FungibleAssetValue.FromRawValue( Currency, WorldVersion >= BlockMetadata.CurrencyAccountProtocolVersion ? GetRawTotalSupplyV7() - : GetRawTotalSupplyV0()); + : Currency.TotalSupplyTrackable + ? GetRawTotalSupplyV0() + : throw TotalSupplyNotTrackableException.WithDefaultMessage(Currency)); +#pragma warning restore SA1118 } public CurrencyAccount MintAsset( @@ -67,29 +65,25 @@ public CurrencyAccount MintAsset( Address recipient, FungibleAssetValue value) { - if (!Currency.Equals(value.Currency)) - { - throw new ArgumentException(); - } - + CheckCurrency(value.Currency); if (value.Sign <= 0) { throw new ArgumentOutOfRangeException( nameof(value), - "The value to mint has to be greater than zero."); + $"The amount to mint, burn, or transfer must be greater than zero: {value}"); } - - if (!Currency.AllowsToMint(context.Signer)) + else if (!Currency.AllowsToMint(context.Signer)) { throw new CurrencyPermissionException( - $"The account {context.Signer} has no permission to mint currency {Currency}.", + $"Given {nameof(context)}'s signer {context.Signer} does not have " + + $"the authority to mint or burn currency {Currency}.", context.Signer, Currency); } return WorldVersion >= BlockMetadata.CurrencyAccountProtocolVersion - ? MintAssetV7(context, recipient, value) - : MintAssetV0(context, recipient, value); + ? MintRawAssetV7(context, recipient, value.RawValue) + : MintRawAssetV0(context, recipient, value.RawValue); } public CurrencyAccount BurnAsset( @@ -97,29 +91,25 @@ public CurrencyAccount BurnAsset( Address owner, FungibleAssetValue value) { - if (!Currency.Equals(value.Currency)) - { - throw new ArgumentException(); - } - + CheckCurrency(value.Currency); if (value.Sign <= 0) { throw new ArgumentOutOfRangeException( nameof(value), - "The value to burn has to be greater than zero."); + $"The amount to mint, burn, or transfer must be greater than zero: {value}"); } - - if (!Currency.AllowsToMint(context.Signer)) + else if (!Currency.AllowsToMint(context.Signer)) { throw new CurrencyPermissionException( - $"The account {context.Signer} has no permission to burn currency {Currency}.", + $"Given {nameof(context)}'s signer {context.Signer} does not have " + + $"the authority to mint or burn currency {Currency}.", context.Signer, Currency); } return WorldVersion >= BlockMetadata.CurrencyAccountProtocolVersion - ? BurnAssetV7(context, owner, value) - : BurnAssetV0(context, owner, value); + ? BurnRawAssetV7(context, owner, value.RawValue) + : BurnRawAssetV0(context, owner, value.RawValue); } public CurrencyAccount TransferAsset( @@ -128,21 +118,17 @@ public CurrencyAccount TransferAsset( Address recipient, FungibleAssetValue value) { - if (!Currency.Equals(value.Currency)) - { - throw new ArgumentException(); - } - + CheckCurrency(value.Currency); if (value.Sign <= 0) { throw new ArgumentOutOfRangeException( nameof(value), - "The value to transfer has to be greater than zero."); + $"The amount to mint, burn, or transfer must be greater than zero: {value}"); } return WorldVersion >= BlockMetadata.CurrencyAccountProtocolVersion - ? TransferAssetV7(context, sender, recipient, value) - : TransferAssetV0(context, sender, recipient, value); + ? TransferRawAssetV7(context, sender, recipient, value.RawValue) + : TransferRawAssetV0(context, sender, recipient, value.RawValue); } public IAccount AsAccount() @@ -150,247 +136,264 @@ public IAccount AsAccount() return new Account(new AccountState(Trie)); } - private CurrencyAccount MintAssetV0( + private CurrencyAccount MintRawAssetV0( IActionContext context, Address recipient, - FungibleAssetValue value) + BigInteger rawValue) { - ValidateMinter(context.Signer); CurrencyAccount currencyAccount = this; - BigInteger prevAmount = currencyAccount.GetRawBalanceV0(recipient); - BigInteger newAmount = prevAmount + value.RawValue; - currencyAccount = currencyAccount.WriteBalanceV0(recipient, newAmount); - + BigInteger prevBalanceRawValue = currencyAccount.GetRawBalanceV0(recipient); + currencyAccount = + currencyAccount.WriteRawBalanceV0(recipient, prevBalanceRawValue + rawValue); if (Currency.TotalSupplyTrackable) { - // NOTE: This should never throw an exception. - BigInteger prevTotalSupply = currencyAccount.GetRawTotalSupplyV0(); + BigInteger prevTotalSupplyRawValue = currencyAccount.GetRawTotalSupplyV0(); + if (Currency.MaximumSupply is { } maximumSupply && + maximumSupply.RawValue < prevTotalSupplyRawValue + rawValue) + { + FungibleAssetValue prevTotalSupply = + FungibleAssetValue.FromRawValue(Currency, prevTotalSupplyRawValue); + FungibleAssetValue value = + FungibleAssetValue.FromRawValue(Currency, rawValue); + throw new SupplyOverflowException( + $"Cannot mint {value} in addition to " + + $"the current total supply of {prevTotalSupply} as it would exceed " + + $"the maximum supply {Currency.MaximumSupply}.", + value); + } + currencyAccount = - currencyAccount.WriteTotalSupplyV0(prevTotalSupply + value.RawValue); + currencyAccount.WriteRawTotalSupplyV0(prevTotalSupplyRawValue + rawValue); } return currencyAccount; } - private CurrencyAccount MintAssetV7( + private CurrencyAccount MintRawAssetV7( IActionContext context, Address recipient, - FungibleAssetValue value) + BigInteger rawValue) { - ValidateMinter(context.Signer); CurrencyAccount currencyAccount = this; - BigInteger prevAmount = currencyAccount.GetRawBalanceV7(recipient); - BigInteger newAmount = prevAmount + value.RawValue; + BigInteger prevBalanceRawValue = currencyAccount.GetRawBalanceV7(recipient); currencyAccount = - currencyAccount.WriteBalanceV7(recipient, newAmount); - BigInteger prevTotalSupply = currencyAccount.GetRawTotalSupplyV7(); + currencyAccount.WriteRawBalanceV7(recipient, prevBalanceRawValue + rawValue); + + BigInteger prevTotalSupplyRawValue = currencyAccount.GetRawTotalSupplyV7(); + if (Currency.MaximumSupply is { } maximumSupply && + maximumSupply.RawValue < prevTotalSupplyRawValue + rawValue) + { + FungibleAssetValue prevTotalSupply = + FungibleAssetValue.FromRawValue(Currency, prevTotalSupplyRawValue); + FungibleAssetValue value = + FungibleAssetValue.FromRawValue(Currency, rawValue); + throw new SupplyOverflowException( + $"Cannot mint {value} in addition to " + + $"the current total supply of {prevTotalSupply} as it would exceed " + + $"the maximum supply {Currency.MaximumSupply}.", + prevTotalSupply); + } + currencyAccount = - currencyAccount.WriteTotalSupplyV7(prevTotalSupply + value.RawValue); + currencyAccount.WriteRawTotalSupplyV7(prevTotalSupplyRawValue + rawValue); return currencyAccount; } - private CurrencyAccount BurnAssetV0( + private CurrencyAccount BurnRawAssetV0( IActionContext context, Address owner, - FungibleAssetValue value) + BigInteger rawValue) { - ValidateMinter(context.Signer); CurrencyAccount currencyAccount = this; - BigInteger prevAmount = currencyAccount.GetRawBalanceV0(owner); - BigInteger newAmount = prevAmount - value.RawValue; - currencyAccount = currencyAccount.WriteBalanceV0(owner, newAmount); + BigInteger prevBalanceRawValue = currencyAccount.GetRawBalanceV0(owner); + if (prevBalanceRawValue - rawValue < 0) + { + FungibleAssetValue prevBalance = + FungibleAssetValue.FromRawValue(Currency, prevBalanceRawValue); + FungibleAssetValue value = FungibleAssetValue.FromRawValue(Currency, rawValue); + throw new InsufficientBalanceException( + $"Cannot burn or transfer {value} from {owner} as the current balance " + + $"of {owner} is {prevBalance}.", + owner, + prevBalance); + } + + currencyAccount = + currencyAccount.WriteRawBalanceV0(owner, prevBalanceRawValue - rawValue); if (Currency.TotalSupplyTrackable) { - // NOTE: This should never throw an exception. BigInteger prevTotalSupply = currencyAccount.GetRawTotalSupplyV0(); currencyAccount = - currencyAccount.WriteTotalSupplyV0(prevTotalSupply - value.RawValue); + currencyAccount.WriteRawTotalSupplyV0(prevTotalSupply - rawValue); } return currencyAccount; } - private CurrencyAccount BurnAssetV7( + private CurrencyAccount BurnRawAssetV7( IActionContext context, Address owner, - FungibleAssetValue value) + BigInteger rawValue) { - ValidateMinter(context.Signer); CurrencyAccount currencyAccount = this; - BigInteger prevAmount = currencyAccount.GetRawBalanceV7(owner); - BigInteger newAmount = prevAmount - value.RawValue; - currencyAccount = currencyAccount.WriteBalanceV7(owner, newAmount); - BigInteger prevTotalSupply = currencyAccount.GetRawTotalSupplyV7(); - currencyAccount = currencyAccount.WriteTotalSupplyV7(prevTotalSupply - value.RawValue); + BigInteger prevBalanceRawValue = currencyAccount.GetRawBalanceV7(owner); + if (prevBalanceRawValue - rawValue < 0) + { + FungibleAssetValue prevBalance = + FungibleAssetValue.FromRawValue(Currency, prevBalanceRawValue); + FungibleAssetValue value = FungibleAssetValue.FromRawValue(Currency, rawValue); + throw new InsufficientBalanceException( + $"Cannot burn or transfer {value} from {owner} as the current balance " + + $"of {owner} is {prevBalance}.", + owner, + prevBalance); + } + + currencyAccount = + currencyAccount.WriteRawBalanceV7(owner, prevBalanceRawValue - rawValue); + + BigInteger prevTotalSupplyRawValue = currencyAccount.GetRawTotalSupplyV7(); + currencyAccount = + currencyAccount.WriteRawTotalSupplyV7(prevTotalSupplyRawValue - rawValue); return currencyAccount; } - private CurrencyAccount TransferAssetV7( + private CurrencyAccount TransferRawAssetV7( IActionContext context, Address sender, Address recipient, - FungibleAssetValue value) + BigInteger rawValue) { CurrencyAccount currencyAccount = this; + BigInteger prevSenderBalanceRawValue = currencyAccount.GetRawBalanceV7(sender); + if (prevSenderBalanceRawValue - rawValue < 0) + { + FungibleAssetValue prevSenderBalance = + FungibleAssetValue.FromRawValue(Currency, prevSenderBalanceRawValue); + FungibleAssetValue value = FungibleAssetValue.FromRawValue(Currency, rawValue); + throw new InsufficientBalanceException( + $"Cannot burn or transfer {value} from {sender} as the current balance " + + $"of {sender} is {prevSenderBalance}.", + sender, + prevSenderBalance); + } - BigInteger senderBalance = currencyAccount.GetRawBalanceV7(sender); - currencyAccount = currencyAccount.WriteBalanceV7( + currencyAccount = currencyAccount.WriteRawBalanceV7( sender, - senderBalance - value.RawValue); - BigInteger recipientBalance = currencyAccount.GetRawBalanceV7(recipient); - currencyAccount = currencyAccount.WriteBalanceV7( + prevSenderBalanceRawValue - rawValue); + BigInteger prevRecipientBalanceRawValue = currencyAccount.GetRawBalanceV7(recipient); + currencyAccount = currencyAccount.WriteRawBalanceV7( recipient, - recipientBalance + value.RawValue); + prevRecipientBalanceRawValue + rawValue); return currencyAccount; } - private CurrencyAccount TransferAssetV0( + private CurrencyAccount TransferRawAssetV0( IActionContext context, Address sender, Address recipient, - FungibleAssetValue value) + BigInteger rawValue) { CurrencyAccount currencyAccount = this; + BigInteger prevSenderBalanceRawValue = currencyAccount.GetRawBalanceV0(sender); + if (prevSenderBalanceRawValue - rawValue < 0) + { + FungibleAssetValue prevSenderBalance = + FungibleAssetValue.FromRawValue(Currency, prevSenderBalanceRawValue); + FungibleAssetValue value = FungibleAssetValue.FromRawValue(Currency, rawValue); + throw new InsufficientBalanceException( + $"Cannot burn or transfer {value} from {sender} as the current balance " + + $"of {sender} is {prevSenderBalance}.", + sender, + prevSenderBalance); + } // NOTE: For backward compatibility with the bugged behavior before // protocol version 1. if (context.BlockProtocolVersion == 0) { - BigInteger senderBalance = currencyAccount.GetRawBalanceV0(sender); - BigInteger recipientBalance = currencyAccount.GetRawBalanceV0(recipient); - currencyAccount = currencyAccount.WriteBalanceV0( + BigInteger prevRecipientBalanceRawValue = + currencyAccount.GetRawBalanceV0(recipient); + currencyAccount = currencyAccount.WriteRawBalanceV0( sender, - senderBalance - value.RawValue); - currencyAccount = currencyAccount.WriteBalanceV0( + prevSenderBalanceRawValue - rawValue); + currencyAccount = currencyAccount.WriteRawBalanceV0( recipient, - recipientBalance + value.RawValue); + prevRecipientBalanceRawValue + rawValue); } else { - BigInteger senderBalance = currencyAccount.GetRawBalanceV0(sender); - currencyAccount = currencyAccount.WriteBalanceV0( + currencyAccount = currencyAccount.WriteRawBalanceV0( sender, - senderBalance - value.RawValue); - BigInteger recipientBalance = currencyAccount.GetRawBalanceV0(recipient); - currencyAccount = currencyAccount.WriteBalanceV0( + prevSenderBalanceRawValue - rawValue); + BigInteger prevRecipientBalanceRawValue = + currencyAccount.GetRawBalanceV0(recipient); + currencyAccount = currencyAccount.WriteRawBalanceV0( recipient, - recipientBalance + value.RawValue); + prevRecipientBalanceRawValue + rawValue); } return currencyAccount; } - private CurrencyAccount WriteBalanceV0( - Address address, - BigInteger value) - { - ValidateBalance(value); - return new CurrencyAccount( - Trie.Set(KeyConverters.ToFungibleAssetKey(address, Currency), new Integer(value)), + private CurrencyAccount WriteRawBalanceV0(Address address, BigInteger rawValue) => + new CurrencyAccount( + Trie.Set( + KeyConverters.ToFungibleAssetKey(address, Currency), new Integer(rawValue)), WorldVersion, Currency); - } - private CurrencyAccount WriteBalanceV7(Address address, BigInteger value) - { - ValidateBalance(value); - return new CurrencyAccount( - Trie.Set(KeyConverters.ToStateKey(address), new Integer(value)), + private CurrencyAccount WriteRawBalanceV7(Address address, BigInteger rawValue) => + new CurrencyAccount( + Trie.Set(KeyConverters.ToStateKey(address), new Integer(rawValue)), WorldVersion, Currency); - } - private CurrencyAccount WriteTotalSupplyV0(BigInteger value) - { - ValidateTotalSupply(value); - return new CurrencyAccount( - Trie.Set(KeyConverters.ToTotalSupplyKey(Currency), new Integer(value)), + private CurrencyAccount WriteRawTotalSupplyV0(BigInteger rawValue) => + new CurrencyAccount( + Trie.Set(KeyConverters.ToTotalSupplyKey(Currency), new Integer(rawValue)), WorldVersion, Currency); - } - private CurrencyAccount WriteTotalSupplyV7(BigInteger value) - { - ValidateTotalSupply(value); - return new CurrencyAccount( + private CurrencyAccount WriteRawTotalSupplyV7(BigInteger rawValue) => + new CurrencyAccount( Trie.Set( KeyConverters.ToStateKey(CurrencyAccount.TotalSupplyAddress), - new Integer(value)), + new Integer(rawValue)), WorldVersion, Currency); - } - private BigInteger GetRawBalanceV0(Address address) - { - return Trie.Get( + private BigInteger GetRawBalanceV0(Address address) => + Trie.Get( KeyConverters.ToFungibleAssetKey(address, Currency)) is Integer i ? i.Value : BigInteger.Zero; - } - private BigInteger GetRawBalanceV7(Address address) - { - return Trie.Get(KeyConverters.ToStateKey(address)) is Integer i + private BigInteger GetRawBalanceV7(Address address) => + Trie.Get(KeyConverters.ToStateKey(address)) is Integer i ? i.Value : BigInteger.Zero; - } - private BigInteger GetRawTotalSupplyV0() - { - if (!Currency.TotalSupplyTrackable) - { - throw TotalSupplyNotTrackableException.WithDefaultMessage(Currency); - } - - return Trie.Get(KeyConverters.ToTotalSupplyKey(Currency)) is Integer i + private BigInteger GetRawTotalSupplyV0() => + Trie.Get(KeyConverters.ToTotalSupplyKey(Currency)) is Integer i ? i.Value : BigInteger.Zero; - } - private BigInteger GetRawTotalSupplyV7() - { - return Trie.Get(KeyConverters.ToStateKey(TotalSupplyAddress)) is Integer i + private BigInteger GetRawTotalSupplyV7() => + Trie.Get(KeyConverters.ToStateKey(TotalSupplyAddress)) is Integer i ? i.Value : BigInteger.Zero; - } - - private void ValidateMinter(Address signer) - { - if (!Currency.AllowsToMint(signer)) - { - // CurrencyPermission - throw new ArgumentException(); - } - } - private void ValidateTotalSupply(BigInteger rawAmount) + private void CheckCurrency(Currency currency) { - if (Currency.MaximumSupply is { } maximumSupply && - maximumSupply.RawValue < rawAmount) - { - // SupplyOverflow - throw new SupplyOverflowException( - "Some message", - FungibleAssetValue.FromRawValue(Currency, rawAmount)); - } - else if (rawAmount < BigInteger.Zero) - { - throw new ArgumentException(); - } - } - - private void ValidateBalance(BigInteger rawAmount) - { - if (rawAmount < 0) + if (!Currency.Equals(currency)) { - // InsufficientBalance - throw new InsufficientBalanceException( - "Some message", - new PrivateKey().Address, - FungibleAssetValue.FromRawValue(Currency, rawAmount)); + throw new ArgumentException( + $"Given currency {currency} should match the account's currency {Currency}.", + nameof(currency)); } } } From a82d4f36f05e01e60aeadd0b09b48c3d79616d6a Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Wed, 8 May 2024 16:23:30 +0900 Subject: [PATCH 6/8] Cleanup --- Libplanet.Action/State/CurrencyAccount.cs | 2 +- Libplanet.Action/State/IWorldExtensions.cs | 25 ++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Libplanet.Action/State/CurrencyAccount.cs b/Libplanet.Action/State/CurrencyAccount.cs index d092dad71c4..02a3d3dcb1e 100644 --- a/Libplanet.Action/State/CurrencyAccount.cs +++ b/Libplanet.Action/State/CurrencyAccount.cs @@ -12,7 +12,7 @@ namespace Libplanet.Action.State /// A special "account" for managing starting with /// . /// - public class CurrencyAccount + public sealed class CurrencyAccount { /// /// The location within the account where diff --git a/Libplanet.Action/State/IWorldExtensions.cs b/Libplanet.Action/State/IWorldExtensions.cs index 47844fa399c..e4dfa1aa328 100644 --- a/Libplanet.Action/State/IWorldExtensions.cs +++ b/Libplanet.Action/State/IWorldExtensions.cs @@ -1,13 +1,9 @@ using System; using System.Diagnostics.Contracts; -using System.Numerics; -using Bencodex.Types; using Libplanet.Crypto; -using Libplanet.Store.Trie; using Libplanet.Types.Assets; using Libplanet.Types.Blocks; using Libplanet.Types.Consensus; -using static Libplanet.Action.State.KeyConverters; namespace Libplanet.Action.State { @@ -130,8 +126,25 @@ public static IWorld TransferAsset( /// The total supply of the . /// /// Thrown when the total supply of the - /// given is not trackable. - /// + /// given is not trackable. A 's + /// total supply is considered trackable if one of the following conditions is satisfied: + /// + /// + /// The has that is + /// greater than or equal + /// to . + /// + /// + /// The has + /// as . + /// + /// + /// That is, a 's total supply is tracked regardless of its + /// value if the + /// has that is greater than or equal to + /// . + /// + /// [Pure] public static FungibleAssetValue GetTotalSupply( this IWorldState worldState, From a9f98b38b0f222b36b6155f56f01b1c5fe786b5c Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Wed, 8 May 2024 16:28:13 +0900 Subject: [PATCH 7/8] Changelog --- CHANGES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 697f5a9e22d..3088adad97a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -25,6 +25,8 @@ To be released. ### Added APIs + - (Libplanet.Action) Added `CurrencyAccount` class. [[#3779]] + ### Behavioral changes - (Libplanet.Mocks) `MockWorldState.SetBalance()` now automatically updates @@ -40,6 +42,7 @@ To be released. [#3774]: https://github.com/planetarium/libplanet/pull/3774 [#3775]: https://github.com/planetarium/libplanet/pull/3775 [#3778]: https://github.com/planetarium/libplanet/pull/3778 +[#3779]: https://github.com/planetarium/libplanet/pull/3779 Version 4.4.2 From 4e5fc2fd6d08c8798f3e71dfdbe28b9eb8e93c29 Mon Sep 17 00:00:00 2001 From: Say Cheong Date: Thu, 9 May 2024 16:24:15 +0900 Subject: [PATCH 8/8] Added tests --- .../Action/ActionEvaluatorTest.Migration.cs | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/Libplanet.Tests/Action/ActionEvaluatorTest.Migration.cs b/Libplanet.Tests/Action/ActionEvaluatorTest.Migration.cs index 7d788e440a0..071b64b607f 100644 --- a/Libplanet.Tests/Action/ActionEvaluatorTest.Migration.cs +++ b/Libplanet.Tests/Action/ActionEvaluatorTest.Migration.cs @@ -285,7 +285,7 @@ public void MigrateWorld() } [Fact] - public void MigrateThroughBlock() + public void MigrateThroughBlockWorldState() { var store = new MemoryStore(); var stateStore = new TrieStateStore(new MemoryKeyValueStore()); @@ -337,5 +337,66 @@ public void MigrateThroughBlock() (Text)"foo", accountTrie.Get(KeyConverters.ToStateKey(ModernAction.Address))); } + + [Fact] + public void MigrateThroughBlockCurrencyAccount() + { + var store = new MemoryStore(); + var stateStore = new TrieStateStore(new MemoryKeyValueStore()); + Log.Debug("Test Start."); + var chain = MakeBlockChain( + policy: new BlockPolicy(), + store: store, + stateStore: stateStore, + actionLoader: new SingleActionLoader(typeof(DumbAction)), + protocolVersion: BlockMetadata.WorldStateProtocolVersion - 1); + Assert.Equal(0, chain.GetWorldState().Version); + var miner = new PrivateKey(); + var preEval1 = TestUtils.ProposeNext( + chain.Tip, + miner: miner.PublicKey, + protocolVersion: BlockMetadata.WorldStateProtocolVersion - 1); + var block1 = chain.EvaluateAndSign(preEval1, miner); + var blockCommit = CreateBlockCommit(block1); + chain.Append(block1, blockCommit); + Assert.Equal(0, chain.GetWorldState().Version); + + // A block that doesn't touch any state does not migrate its state. + var block2 = chain.ProposeBlock(miner, blockCommit); + blockCommit = CreateBlockCommit(block2); + chain.Append(block2, blockCommit); + Assert.Equal(0, chain.GetWorldState().Version); + + // Check if after migration, accounts can be created correctly. + var action = DumbAction.Create( + null, + (null, miner.Address, 10)); + + var tx = Transaction.Create( + nonce: 0, + privateKey: miner, + genesisHash: chain.Genesis.Hash, + actions: new[] { action }.ToPlainValues()); + + chain.StageTransaction(tx); + var block3 = chain.ProposeBlock(miner, blockCommit); + chain.Append(block3, CreateBlockCommit(block3)); + Assert.Equal(BlockMetadata.CurrentProtocolVersion, chain.GetWorldState().Version); + + var currencyAccountStateRoot = stateStore + .GetStateRoot(block3.StateRootHash) + .Get(KeyConverters.ToStateKey( + new Address(DumbAction.DumbCurrency.Hash.ByteArray))); + Assert.NotNull(currencyAccountStateRoot); + var currencyAccountTrie = stateStore.GetStateRoot( + new HashDigest(currencyAccountStateRoot)); + Assert.Equal( + new Integer(10), + currencyAccountTrie.Get(KeyConverters.ToStateKey(miner.Address))); + Assert.Equal( + new Integer(10), + currencyAccountTrie.Get( + KeyConverters.ToStateKey(CurrencyAccount.TotalSupplyAddress))); + } } }