From 8a118ed6c913332eeef9ae38276287ab2136909f Mon Sep 17 00:00:00 2001 From: moreal Date: Thu, 27 Jul 2023 13:29:34 +0900 Subject: [PATCH] feat: introduce `Lib9c.StateService` moved from `planetarium/NineChronicles.Headless` --- .../Lib9c.StateService.Shared.csproj | 9 + .../RemoteEvaluationRequest.cs | 6 + .../RemoteEvaluationResponse.cs | 6 + .../Controllers/RemoteEvaluationController.cs | 52 ++++ .Lib9c.StateService/Lib9c.StateService.csproj | 20 ++ .Lib9c.StateService/Program.cs | 40 +++ .../Properties/launchSettings.json | 41 +++ .Lib9c.StateService/appsettings-schema.json | 11 + .../appsettings.Development.json | 8 + .Lib9c.StateService/appsettings.json | 10 + .../ActionEvaluationSerializerTest.cs | 49 +++ ...tionEvaluatorCommonComponents.Tests.csproj | 29 ++ .../Usings.cs | 1 + .../AccountDelta.cs | 65 ++++ .../AccountStateDelta.cs | 293 ++++++++++++++++++ .../AccountStateDeltaMarshaller.cs | 60 ++++ .../ActionContext.cs | 81 +++++ .../ActionContextMarshaller.cs | 77 +++++ .../ActionEvaluation.cs | 27 ++ .../ActionEvaluationMarshaller.cs | 60 ++++ ...ons.ActionEvaluatorCommonComponents.csproj | 13 + .../PreEvaluationBlockMarshaller.cs | 39 +++ .../Random.cs | 14 + .../TransactionMarshaller.cs | 23 ++ ...ensions.RemoteActionEvaluator.Tests.csproj | 29 ++ .../Usings.cs | 1 + .../AssemblyInfo.cs | 3 + ...et.Extensions.RemoteActionEvaluator.csproj | 15 + .../RemoteActionEvaluator.cs | 83 +++++ ...t.Extensions.RemoteBlockChainStates.csproj | 19 ++ .../RemoteBlockChainStates.cs | 47 +++ .../RemoteBlockState.cs | 219 +++++++++++++ Lib9c.sln | 42 +++ 33 files changed, 1492 insertions(+) create mode 100644 .Lib9c.StateService.Shared/Lib9c.StateService.Shared.csproj create mode 100644 .Lib9c.StateService.Shared/RemoteEvaluationRequest.cs create mode 100644 .Lib9c.StateService.Shared/RemoteEvaluationResponse.cs create mode 100644 .Lib9c.StateService/Controllers/RemoteEvaluationController.cs create mode 100644 .Lib9c.StateService/Lib9c.StateService.csproj create mode 100644 .Lib9c.StateService/Program.cs create mode 100644 .Lib9c.StateService/Properties/launchSettings.json create mode 100644 .Lib9c.StateService/appsettings-schema.json create mode 100644 .Lib9c.StateService/appsettings.Development.json create mode 100644 .Lib9c.StateService/appsettings.json create mode 100644 .Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/ActionEvaluationSerializerTest.cs create mode 100644 .Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests.csproj create mode 100644 .Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/Usings.cs create mode 100644 .Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountDelta.cs create mode 100644 .Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountStateDelta.cs create mode 100644 .Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountStateDeltaMarshaller.cs create mode 100644 .Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContext.cs create mode 100644 .Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContextMarshaller.cs create mode 100644 .Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionEvaluation.cs create mode 100644 .Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionEvaluationMarshaller.cs create mode 100644 .Libplanet.Extensions.ActionEvaluatorCommonComponents/Libplanet.Extensions.ActionEvaluatorCommonComponents.csproj create mode 100644 .Libplanet.Extensions.ActionEvaluatorCommonComponents/PreEvaluationBlockMarshaller.cs create mode 100644 .Libplanet.Extensions.ActionEvaluatorCommonComponents/Random.cs create mode 100644 .Libplanet.Extensions.ActionEvaluatorCommonComponents/TransactionMarshaller.cs create mode 100644 .Libplanet.Extensions.RemoteActionEvaluator.Tests/Libplanet.Extensions.RemoteActionEvaluator.Tests.csproj create mode 100644 .Libplanet.Extensions.RemoteActionEvaluator.Tests/Usings.cs create mode 100644 .Libplanet.Extensions.RemoteActionEvaluator/AssemblyInfo.cs create mode 100644 .Libplanet.Extensions.RemoteActionEvaluator/Libplanet.Extensions.RemoteActionEvaluator.csproj create mode 100644 .Libplanet.Extensions.RemoteActionEvaluator/RemoteActionEvaluator.cs create mode 100644 .Libplanet.Extensions.RemoteBlockChainStates/Libplanet.Extensions.RemoteBlockChainStates.csproj create mode 100644 .Libplanet.Extensions.RemoteBlockChainStates/RemoteBlockChainStates.cs create mode 100644 .Libplanet.Extensions.RemoteBlockChainStates/RemoteBlockState.cs diff --git a/.Lib9c.StateService.Shared/Lib9c.StateService.Shared.csproj b/.Lib9c.StateService.Shared/Lib9c.StateService.Shared.csproj new file mode 100644 index 0000000000..132c02c59c --- /dev/null +++ b/.Lib9c.StateService.Shared/Lib9c.StateService.Shared.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/.Lib9c.StateService.Shared/RemoteEvaluationRequest.cs b/.Lib9c.StateService.Shared/RemoteEvaluationRequest.cs new file mode 100644 index 0000000000..e5cee6ebb3 --- /dev/null +++ b/.Lib9c.StateService.Shared/RemoteEvaluationRequest.cs @@ -0,0 +1,6 @@ +namespace Lib9c.StateService.Shared; + +public class RemoteEvaluationRequest +{ + public byte[] PreEvaluationBlock { get; set; } +} diff --git a/.Lib9c.StateService.Shared/RemoteEvaluationResponse.cs b/.Lib9c.StateService.Shared/RemoteEvaluationResponse.cs new file mode 100644 index 0000000000..8aed77d193 --- /dev/null +++ b/.Lib9c.StateService.Shared/RemoteEvaluationResponse.cs @@ -0,0 +1,6 @@ +namespace Lib9c.StateService.Shared; + +public class RemoteEvaluationResponse +{ + public byte[][] Evaluations { get; set; } +} diff --git a/.Lib9c.StateService/Controllers/RemoteEvaluationController.cs b/.Lib9c.StateService/Controllers/RemoteEvaluationController.cs new file mode 100644 index 0000000000..75adcc99ae --- /dev/null +++ b/.Lib9c.StateService/Controllers/RemoteEvaluationController.cs @@ -0,0 +1,52 @@ +using Bencodex; +using Bencodex.Types; +using Lib9c.StateService.Shared; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Extensions.ActionEvaluatorCommonComponents; +using Microsoft.AspNetCore.Mvc; +using Nekoyume.Action; +using Nekoyume.Action.Loader; + +namespace Lib9c.StateService.Controllers; + +[ApiController] +[Route("/evaluation")] +public class RemoteEvaluationController : ControllerBase +{ + private readonly IBlockChainStates _blockChainStates; + private readonly ILogger _logger; + private readonly Codec _codec; + + public RemoteEvaluationController( + IBlockChainStates blockChainStates, + ILogger logger, + Codec codec) + { + _blockChainStates = blockChainStates; + _logger = logger; + _codec = codec; + } + + [HttpPost] + public ActionResult GetEvaluation([FromBody] RemoteEvaluationRequest request) + { + var decoded = _codec.Decode(request.PreEvaluationBlock); + if (decoded is not Dictionary dictionary) + { + return StatusCode(StatusCodes.Status400BadRequest); + } + + var preEvaluationBlock = PreEvaluationBlockMarshaller.Unmarshal(dictionary); + var actionEvaluator = + new ActionEvaluator( + context => new RewardGold(), + _blockChainStates, + new NCActionLoader()); + return Ok(new RemoteEvaluationResponse + { + Evaluations = actionEvaluator.Evaluate(preEvaluationBlock).Select(ActionEvaluationMarshaller.Serialize) + .ToArray(), + }); + } +} diff --git a/.Lib9c.StateService/Lib9c.StateService.csproj b/.Lib9c.StateService/Lib9c.StateService.csproj new file mode 100644 index 0000000000..f4385886e6 --- /dev/null +++ b/.Lib9c.StateService/Lib9c.StateService.csproj @@ -0,0 +1,20 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + diff --git a/.Lib9c.StateService/Program.cs b/.Lib9c.StateService/Program.cs new file mode 100644 index 0000000000..a08391d0a1 --- /dev/null +++ b/.Lib9c.StateService/Program.cs @@ -0,0 +1,40 @@ +using Bencodex; +using Libplanet.Action.State; +using Libplanet.Extensions.RemoteBlockChainStates; + +var builder = WebApplication.CreateBuilder(args); + +builder.Configuration.AddEnvironmentVariables(); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.AddSingleton(); + +builder.Services.AddSingleton(_ => +{ + const string DefaultEndpoint = "http://localhost:31280/graphql/explorer"; + var endpoint = builder.Configuration.GetValue("RemoteBlockChainStatesEndpoint") ?? DefaultEndpoint; + return new RemoteBlockChainStates(new Uri(endpoint)); +}); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/.Lib9c.StateService/Properties/launchSettings.json b/.Lib9c.StateService/Properties/launchSettings.json new file mode 100644 index 0000000000..6a1d8fda1b --- /dev/null +++ b/.Lib9c.StateService/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:25712", + "sslPort": 44330 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5157", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7140;http://localhost:5157", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/.Lib9c.StateService/appsettings-schema.json b/.Lib9c.StateService/appsettings-schema.json new file mode 100644 index 0000000000..168a36a46b --- /dev/null +++ b/.Lib9c.StateService/appsettings-schema.json @@ -0,0 +1,11 @@ +{ + "type": "object", + "description": "appsettings.json to configure application.", + "properties": { + "RemoteBlockChainStatesEndpoint": { + "type": "string", + "description": "The headless' Libplanet.Explorer GraphQL endpoint. (e.g., http://localhost/graphql/explorer)" + } + }, + "required": ["RemoteBlockChainStatesEndpoint"] +} diff --git a/.Lib9c.StateService/appsettings.Development.json b/.Lib9c.StateService/appsettings.Development.json new file mode 100644 index 0000000000..0c208ae918 --- /dev/null +++ b/.Lib9c.StateService/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/.Lib9c.StateService/appsettings.json b/.Lib9c.StateService/appsettings.json new file mode 100644 index 0000000000..e3001fa1aa --- /dev/null +++ b/.Lib9c.StateService/appsettings.json @@ -0,0 +1,10 @@ +{ + "$schema": "./appsettings-schema.json", + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/.Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/ActionEvaluationSerializerTest.cs b/.Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/ActionEvaluationSerializerTest.cs new file mode 100644 index 0000000000..a10649ffe8 --- /dev/null +++ b/.Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/ActionEvaluationSerializerTest.cs @@ -0,0 +1,49 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Crypto; + +namespace Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests; + +public class ActionEvaluationSerializerTest +{ + [Fact] + public void Serialization() + { + var addresses = Enumerable.Repeat(0, 4).Select(_ => new PrivateKey().ToAddress()).ToImmutableList(); + AccountStateDelta outputStates = (AccountStateDelta)new AccountStateDelta() + .SetState(addresses[0], Null.Value) + .SetState(addresses[1], (Text)"foo") + .SetState(addresses[2], new List((Text)"bar")); + + var previousStates = new AccountStateDelta(); + + var actionEvaluation = new ActionEvaluation( + Null.Value, + new ActionContext(null, + addresses[0], + null, + addresses[1], + 0, + 0, + false, + previousStates, + new Random(123), + null, + true), + outputStates, + new Libplanet.Action.UnexpectedlyTerminatedActionException("", null, null, null, null, new NullAction(), null)); + var serialized = ActionEvaluationMarshaller.Serialize(actionEvaluation); + var deserialized = ActionEvaluationMarshaller.Deserialize(serialized); + + Assert.Equal(Null.Value, deserialized.Action); + Assert.Equal(123, deserialized.InputContext.Random.Seed); + Assert.Equal(0, deserialized.InputContext.BlockIndex); + Assert.Equal(0, deserialized.InputContext.BlockProtocolVersion); + Assert.Equal(addresses[0], deserialized.InputContext.Signer); + Assert.Equal(addresses[1], deserialized.InputContext.Miner); + Assert.Equal(Null.Value, deserialized.OutputState.GetState(addresses[0])); + Assert.Equal((Text)"foo", deserialized.OutputState.GetState(addresses[1])); + Assert.Equal(new List((Text)"bar"), deserialized.OutputState.GetState(addresses[2])); + } +} diff --git a/.Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests.csproj b/.Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests.csproj new file mode 100644 index 0000000000..1dfcb77ebb --- /dev/null +++ b/.Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/.Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/Usings.cs b/.Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/Usings.cs new file mode 100644 index 0000000000..c802f4480b --- /dev/null +++ b/.Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/.Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountDelta.cs b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountDelta.cs new file mode 100644 index 0000000000..0f9ed087b5 --- /dev/null +++ b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountDelta.cs @@ -0,0 +1,65 @@ +using System.Collections.Immutable; +using System.Numerics; +using Bencodex.Types; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Libplanet.Types.Consensus; +using Libplanet.Action.State; + +namespace Libplanet.Extensions.ActionEvaluatorCommonComponents +{ + public class AccountDelta : IAccountDelta + { + public AccountDelta() + { + States = ImmutableDictionary.Empty; + Fungibles = ImmutableDictionary<(Address, Currency), BigInteger>.Empty; + TotalSupplies = ImmutableDictionary.Empty; + ValidatorSet = null; + } + + public AccountDelta( + IImmutableDictionary statesDelta, + IImmutableDictionary<(Address, Currency), BigInteger> fungiblesDelta, + IImmutableDictionary totalSuppliesDelta, + ValidatorSet? validatorSetDelta) + { + States = statesDelta; + Fungibles = fungiblesDelta; + TotalSupplies = totalSuppliesDelta; + ValidatorSet = validatorSetDelta; + } + + /// + public IImmutableSet
UpdatedAddresses => + StateUpdatedAddresses.Union(FungibleUpdatedAddresses); + + /// + public IImmutableSet
StateUpdatedAddresses => + States.Keys.ToImmutableHashSet(); + + /// + public IImmutableDictionary States { get; } + + /// + public IImmutableSet
FungibleUpdatedAddresses => + Fungibles.Keys.Select(pair => pair.Item1).ToImmutableHashSet(); + + /// + public IImmutableSet<(Address, Currency)> UpdatedFungibleAssets => + Fungibles.Keys.ToImmutableHashSet(); + + /// + public IImmutableDictionary<(Address, Currency), BigInteger> Fungibles { get; } + + /// + public IImmutableSet UpdatedTotalSupplyCurrencies => + TotalSupplies.Keys.ToImmutableHashSet(); + + /// + public IImmutableDictionary TotalSupplies { get; } + + /// + public ValidatorSet? ValidatorSet { get; } + } +} diff --git a/.Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountStateDelta.cs b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountStateDelta.cs new file mode 100644 index 0000000000..8a37cb91d6 --- /dev/null +++ b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountStateDelta.cs @@ -0,0 +1,293 @@ +using System.Collections.Immutable; +using System.Numerics; +using Bencodex; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Libplanet.Types.Consensus; + +namespace Libplanet.Extensions.ActionEvaluatorCommonComponents; + +public class AccountStateDelta : IAccountStateDelta +{ + private IImmutableDictionary _states; + private IImmutableDictionary<(Address, Currency), BigInteger> _fungibles; + private IImmutableDictionary _totalSupplies; + private ValidatorSet? _validatorSet; + private IAccountDelta _delta; + + public IImmutableSet
UpdatedAddresses => _delta.UpdatedAddresses; + + public IImmutableSet
StateUpdatedAddresses => _delta.StateUpdatedAddresses; + + public IImmutableSet<(Address, Currency)> UpdatedFungibleAssets => _delta.UpdatedFungibleAssets; + +#pragma warning disable LAA1002 + public IImmutableSet<(Address, Currency)> TotalUpdatedFungibleAssets { get; } +#pragma warning restore LAA1002 + + public IImmutableSet UpdatedTotalSupplyCurrencies => _delta.UpdatedTotalSupplyCurrencies; + + public AccountStateDelta() + : this( + ImmutableDictionary.Empty, + ImmutableDictionary<(Address, Currency), BigInteger>.Empty, + ImmutableDictionary.Empty, + null) + { + } + + public AccountStateDelta( + IImmutableDictionary states, + IImmutableDictionary<(Address, Currency), BigInteger> fungibles, + IImmutableDictionary totalSupplies, + ValidatorSet? validatorSet + ) + { + _delta = new AccountDelta( + states, + fungibles, + totalSupplies, + validatorSet); + _states = states; + _fungibles = fungibles; + _totalSupplies = totalSupplies; + _validatorSet = validatorSet; + } + + public AccountStateDelta(Dictionary states, List fungibles, List totalSupplies, IValue validatorSet) + { + // This assumes `states` consists of only Binary keys: + _states = states + .ToImmutableDictionary( + kv => new Address(((Binary)kv.Key).ByteArray), + kv => kv.Value); + + _fungibles = fungibles + .Cast() + .Select(dict => + new KeyValuePair<(Address, Currency), BigInteger>( + ( + new Address(((Binary)dict["address"]).ByteArray), + new Currency(dict["currency"]) + ), + ((Integer)dict["amount"]).Value + )) + .ToImmutableDictionary(); + + // This assumes `totalSupplies` consists of only Binary keys: + _totalSupplies = totalSupplies + .Cast() + .Select(dict => + new KeyValuePair( + new Currency(dict["currency"]), + new BigInteger((Integer)dict["amount"]))) + .ToImmutableDictionary(); + + _validatorSet = validatorSet is Null + ? null + : new ValidatorSet(validatorSet); + + _delta = new AccountDelta( + _states, + _fungibles, + _totalSupplies, + _validatorSet); + } + + public AccountStateDelta(IValue serialized) + : this((Dictionary)serialized) + { + } + + public AccountStateDelta(Dictionary dict) + : this( + (Dictionary)dict["states"], + (List)dict["balances"], + (List)dict["totalSupplies"], + dict["validatorSet"]) + { + } + + public AccountStateDelta(byte[] bytes) + : this((Dictionary)new Codec().Decode(bytes)) + { + } + + public IAccountDelta Delta => _delta; + + public IValue? GetState(Address address) => + _states.ContainsKey(address) + ? _states[address] + : throw new NotSupportedException(); + + public IReadOnlyList GetStates(IReadOnlyList
addresses) => + addresses.Select(GetState).ToArray(); + + public IAccountStateDelta SetState(Address address, IValue state) => + new AccountStateDelta(_states.SetItem(address, state), _fungibles, _totalSupplies, _validatorSet); + public FungibleAssetValue GetBalance(Address address, Currency currency) + { + if (!_fungibles.TryGetValue((address, currency), out BigInteger rawValue)) + { + throw new NotSupportedException(); + } + + return FungibleAssetValue.FromRawValue(currency, rawValue); + } + + public FungibleAssetValue GetTotalSupply(Currency currency) + { + if (!currency.TotalSupplyTrackable) + { + var msg = + $"The total supply value of the currency {currency} is not trackable" + + " because it is a legacy untracked currency which might have been" + + " established before the introduction of total supply tracking support."; + throw new TotalSupplyNotTrackableException(msg, currency); + } + + // Return dirty state if it exists. + if (_totalSupplies.TryGetValue(currency, out var totalSupplyValue)) + { + return FungibleAssetValue.FromRawValue(currency, totalSupplyValue); + } + + throw new NotSupportedException(); + } + + public IAccountStateDelta MintAsset( + IActionContext context, Address recipient, FungibleAssetValue value) + { + // FIXME: 트랜잭션 서명자를 알아내 currency.AllowsToMint() 확인해서 CurrencyPermissionException + // 던지는 처리를 해야하는데 여기서 트랜잭션 서명자를 무슨 수로 가져올지 잘 모르겠음. + + var currency = value.Currency; + + if (value <= currency * 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + var nextAmount = GetBalance(recipient, value.Currency) + value; + + if (currency.TotalSupplyTrackable) + { + var currentTotalSupply = GetTotalSupply(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 new AccountStateDelta( + _states, + _fungibles.SetItem( + (recipient, value.Currency), + nextAmount.RawValue + ), + _totalSupplies.SetItem(currency, (currentTotalSupply + value).RawValue), + _validatorSet + ); + } + + return new AccountStateDelta( + _states, + _fungibles.SetItem( + (recipient, value.Currency), + nextAmount.RawValue + ), + _totalSupplies, + _validatorSet + ); + } + + public IAccountStateDelta TransferAsset( + IActionContext context, + Address sender, + Address recipient, + FungibleAssetValue value, + bool allowNegativeBalance = false) + { + if (value.Sign <= 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + FungibleAssetValue senderBalance = GetBalance(sender, value.Currency); + if (senderBalance < value) + { + throw new InsufficientBalanceException( + $"There is no sufficient balance for {sender}: {senderBalance} < {value}", + sender, + senderBalance + ); + } + + Currency currency = value.Currency; + FungibleAssetValue senderRemains = senderBalance - value; + FungibleAssetValue recipientRemains = GetBalance(recipient, currency) + value; + var balances = _fungibles + .SetItem((sender, currency), senderRemains.RawValue) + .SetItem((recipient, currency), recipientRemains.RawValue); + return new AccountStateDelta(_states, balances, _totalSupplies, _validatorSet); + } + + public IAccountStateDelta BurnAsset( + IActionContext context, Address owner, FungibleAssetValue value) + { + // FIXME: 트랜잭션 서명자를 알아내 currency.AllowsToMint() 확인해서 CurrencyPermissionException + // 던지는 처리를 해야하는데 여기서 트랜잭션 서명자를 무슨 수로 가져올지 잘 모르겠음. + + var currency = value.Currency; + + if (value <= currency * 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + FungibleAssetValue balance = GetBalance(owner, currency); + if (balance < value) + { + throw new InsufficientBalanceException( + $"There is no sufficient balance for {owner}: {balance} < {value}", + owner, + value + ); + } + + FungibleAssetValue nextValue = balance - value; + return new AccountStateDelta( + _states, + _fungibles.SetItem( + (owner, currency), + nextValue.RawValue + ), + currency.TotalSupplyTrackable + ? _totalSupplies.SetItem( + currency, + (GetTotalSupply(currency) - value).RawValue) + : _totalSupplies, + _validatorSet + ); + } + + public ValidatorSet GetValidatorSet() + { + return _validatorSet ?? throw new NotSupportedException(); + } + + public IAccountStateDelta SetValidator(Validator validator) + { + return new AccountStateDelta( + _states, + _fungibles, + _totalSupplies, + GetValidatorSet().Update(validator) + ); + } +} diff --git a/.Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountStateDeltaMarshaller.cs b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountStateDeltaMarshaller.cs new file mode 100644 index 0000000000..20375ca897 --- /dev/null +++ b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/AccountStateDeltaMarshaller.cs @@ -0,0 +1,60 @@ +using Bencodex; +using Bencodex.Types; +using Libplanet.Action.State; + +namespace Libplanet.Extensions.ActionEvaluatorCommonComponents; + +public static class AccountStateDeltaMarshaller +{ + private static readonly Codec Codec = new Codec(); + + public static byte[] Serialize(this IAccountStateDelta value) + { + return Codec.Encode(Marshal(value)); + } + + public static IEnumerable Marshal(IEnumerable stateDeltas) + { + foreach (var stateDelta in stateDeltas) + { + var bdict = Marshal(stateDelta); + yield return bdict; + } + } + + public static Dictionary Marshal(IAccountStateDelta stateDelta) + { + var state = new Dictionary(stateDelta.Delta.States.Select( + kv => new KeyValuePair( + new Binary(kv.Key.ByteArray), + kv.Value))); + var balance = new List(stateDelta.Delta.Fungibles.Select( + kv => Dictionary.Empty + .Add("address", new Binary(kv.Key.Item1.ByteArray)) + .Add("currency", kv.Key.Item2.Serialize()) + .Add("amount", new Integer(kv.Value)))); + var totalSupply = new List(stateDelta.Delta.TotalSupplies.Select( + kv => Dictionary.Empty + .Add("currency", kv.Key.Serialize()) + .Add("amount", new Integer(kv.Value)))); + var bdict = Dictionary.Empty + .Add("states", state) + .Add("balances", balance) + .Add("totalSupplies", totalSupply) + .Add("validatorSet", stateDelta.Delta.ValidatorSet is { } validatorSet + ? validatorSet.Bencoded + : Null.Value); + return bdict; + } + + public static AccountStateDelta Unmarshal(IValue marshalled) + { + return new AccountStateDelta(marshalled); + } + + public static AccountStateDelta Deserialize(byte[] serialized) + { + var decoded = Codec.Decode(serialized); + return Unmarshal(decoded); + } +} diff --git a/.Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContext.cs b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContext.cs new file mode 100644 index 0000000000..20c5ce9535 --- /dev/null +++ b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContext.cs @@ -0,0 +1,81 @@ +using System.Security.Cryptography; +using Libplanet.Action; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Types.Blocks; +using Libplanet.Action.State; +using Libplanet.Types.Tx; + +namespace Libplanet.Extensions.ActionEvaluatorCommonComponents; + +public class ActionContext : IActionContext +{ + public ActionContext( + BlockHash? genesisHash, + Address signer, + TxId? txId, + Address miner, + long blockIndex, + int blockProtocolVersion, + bool rehearsal, + AccountStateDelta previousState, + IRandom random, + HashDigest? previousStateRootHash, + bool blockAction) + { + GenesisHash = genesisHash; + Signer = signer; + TxId = txId; + Miner = miner; + BlockIndex = blockIndex; + BlockProtocolVersion = blockProtocolVersion; + Rehearsal = rehearsal; + PreviousState = previousState; + Random = random; + PreviousStateRootHash = previousStateRootHash; + BlockAction = blockAction; + } + + public BlockHash? GenesisHash { get; } + public Address Signer { get; init; } + public TxId? TxId { get; } + public Address Miner { get; init; } + public long BlockIndex { get; init; } + public int BlockProtocolVersion { get; init; } + public bool Rehearsal { get; init; } + public AccountStateDelta PreviousState { get; init; } + IAccountStateDelta IActionContext.PreviousState => PreviousState; + public IRandom Random { get; init; } + public HashDigest? PreviousStateRootHash { get; init; } + public bool BlockAction { get; init; } + + public void PutLog(string log) + { + throw new NotImplementedException(); + } + + public void UseGas(long gas) + { + throw new NotImplementedException(); + } + + public IActionContext GetUnconsumedContext() + { + return new ActionContext( + GenesisHash, + Signer, + TxId, + Miner, + BlockIndex, + BlockProtocolVersion, + Rehearsal, + PreviousState, + new Random(Random.Seed), + PreviousStateRootHash, + BlockAction); + } + + public long GasUsed() => 0; + + public long GasLimit() => 0; +} diff --git a/.Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContextMarshaller.cs b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContextMarshaller.cs new file mode 100644 index 0000000000..5444f70a72 --- /dev/null +++ b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionContextMarshaller.cs @@ -0,0 +1,77 @@ +using System.Security.Cryptography; +using Bencodex; +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Common; +using Libplanet.Crypto; +using Libplanet.Types.Blocks; +using Libplanet.Types.Tx; +using Boolean = Bencodex.Types.Boolean; + +namespace Libplanet.Extensions.ActionEvaluatorCommonComponents; + +public static class ActionContextMarshaller +{ + private static readonly Codec Codec = new Codec(); + + public static byte[] Serialize(this IActionContext actionContext) + { + return Codec.Encode(Marshal(actionContext)); + } + + public static Dictionary Marshal(this IActionContext actionContext) + { + var dictionary = Bencodex.Types.Dictionary.Empty + .Add("block_action", actionContext.BlockAction) + .Add("miner", actionContext.Miner.ToHex()) + .Add("rehearsal", actionContext.Rehearsal) + .Add("block_index", actionContext.BlockIndex) + .Add("block_protocol_version", actionContext.BlockProtocolVersion) + .Add("random_seed", actionContext.Random.Seed) + .Add("signer", actionContext.Signer.ToHex()) + .Add("previous_states", AccountStateDeltaMarshaller.Marshal(actionContext.PreviousState)); + + if (actionContext.TxId is { } txId) + { + dictionary = dictionary.Add("tx_id", txId.ByteArray); + } + + return dictionary; + } + + public static ActionContext Unmarshal(Dictionary dictionary) + { + return new ActionContext( + genesisHash: dictionary.TryGetValue((Text)"genesis_hash", out IValue genesisHashValue) && + genesisHashValue is Binary genesisHashBinaryValue + ? new BlockHash(genesisHashBinaryValue.ByteArray) + : null, + blockIndex: (Integer)dictionary["block_index"], + blockProtocolVersion: (Integer)dictionary["block_protocol_version"], + signer: new Address(((Text)dictionary["signer"]).Value), + txId: dictionary.TryGetValue((Text)"tx_id", out IValue txIdValue) && + txIdValue is Binary txIdBinaryValue + ? new TxId(txIdBinaryValue.ByteArray) + : null, + blockAction: (Boolean)dictionary["block_action"], + miner: new Address(((Text)dictionary["miner"]).Value), + rehearsal: (Boolean)dictionary["rehearsal"], + previousStateRootHash: dictionary.ContainsKey("previous_state_root_hash") + ? new HashDigest(((Binary)dictionary["previous_state_root_hash"]).ByteArray) + : null, + previousState: AccountStateDeltaMarshaller.Unmarshal(dictionary["previous_states"]), + random: new Random((Integer)dictionary["random_seed"]) + ); + } + + public static ActionContext Deserialize(byte[] serialized) + { + var decoded = Codec.Decode(serialized); + if (!(decoded is Dictionary dictionary)) + { + throw new ArgumentException($"Expected 'Dictionary' but {decoded.GetType().Name}", nameof(serialized)); + } + + return Unmarshal(dictionary); + } +} diff --git a/.Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionEvaluation.cs b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionEvaluation.cs new file mode 100644 index 0000000000..0daefb29ff --- /dev/null +++ b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionEvaluation.cs @@ -0,0 +1,27 @@ +using Bencodex.Types; +using Libplanet.Action; +using Libplanet.Action.State; + +namespace Libplanet.Extensions.ActionEvaluatorCommonComponents; + +public class ActionEvaluation : IActionEvaluation +{ + public ActionEvaluation( + IValue action, + ActionContext inputContext, + AccountStateDelta outputState, + Exception? exception) + { + Action = action; + InputContext = inputContext; + OutputState = outputState; + Exception = exception; + } + + public IValue Action { get; } + public ActionContext InputContext { get; } + IActionContext IActionEvaluation.InputContext => InputContext; + public AccountStateDelta OutputState { get; } + IAccountStateDelta IActionEvaluation.OutputState => OutputState; + public Exception? Exception { get; } +} diff --git a/.Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionEvaluationMarshaller.cs b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionEvaluationMarshaller.cs new file mode 100644 index 0000000000..d77199face --- /dev/null +++ b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/ActionEvaluationMarshaller.cs @@ -0,0 +1,60 @@ +using Bencodex; +using Bencodex.Types; +using Libplanet.Action; + +namespace Libplanet.Extensions.ActionEvaluatorCommonComponents; + +public static class ActionEvaluationMarshaller +{ + private static readonly Codec Codec = new Codec(); + + public static byte[] Serialize(this IActionEvaluation actionEvaluation) + { + return Codec.Encode(Marshal(actionEvaluation)); + } + + public static IEnumerable Marshal(this IEnumerable actionEvaluations) + { + var actionEvaluationsArray = actionEvaluations.ToArray(); + var outputStates = AccountStateDeltaMarshaller.Marshal(actionEvaluationsArray.Select(aev => aev.OutputState)); + var previousStates = AccountStateDeltaMarshaller.Marshal(actionEvaluationsArray.Select(aev => aev.InputContext.PreviousState)); + foreach (var actionEvaluation in actionEvaluationsArray) + { + yield return Dictionary.Empty + .Add("action", actionEvaluation.Action) + .Add("output_states", AccountStateDeltaMarshaller.Marshal(actionEvaluation.OutputState)) + .Add("input_context", ActionContextMarshaller.Marshal(actionEvaluation.InputContext)) + .Add("exception", actionEvaluation.Exception?.GetType().FullName is { } typeName ? (Text)typeName : Null.Value); + } + } + + public static Dictionary Marshal(this IActionEvaluation actionEvaluation) + { + return Dictionary.Empty + .Add("action", actionEvaluation.Action) + .Add("output_states", AccountStateDeltaMarshaller.Marshal(actionEvaluation.OutputState)) + .Add("input_context", ActionContextMarshaller.Marshal(actionEvaluation.InputContext)) + .Add("exception", actionEvaluation.Exception?.GetType().FullName is { } typeName ? (Text)typeName : Null.Value); + } + + public static ActionEvaluation Unmarshal(IValue value) + { + if (value is not Dictionary dictionary) + { + throw new ArgumentException(nameof(value)); + } + + return new ActionEvaluation( + dictionary["action"], + ActionContextMarshaller.Unmarshal((Dictionary)dictionary["input_context"]), + AccountStateDeltaMarshaller.Unmarshal(dictionary["output_states"]), + dictionary["exception"] is Text typeName ? new Exception(typeName) : null + ); + } + + public static ActionEvaluation Deserialize(byte[] serialized) + { + var decoded = Codec.Decode(serialized); + return Unmarshal(decoded); + } +} diff --git a/.Libplanet.Extensions.ActionEvaluatorCommonComponents/Libplanet.Extensions.ActionEvaluatorCommonComponents.csproj b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/Libplanet.Extensions.ActionEvaluatorCommonComponents.csproj new file mode 100644 index 0000000000..9c6e17ed8b --- /dev/null +++ b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/Libplanet.Extensions.ActionEvaluatorCommonComponents.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/.Libplanet.Extensions.ActionEvaluatorCommonComponents/PreEvaluationBlockMarshaller.cs b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/PreEvaluationBlockMarshaller.cs new file mode 100644 index 0000000000..dc34196bf8 --- /dev/null +++ b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/PreEvaluationBlockMarshaller.cs @@ -0,0 +1,39 @@ +using Bencodex; +using Bencodex.Types; +using Libplanet.Types.Blocks; + +namespace Libplanet.Extensions.ActionEvaluatorCommonComponents; + +public static class PreEvaluationBlockMarshaller +{ + private static readonly Codec Codec = new Codec(); + + // Copied form Libplanet/Blocks/BlockMarshaler.cs + // Block fields: + private static readonly byte[] HeaderKey = { 0x48 }; // 'H' + private static readonly byte[] TransactionsKey = { 0x54 }; // 'T' + + public static Dictionary Marshal(this IPreEvaluationBlock block) + { + return Dictionary.Empty + .Add(HeaderKey, BlockMarshaler.MarshalPreEvaluationBlockHeader(block)) + .Add(TransactionsKey, new List(block.Transactions.Select(TransactionMarshaller.Serialize))); + } + + public static IPreEvaluationBlock Unmarshal(Dictionary dictionary) + { + return new PreEvaluationBlock( + BlockMarshaler.UnmarshalPreEvaluationBlockHeader((Dictionary)dictionary[HeaderKey]), + BlockMarshaler.UnmarshalBlockTransactions(dictionary)); + } + + public static byte[] Serialize(this IPreEvaluationBlock block) + { + return Codec.Encode(Marshal(block)); + } + + public static IPreEvaluationBlock Deserialize(byte[] serialized) + { + return Unmarshal((Dictionary)Codec.Decode(serialized)); + } +} diff --git a/.Libplanet.Extensions.ActionEvaluatorCommonComponents/Random.cs b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/Random.cs new file mode 100644 index 0000000000..f1006cc56e --- /dev/null +++ b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/Random.cs @@ -0,0 +1,14 @@ +using Libplanet.Action; + +namespace Libplanet.Extensions.ActionEvaluatorCommonComponents; + +public class Random : System.Random, IRandom +{ + public Random(int seed) + : base(seed) + { + Seed = seed; + } + + public int Seed { get; private set; } +} diff --git a/.Libplanet.Extensions.ActionEvaluatorCommonComponents/TransactionMarshaller.cs b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/TransactionMarshaller.cs new file mode 100644 index 0000000000..a203f523d8 --- /dev/null +++ b/.Libplanet.Extensions.ActionEvaluatorCommonComponents/TransactionMarshaller.cs @@ -0,0 +1,23 @@ +using Bencodex; +using Bencodex.Types; +using Libplanet.Types.Tx; + +namespace Libplanet.Extensions.ActionEvaluatorCommonComponents; + +public static class TransactionMarshaller +{ + private static readonly Codec Codec = new Codec(); + + // Copied from Libplanet/Tx/TxMarshaler.cs + private static readonly Binary SignatureKey = new byte[] { 0x53 }; // 'S' + + public static Dictionary Marshal(this ITransaction transaction) + { + return transaction.MarshalUnsignedTx().Add(SignatureKey, transaction.Signature); + } + + public static byte[] Serialize(this ITransaction transaction) + { + return Codec.Encode(Marshal(transaction)); + } +} diff --git a/.Libplanet.Extensions.RemoteActionEvaluator.Tests/Libplanet.Extensions.RemoteActionEvaluator.Tests.csproj b/.Libplanet.Extensions.RemoteActionEvaluator.Tests/Libplanet.Extensions.RemoteActionEvaluator.Tests.csproj new file mode 100644 index 0000000000..bf79b854ef --- /dev/null +++ b/.Libplanet.Extensions.RemoteActionEvaluator.Tests/Libplanet.Extensions.RemoteActionEvaluator.Tests.csproj @@ -0,0 +1,29 @@ + + + + net6.0 + enable + enable + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + diff --git a/.Libplanet.Extensions.RemoteActionEvaluator.Tests/Usings.cs b/.Libplanet.Extensions.RemoteActionEvaluator.Tests/Usings.cs new file mode 100644 index 0000000000..c802f4480b --- /dev/null +++ b/.Libplanet.Extensions.RemoteActionEvaluator.Tests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/.Libplanet.Extensions.RemoteActionEvaluator/AssemblyInfo.cs b/.Libplanet.Extensions.RemoteActionEvaluator/AssemblyInfo.cs new file mode 100644 index 0000000000..632c7b0189 --- /dev/null +++ b/.Libplanet.Extensions.RemoteActionEvaluator/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Libplanet.Extensions.RemoteActionEvaluator.Tests")] diff --git a/.Libplanet.Extensions.RemoteActionEvaluator/Libplanet.Extensions.RemoteActionEvaluator.csproj b/.Libplanet.Extensions.RemoteActionEvaluator/Libplanet.Extensions.RemoteActionEvaluator.csproj new file mode 100644 index 0000000000..3e68102fea --- /dev/null +++ b/.Libplanet.Extensions.RemoteActionEvaluator/Libplanet.Extensions.RemoteActionEvaluator.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + enable + enable + + + + + + + + + diff --git a/.Libplanet.Extensions.RemoteActionEvaluator/RemoteActionEvaluator.cs b/.Libplanet.Extensions.RemoteActionEvaluator/RemoteActionEvaluator.cs new file mode 100644 index 0000000000..4fd8a38716 --- /dev/null +++ b/.Libplanet.Extensions.RemoteActionEvaluator/RemoteActionEvaluator.cs @@ -0,0 +1,83 @@ +using System.Collections.Immutable; +using System.Diagnostics.Contracts; +using System.Net.Http.Json; +using Bencodex.Types; +using Lib9c.StateService.Shared; +using Libplanet.Action; +using Libplanet.Action.Loader; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Libplanet.Types.Blocks; +using Libplanet.Types.Consensus; +using Libplanet.Extensions.ActionEvaluatorCommonComponents; + +namespace Libplanet.Extensions.RemoteActionEvaluator; + +public class RemoteActionEvaluator : IActionEvaluator +{ + private readonly Uri _endpoint; + private readonly IBlockChainStates _blockChainStates; + + public RemoteActionEvaluator(Uri endpoint, IBlockChainStates blockChainStates) + { + _endpoint = endpoint; + _blockChainStates = blockChainStates; + } + + public IActionLoader ActionLoader => throw new NotSupportedException(); + + public IReadOnlyList Evaluate(IPreEvaluationBlock block) + { + using var httpClient = new HttpClient(); + var response = httpClient.PostAsJsonAsync(_endpoint, new RemoteEvaluationRequest + { + PreEvaluationBlock = PreEvaluationBlockMarshaller.Serialize(block), + }).Result; + var evaluationResponse = response.Content.ReadFromJsonAsync().Result; + + var actionEvaluations = evaluationResponse.Evaluations.Select(ActionEvaluationMarshaller.Deserialize) + .ToImmutableList(); + + return actionEvaluations; + } + + [Pure] + private static IReadOnlyList NullAccountStateGetter( + IReadOnlyList
addresses + ) => + new IValue?[addresses.Count]; + + [Pure] + private static FungibleAssetValue NullAccountBalanceGetter( + Address address, + Currency currency + ) => + currency * 0; + + [Pure] + private static FungibleAssetValue NullTotalSupplyGetter(Currency currency) + { + if (!currency.TotalSupplyTrackable) + { + throw WithDefaultMessage(currency); + } + + return currency * 0; + } + + [Pure] + private static ValidatorSet NullValidatorSetGetter() + { + return new ValidatorSet(); + } + + private static TotalSupplyNotTrackableException WithDefaultMessage(Currency currency) + { + var msg = + $"The total supply value of the currency {currency} is not trackable because it" + + " is a legacy untracked currency which might have been established before" + + " the introduction of total supply tracking support."; + return new TotalSupplyNotTrackableException(msg, currency); + } +} diff --git a/.Libplanet.Extensions.RemoteBlockChainStates/Libplanet.Extensions.RemoteBlockChainStates.csproj b/.Libplanet.Extensions.RemoteBlockChainStates/Libplanet.Extensions.RemoteBlockChainStates.csproj new file mode 100644 index 0000000000..5eb43e1803 --- /dev/null +++ b/.Libplanet.Extensions.RemoteBlockChainStates/Libplanet.Extensions.RemoteBlockChainStates.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + diff --git a/.Libplanet.Extensions.RemoteBlockChainStates/RemoteBlockChainStates.cs b/.Libplanet.Extensions.RemoteBlockChainStates/RemoteBlockChainStates.cs new file mode 100644 index 0000000000..040b022071 --- /dev/null +++ b/.Libplanet.Extensions.RemoteBlockChainStates/RemoteBlockChainStates.cs @@ -0,0 +1,47 @@ +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Libplanet.Types.Blocks; +using Libplanet.Types.Consensus; + +namespace Libplanet.Extensions.RemoteBlockChainStates +{ + public class RemoteBlockChainStates : IBlockChainStates + { + private readonly Uri _explorerEndpoint; + + public RemoteBlockChainStates(Uri explorerEndpoint) + { + _explorerEndpoint = explorerEndpoint; + } + + public IValue? GetState(Address address, BlockHash? offset) => + GetStates(new[] { address }, offset).First(); + + public IReadOnlyList GetStates(IReadOnlyList
addresses, BlockHash? offset) + { + return new RemoteBlockState(_explorerEndpoint, offset).GetStates(addresses); + } + + public FungibleAssetValue GetBalance(Address address, Currency currency, BlockHash? offset) + { + return new RemoteBlockState(_explorerEndpoint, offset).GetBalance(address, currency); + } + + public FungibleAssetValue GetTotalSupply(Currency currency, BlockHash? offset) + { + return new RemoteBlockState(_explorerEndpoint, offset).GetTotalSupply(currency); + } + + public ValidatorSet GetValidatorSet(BlockHash? offset) + { + return new RemoteBlockState(_explorerEndpoint, offset).GetValidatorSet(); + } + + public IBlockState GetBlockState(BlockHash? offset) + { + return new RemoteBlockState(_explorerEndpoint, offset); + } + } +} diff --git a/.Libplanet.Extensions.RemoteBlockChainStates/RemoteBlockState.cs b/.Libplanet.Extensions.RemoteBlockChainStates/RemoteBlockState.cs new file mode 100644 index 0000000000..08e047e54d --- /dev/null +++ b/.Libplanet.Extensions.RemoteBlockChainStates/RemoteBlockState.cs @@ -0,0 +1,219 @@ +using Bencodex; +using Bencodex.Types; +using GraphQL; +using GraphQL.Client.Http; +using GraphQL.Client.Serializer.SystemTextJson; +using Libplanet.Action.State; +using Libplanet.Common; +using Libplanet.Types.Assets; +using Libplanet.Types.Blocks; +using Libplanet.Types.Consensus; +using Libplanet.Crypto; + +namespace Libplanet.Extensions.RemoteBlockChainStates; + +public class RemoteBlockState : IBlockState +{ + private readonly Uri _explorerEndpoint; + private readonly GraphQLHttpClient _graphQlHttpClient; + + public RemoteBlockState(Uri explorerEndpoint, BlockHash? blockHash) + { + _explorerEndpoint = explorerEndpoint; + _graphQlHttpClient = + new GraphQLHttpClient(_explorerEndpoint, new SystemTextJsonSerializer()); + BlockHash = blockHash; + } + + public IValue? GetState(Address address) => + GetStates(new[] { address }).First(); + + public IReadOnlyList GetStates(IReadOnlyList
addresses) + { + var response = _graphQlHttpClient.SendQueryAsync( + new GraphQLRequest( + @"query GetStates($addresses: [Address!]!, $offsetBlockHash: ID!) + { + stateQuery + { + states(addresses: $addresses, offsetBlockHash: $offsetBlockHash) + } + }", + operationName: "GetStates", + variables: new + { + addresses = addresses.Select(x => x.ToString()).ToArray(), + offsetBlockHash = BlockHash is { } hash + ? ByteUtil.Hex(hash.ByteArray) + : throw new NotSupportedException(), + })).Result; + var codec = new Codec(); + return response.Data.StateQuery.States + .Select(nullableState => nullableState is { } state ? codec.Decode(state) : null).ToList(); + } + + public FungibleAssetValue GetBalance(Address address, Currency currency) + { + object? currencyInput = currency.TotalSupplyTrackable ? new + { + ticker = currency.Ticker, + decimalPlaces = currency.DecimalPlaces, + minters = currency.Minters?.Select(addr => addr.ToString()).ToArray(), + totalSupplyTrackable = currency.TotalSupplyTrackable, + maximumSupplyMajorUnit = currency.MaximumSupply.Value.MajorUnit, + maximumSupplyMinorUnit = currency.MaximumSupply.Value.MinorUnit, + } : new + { + ticker = currency.Ticker, + decimalPlaces = currency.DecimalPlaces, + minters = currency.Minters?.Select(addr => addr.ToString()).ToArray(), + totalSupplyTrackable = currency.TotalSupplyTrackable, + }; + var response = _graphQlHttpClient.SendQueryAsync( + new GraphQLRequest( + @"query GetBalance($owner: Address!, $currency: CurrencyInput!, $offsetBlockHash: ID!) + { + stateQuery + { + balance(owner: $owner, currency: $currency, offsetBlockHash: $offsetBlockHash) + { + string + } + } + }", + operationName: "GetBalance", + variables: new + { + owner = address.ToString(), + currency = currencyInput, + offsetBlockHash = BlockHash is { } hash + ? ByteUtil.Hex(hash.ByteArray) + : throw new NotSupportedException(), + })).Result; + + return FungibleAssetValue.Parse(currency, response.Data.StateQuery.Balance.String.Split()[0]); + } + + public FungibleAssetValue GetTotalSupply(Currency currency) + { + object? currencyInput = currency.TotalSupplyTrackable ? new + { + ticker = currency.Ticker, + decimalPlaces = currency.DecimalPlaces, + minters = currency.Minters.Select(addr => addr.ToString()).ToArray(), + totalSupplyTrackable = currency.TotalSupplyTrackable, + maximumSupplyMajorUnit = currency.MaximumSupply.Value.MajorUnit, + maximumSupplyMinorUnit = currency.MaximumSupply.Value.MinorUnit, + } : new + { + ticker = currency.Ticker, + decimalPlaces = currency.DecimalPlaces, + minters = currency.Minters.Select(addr => addr.ToString()).ToArray(), + totalSupplyTrackable = currency.TotalSupplyTrackable, + }; + var response = _graphQlHttpClient.SendQueryAsync( + new GraphQLRequest( + @"query GetTotalSupply(currency: CurrencyInput!, $offsetBlockHash: ID!) + { + stateQuery + { + totalSupply(currency: $currency, offsetBlockHash: $offsetBlockHash) + { + string + } + } + }", + operationName: "GetTotalSupply", + variables: new + { + currency = currencyInput, + offsetBlockHash = BlockHash is { } hash + ? ByteUtil.Hex(hash.ByteArray) + : throw new NotSupportedException(), + })).Result; + + return FungibleAssetValue.Parse(currency, response.Data.StateQuery.TotalSupply.String.Split()[0]); + } + + public ValidatorSet GetValidatorSet() + { + var response = _graphQlHttpClient.SendQueryAsync( + new GraphQLRequest( + @"query GetValidators($offsetBlockHash: ID!) + { + stateQuery + { + validators(offsetBlockHash: $offsetBlockHash) + { + publicKey + power + } + } + }", + operationName: "GetValidators", + variables: new + { + offsetBlockHash = BlockHash is { } hash + ? ByteUtil.Hex(hash.ByteArray) + : throw new NotSupportedException(), + })).Result; + + return new ValidatorSet(response.Data.StateQuery.Validators + .Select(x => + new Validator(new PublicKey(ByteUtil.ParseHex(x.PublicKey)), x.Power)) + .ToList()); + } + + public BlockHash? BlockHash { get; } + + private class GetStatesResponseType + { + public StateQueryWithStatesType StateQuery { get; set; } + } + + private class StateQueryWithStatesType + { + public byte[][] States { get; set; } + } + + private class GetBalanceResponseType + { + public StateQueryWithBalanceType StateQuery { get; set; } + } + + private class StateQueryWithBalanceType + { + public FungibleAssetValueWithStringType Balance { get; set; } + } + + private class FungibleAssetValueWithStringType + { + public string String { get; set; } + } + + private class GetTotalSupplyResponseType + { + public StateQueryWithTotalSupplyType StateQuery { get; set; } + } + + private class StateQueryWithTotalSupplyType + { + public FungibleAssetValueWithStringType TotalSupply { get; set; } + } + + private class GetValidatorsResponseType + { + public StateQueryWithValidatorsType StateQuery { get; set; } + } + + private class StateQueryWithValidatorsType + { + public ValidatorType[] Validators { get; set; } + } + + private class ValidatorType + { + public string PublicKey { get; set; } + public long Power { get; set; } + } +} diff --git a/Lib9c.sln b/Lib9c.sln index 01508f9b6e..37a3e8594e 100644 --- a/Lib9c.sln +++ b/Lib9c.sln @@ -60,6 +60,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libplanet.Types", ".Libplan EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libplanet.Store", ".Libplanet\Libplanet.Store\Libplanet.Store.csproj", "{82BCD815-0AB6-4EEF-A12B-CDB9CD98EEA1}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Extensions.ActionEvaluatorCommonComponents", ".Libplanet.Extensions.ActionEvaluatorCommonComponents\Libplanet.Extensions.ActionEvaluatorCommonComponents.csproj", "{64C44AFB-1E14-44D3-B236-A4A37DF2C27A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests", ".Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests\Libplanet.Extensions.ActionEvaluatorCommonComponents.Tests.csproj", "{EACB2E8D-9A13-491C-BACD-5D79C6C13783}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Extensions.RemoteActionEvaluator", ".Libplanet.Extensions.RemoteActionEvaluator\Libplanet.Extensions.RemoteActionEvaluator.csproj", "{0ED5DBA2-C334-40F2-8EB6-2B4D15C1AB4B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Extensions.RemoteActionEvaluator.Tests", ".Libplanet.Extensions.RemoteActionEvaluator.Tests\Libplanet.Extensions.RemoteActionEvaluator.Tests.csproj", "{3608A63A-A52D-4EB5-A96D-36C8F11CE603}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Extensions.RemoteBlockChainStates", ".Libplanet.Extensions.RemoteBlockChainStates\Libplanet.Extensions.RemoteBlockChainStates.csproj", "{63544447-4FCD-48D1-898C-974FBA6834AD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lib9c.StateService", ".Lib9c.StateService\Lib9c.StateService.csproj", "{EB97AB26-1C8F-48F5-97FF-8A6DF8FAB879}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lib9c.StateService.Shared", ".Lib9c.StateService.Shared\Lib9c.StateService.Shared.csproj", "{5D3489AE-A403-4ADD-94E9-48463A42643E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -166,6 +180,34 @@ Global {82BCD815-0AB6-4EEF-A12B-CDB9CD98EEA1}.Debug|Any CPU.Build.0 = Debug|Any CPU {82BCD815-0AB6-4EEF-A12B-CDB9CD98EEA1}.Release|Any CPU.ActiveCfg = Release|Any CPU {82BCD815-0AB6-4EEF-A12B-CDB9CD98EEA1}.Release|Any CPU.Build.0 = Release|Any CPU + {64C44AFB-1E14-44D3-B236-A4A37DF2C27A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64C44AFB-1E14-44D3-B236-A4A37DF2C27A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64C44AFB-1E14-44D3-B236-A4A37DF2C27A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64C44AFB-1E14-44D3-B236-A4A37DF2C27A}.Release|Any CPU.Build.0 = Release|Any CPU + {EACB2E8D-9A13-491C-BACD-5D79C6C13783}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EACB2E8D-9A13-491C-BACD-5D79C6C13783}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EACB2E8D-9A13-491C-BACD-5D79C6C13783}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EACB2E8D-9A13-491C-BACD-5D79C6C13783}.Release|Any CPU.Build.0 = Release|Any CPU + {0ED5DBA2-C334-40F2-8EB6-2B4D15C1AB4B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0ED5DBA2-C334-40F2-8EB6-2B4D15C1AB4B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0ED5DBA2-C334-40F2-8EB6-2B4D15C1AB4B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0ED5DBA2-C334-40F2-8EB6-2B4D15C1AB4B}.Release|Any CPU.Build.0 = Release|Any CPU + {3608A63A-A52D-4EB5-A96D-36C8F11CE603}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3608A63A-A52D-4EB5-A96D-36C8F11CE603}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3608A63A-A52D-4EB5-A96D-36C8F11CE603}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3608A63A-A52D-4EB5-A96D-36C8F11CE603}.Release|Any CPU.Build.0 = Release|Any CPU + {63544447-4FCD-48D1-898C-974FBA6834AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63544447-4FCD-48D1-898C-974FBA6834AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63544447-4FCD-48D1-898C-974FBA6834AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63544447-4FCD-48D1-898C-974FBA6834AD}.Release|Any CPU.Build.0 = Release|Any CPU + {EB97AB26-1C8F-48F5-97FF-8A6DF8FAB879}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB97AB26-1C8F-48F5-97FF-8A6DF8FAB879}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB97AB26-1C8F-48F5-97FF-8A6DF8FAB879}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB97AB26-1C8F-48F5-97FF-8A6DF8FAB879}.Release|Any CPU.Build.0 = Release|Any CPU + {5D3489AE-A403-4ADD-94E9-48463A42643E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D3489AE-A403-4ADD-94E9-48463A42643E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D3489AE-A403-4ADD-94E9-48463A42643E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D3489AE-A403-4ADD-94E9-48463A42643E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE