diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..a173a3d2e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug NineChronicles.Headless", + "type": "coreclr", + "request": "launch", + "program": "${workspaceFolder}/NineChronicles.Headless.Executable/bin/Debug/net6/NineChronicles.Headless.Executable.dll", + "cwd": "${workspaceFolder}/NineChronicles.Headless.Executable", + "stopAtEntry": false, + "console": "integratedTerminal", + "preLaunchTask": "build", + "args": [ + "--config", + "appsettings.local.json" + ] + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..57dae1e9a --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/NineChronicles.Headless.Executable/NineChronicles.Headless.Executable.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/NineChronicles.Headless.Executable/NineChronicles.Headless.Executable.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/NineChronicles.Headless.Executable/NineChronicles.Headless.Executable.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} diff --git a/Lib9c b/Lib9c index 7c90f7d82..ea35b3a5e 160000 --- a/Lib9c +++ b/Lib9c @@ -1 +1 @@ -Subproject commit 7c90f7d8243b1fc80a2e770abd0891a7a4f00bed +Subproject commit ea35b3a5eac201841340bfadaf68304e8586a0ff diff --git a/Libplanet.Extensions.ForkableActionEvaluator/HardForkRouter.cs b/Libplanet.Extensions.ForkableActionEvaluator/HardForkRouter.cs index d85913bfe..87f8cea47 100644 --- a/Libplanet.Extensions.ForkableActionEvaluator/HardForkRouter.cs +++ b/Libplanet.Extensions.ForkableActionEvaluator/HardForkRouter.cs @@ -52,7 +52,7 @@ private static void CheckCollision( { throw new ArgumentOutOfRangeException( nameof(pairs), - "The pairs must cover all range over blockchain. Its last element's start index wasn't 0."); + $"The pairs must cover all range over blockchain. Its last element's end index wasn't long.MaxValue({long.MaxValue})."); } if (pairs.Length == 1) diff --git a/Libplanet.Headless/Hosting/LibplanetNodeService.cs b/Libplanet.Headless/Hosting/LibplanetNodeService.cs index 486513ee6..51a85c961 100644 --- a/Libplanet.Headless/Hosting/LibplanetNodeService.cs +++ b/Libplanet.Headless/Hosting/LibplanetNodeService.cs @@ -43,6 +43,8 @@ public class LibplanetNodeService : BackgroundService, IDisposable public readonly IStateStore StateStore; + public readonly IKeyValueStore StateKeyValueStore; + public readonly BlockChain BlockChain; public readonly Swarm Swarm; @@ -174,6 +176,8 @@ IActionEvaluator BuildActionEvaluator(IActionEvaluatorConfiguration actionEvalua ); } + StateKeyValueStore = keyValueStore; + _obsoletedChainIds = chainIds.Where(chainId => chainId != BlockChain.Id).ToList(); _exceptionHandlerAction = exceptionHandlerAction; @@ -235,6 +239,7 @@ IActionEvaluator BuildActionEvaluator(IActionEvaluatorConfiguration actionEvalua ConsensusPrivateKey = Properties.ConsensusPrivateKey, ConsensusWorkers = 500, TargetBlockInterval = TimeSpan.FromMilliseconds(Properties.ConsensusTargetBlockIntervalMilliseconds ?? 7000), + ContextTimeoutOptions = Properties.ContextTimeoutOption, }; } diff --git a/Libplanet.Headless/Hosting/LibplanetNodeServiceProperties.cs b/Libplanet.Headless/Hosting/LibplanetNodeServiceProperties.cs index 5c87386d3..b174f398c 100644 --- a/Libplanet.Headless/Hosting/LibplanetNodeServiceProperties.cs +++ b/Libplanet.Headless/Hosting/LibplanetNodeServiceProperties.cs @@ -5,6 +5,7 @@ using Libplanet.Types.Blocks; using Libplanet.Crypto; using Libplanet.Net; +using Libplanet.Net.Consensus; namespace Libplanet.Headless.Hosting { @@ -67,6 +68,8 @@ public class LibplanetNodeServiceProperties public TimeSpan TipTimeout { get; set; } = TimeSpan.FromSeconds(60); + public ContextTimeoutOption ContextTimeoutOption { get; set; } + public int DemandBuffer { get; set; } = 1150; public ImmutableList ConsensusSeeds { get; set; } diff --git a/NineChronicles.Headless.AccessControlCenter/Controllers/AccessControlServiceController.cs b/NineChronicles.Headless.AccessControlCenter/Controllers/AccessControlServiceController.cs index 2e9658ecb..4888a6458 100644 --- a/NineChronicles.Headless.AccessControlCenter/Controllers/AccessControlServiceController.cs +++ b/NineChronicles.Headless.AccessControlCenter/Controllers/AccessControlServiceController.cs @@ -27,7 +27,7 @@ public AccessControlServiceController(IMutableAccessControlService accessControl [HttpGet("entries/{address}")] public ActionResult GetTxQuota(string address) { - var result = _accessControlService.GetTxQuota(new Address(address)); + var result = _accessControlService.GetTxQuotaAsync(new Address(address)).Result; return result != null ? result : NotFound(); } diff --git a/NineChronicles.Headless.Executable.sln b/NineChronicles.Headless.Executable.sln index 6e9604ede..ef9053828 100644 --- a/NineChronicles.Headless.Executable.sln +++ b/NineChronicles.Headless.Executable.sln @@ -72,14 +72,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libplanet.Crypto", "Lib9c\. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libplanet.Extensions.ActionEvaluatorCommonComponents", "Lib9c\.Libplanet.Extensions.ActionEvaluatorCommonComponents\Libplanet.Extensions.ActionEvaluatorCommonComponents.csproj", "{A6922395-36E5-4B0A-BEBD-9BCE34D08722}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libplanet.Extensions.RemoteBlockChainStates", "Lib9c\.Libplanet.Extensions.RemoteBlockChainStates\Libplanet.Extensions.RemoteBlockChainStates.csproj", "{8F9E5505-C157-4DF3-A419-FF0108731397}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NineChronicles.Headless.AccessControlCenter", "NineChronicles.Headless.AccessControlCenter\NineChronicles.Headless.AccessControlCenter.csproj", "{162C0F4B-A1D9-4132-BC34-31F1247BC26B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libplanet.Extensions.PluggedActionEvaluator", "Libplanet.Extensions.PluggedActionEvaluator\Libplanet.Extensions.PluggedActionEvaluator.csproj", "{DE91C36D-3999-47B6-A0BD-848C8EBA2A76}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lib9c.Plugin.Shared", "Lib9c\.Lib9c.Plugin.Shared\Lib9c.Plugin.Shared.csproj", "{3D32DA34-E619-429F-8421-848FF4F14417}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Mocks", "Lib9c\.Libplanet\Libplanet.Mocks\Libplanet.Mocks.csproj", "{F79B695B-6FCC-43F5-AEE4-88E484382B9B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Store.Remote", "Lib9c\.Libplanet\Libplanet.Store.Remote\Libplanet.Store.Remote.csproj", "{D1E15F81-8765-4DCA-9299-675415686C23}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -651,24 +653,6 @@ Global {A6922395-36E5-4B0A-BEBD-9BCE34D08722}.Release|x64.Build.0 = Release|Any CPU {A6922395-36E5-4B0A-BEBD-9BCE34D08722}.Release|x86.ActiveCfg = Release|Any CPU {A6922395-36E5-4B0A-BEBD-9BCE34D08722}.Release|x86.Build.0 = Release|Any CPU - {8F9E5505-C157-4DF3-A419-FF0108731397}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8F9E5505-C157-4DF3-A419-FF0108731397}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8F9E5505-C157-4DF3-A419-FF0108731397}.Debug|x64.ActiveCfg = Debug|Any CPU - {8F9E5505-C157-4DF3-A419-FF0108731397}.Debug|x64.Build.0 = Debug|Any CPU - {8F9E5505-C157-4DF3-A419-FF0108731397}.Debug|x86.ActiveCfg = Debug|Any CPU - {8F9E5505-C157-4DF3-A419-FF0108731397}.Debug|x86.Build.0 = Debug|Any CPU - {8F9E5505-C157-4DF3-A419-FF0108731397}.DevEx|Any CPU.ActiveCfg = Debug|Any CPU - {8F9E5505-C157-4DF3-A419-FF0108731397}.DevEx|Any CPU.Build.0 = Debug|Any CPU - {8F9E5505-C157-4DF3-A419-FF0108731397}.DevEx|x64.ActiveCfg = Debug|Any CPU - {8F9E5505-C157-4DF3-A419-FF0108731397}.DevEx|x64.Build.0 = Debug|Any CPU - {8F9E5505-C157-4DF3-A419-FF0108731397}.DevEx|x86.ActiveCfg = Debug|Any CPU - {8F9E5505-C157-4DF3-A419-FF0108731397}.DevEx|x86.Build.0 = Debug|Any CPU - {8F9E5505-C157-4DF3-A419-FF0108731397}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8F9E5505-C157-4DF3-A419-FF0108731397}.Release|Any CPU.Build.0 = Release|Any CPU - {8F9E5505-C157-4DF3-A419-FF0108731397}.Release|x64.ActiveCfg = Release|Any CPU - {8F9E5505-C157-4DF3-A419-FF0108731397}.Release|x64.Build.0 = Release|Any CPU - {8F9E5505-C157-4DF3-A419-FF0108731397}.Release|x86.ActiveCfg = Release|Any CPU - {8F9E5505-C157-4DF3-A419-FF0108731397}.Release|x86.Build.0 = Release|Any CPU {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Debug|Any CPU.Build.0 = Debug|Any CPU {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -723,6 +707,42 @@ Global {3D32DA34-E619-429F-8421-848FF4F14417}.Release|x64.Build.0 = Release|Any CPU {3D32DA34-E619-429F-8421-848FF4F14417}.Release|x86.ActiveCfg = Release|Any CPU {3D32DA34-E619-429F-8421-848FF4F14417}.Release|x86.Build.0 = Release|Any CPU + {F79B695B-6FCC-43F5-AEE4-88E484382B9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F79B695B-6FCC-43F5-AEE4-88E484382B9B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F79B695B-6FCC-43F5-AEE4-88E484382B9B}.Debug|x64.ActiveCfg = Debug|Any CPU + {F79B695B-6FCC-43F5-AEE4-88E484382B9B}.Debug|x64.Build.0 = Debug|Any CPU + {F79B695B-6FCC-43F5-AEE4-88E484382B9B}.Debug|x86.ActiveCfg = Debug|Any CPU + {F79B695B-6FCC-43F5-AEE4-88E484382B9B}.Debug|x86.Build.0 = Debug|Any CPU + {F79B695B-6FCC-43F5-AEE4-88E484382B9B}.DevEx|Any CPU.ActiveCfg = Debug|Any CPU + {F79B695B-6FCC-43F5-AEE4-88E484382B9B}.DevEx|Any CPU.Build.0 = Debug|Any CPU + {F79B695B-6FCC-43F5-AEE4-88E484382B9B}.DevEx|x64.ActiveCfg = Debug|Any CPU + {F79B695B-6FCC-43F5-AEE4-88E484382B9B}.DevEx|x64.Build.0 = Debug|Any CPU + {F79B695B-6FCC-43F5-AEE4-88E484382B9B}.DevEx|x86.ActiveCfg = Debug|Any CPU + {F79B695B-6FCC-43F5-AEE4-88E484382B9B}.DevEx|x86.Build.0 = Debug|Any CPU + {F79B695B-6FCC-43F5-AEE4-88E484382B9B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F79B695B-6FCC-43F5-AEE4-88E484382B9B}.Release|Any CPU.Build.0 = Release|Any CPU + {F79B695B-6FCC-43F5-AEE4-88E484382B9B}.Release|x64.ActiveCfg = Release|Any CPU + {F79B695B-6FCC-43F5-AEE4-88E484382B9B}.Release|x64.Build.0 = Release|Any CPU + {F79B695B-6FCC-43F5-AEE4-88E484382B9B}.Release|x86.ActiveCfg = Release|Any CPU + {F79B695B-6FCC-43F5-AEE4-88E484382B9B}.Release|x86.Build.0 = Release|Any CPU + {D1E15F81-8765-4DCA-9299-675415686C23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1E15F81-8765-4DCA-9299-675415686C23}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1E15F81-8765-4DCA-9299-675415686C23}.Debug|x64.ActiveCfg = Debug|Any CPU + {D1E15F81-8765-4DCA-9299-675415686C23}.Debug|x64.Build.0 = Debug|Any CPU + {D1E15F81-8765-4DCA-9299-675415686C23}.Debug|x86.ActiveCfg = Debug|Any CPU + {D1E15F81-8765-4DCA-9299-675415686C23}.Debug|x86.Build.0 = Debug|Any CPU + {D1E15F81-8765-4DCA-9299-675415686C23}.DevEx|Any CPU.ActiveCfg = Debug|Any CPU + {D1E15F81-8765-4DCA-9299-675415686C23}.DevEx|Any CPU.Build.0 = Debug|Any CPU + {D1E15F81-8765-4DCA-9299-675415686C23}.DevEx|x64.ActiveCfg = Debug|Any CPU + {D1E15F81-8765-4DCA-9299-675415686C23}.DevEx|x64.Build.0 = Debug|Any CPU + {D1E15F81-8765-4DCA-9299-675415686C23}.DevEx|x86.ActiveCfg = Debug|Any CPU + {D1E15F81-8765-4DCA-9299-675415686C23}.DevEx|x86.Build.0 = Debug|Any CPU + {D1E15F81-8765-4DCA-9299-675415686C23}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1E15F81-8765-4DCA-9299-675415686C23}.Release|Any CPU.Build.0 = Release|Any CPU + {D1E15F81-8765-4DCA-9299-675415686C23}.Release|x64.ActiveCfg = Release|Any CPU + {D1E15F81-8765-4DCA-9299-675415686C23}.Release|x64.Build.0 = Release|Any CPU + {D1E15F81-8765-4DCA-9299-675415686C23}.Release|x86.ActiveCfg = Release|Any CPU + {D1E15F81-8765-4DCA-9299-675415686C23}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/NineChronicles.Headless.Executable/Commands/ReplayCommand.Queries.cs b/NineChronicles.Headless.Executable/Commands/ReplayCommand.Queries.cs index a40d39800..3d61c5b89 100644 --- a/NineChronicles.Headless.Executable/Commands/ReplayCommand.Queries.cs +++ b/NineChronicles.Headless.Executable/Commands/ReplayCommand.Queries.cs @@ -47,7 +47,9 @@ query GetBlockData($hash: ID!) preEvaluationHash previousBlock { hash + stateRootHash } + stateRootHash } }} } @@ -87,6 +89,7 @@ private sealed class BlockType public string? Miner { get; set; } public string? PreEvaluationHash { get; set; } public BlockType? PreviousBlock { get; set; } + public string? StateRootHash { get; set; } } private sealed class TransactionQueryType diff --git a/NineChronicles.Headless.Executable/Commands/ReplayCommand.cs b/NineChronicles.Headless.Executable/Commands/ReplayCommand.cs index 9e1db0b20..23caeadfa 100644 --- a/NineChronicles.Headless.Executable/Commands/ReplayCommand.cs +++ b/NineChronicles.Headless.Executable/Commands/ReplayCommand.cs @@ -14,17 +14,19 @@ using Cocona.Help; using GraphQL.Client.Http; using GraphQL.Client.Serializer.SystemTextJson; +using Grpc.Net.Client; using Libplanet.Action; using Libplanet.Action.Loader; using Libplanet.Blockchain; using Libplanet.Blockchain.Policies; -using Libplanet.Extensions.RemoteBlockChainStates; using Libplanet.Types.Blocks; using Libplanet.RocksDBStore; using Libplanet.Action.State; using Libplanet.Common; using Libplanet.Crypto; using Libplanet.Store; +using Libplanet.Store.Remote; +using Libplanet.Store.Remote.Client; using Libplanet.Types.Tx; using Nekoyume.Action; using Nekoyume.Action.Loader; @@ -353,8 +355,10 @@ public int Blocks( public int RemoteTx( [Option("tx", new[] { 't' }, Description = "The transaction id")] string transactionId, - [Option("endpoint", new[] { 'e' }, Description = "GraphQL endpoint to get remote state")] - string endpoint) + [Option("endpoint", new[] { 'e' }, Description = "GraphQL endpoint to get block data.")] + string endpoint, + [Option("grpc-endpoint", new []{ 'g' }, Description = "gRPC endpoint to get remote states.")] + string grpcEndpoint) { var graphQlClient = new GraphQLHttpClient(new Uri(endpoint), new SystemTextJsonSerializer()); var transactionResponse = GetTransactionData(graphQlClient, transactionId); @@ -378,19 +382,41 @@ public int RemoteTx( var block = GetBlockData(graphQlClient, transactionResult.BlockHash)?.ChainQuery?.BlockQuery?.Block; var previousBlockHashValue = block?.PreviousBlock?.Hash; + var previousBlockHashStateRootHash = block?.PreviousBlock?.StateRootHash; var preEvaluationHashValue = block?.PreEvaluationHash; var minerValue = block?.Miner; - if (previousBlockHashValue is null || preEvaluationHashValue is null || minerValue is null) + if (previousBlockHashValue is null || preEvaluationHashValue is null || minerValue is null || previousBlockHashStateRootHash is null) { throw new CommandExitedException("Failed to get block from query", -1); } var miner = new Address(minerValue); - var explorerEndpoint = $"{endpoint}/explorer"; - var blockChainStates = new RemoteBlockChainStates(new Uri(explorerEndpoint)); + var channel = GrpcChannel.ForAddress(grpcEndpoint); + var keyValueServiceClient = new KeyValueStore.KeyValueStoreClient(channel); + var cacheKeyValueStore = + new RocksDBKeyValueStore(Path.Combine(Path.GetTempPath(), "9c-headless-replay-remotetx")); + var keyValueStore = new ReplicableKeyValueStore( + new RemoteKeyValueStore(keyValueServiceClient), + cacheKeyValueStore); + var store = new AnonymousStore + { + GetBlockDigest = hash => + { + var stateRootHash = HashDigest.FromString(block!.StateRootHash!); + var privateKey = new PrivateKey(); + var blockMetadata = new BlockMetadata( + 0, DateTimeOffset.Now, privateKey.PublicKey, null, null, null); + var blockContent = new BlockContent(blockMetadata); + var preEvaluationBlock = blockContent.Propose(); + var b = preEvaluationBlock.Sign(privateKey, stateRootHash); + return new BlockDigest(b.Header, ImmutableArray>.Empty); + } + }; + + var previousStateRootHash = HashDigest.FromString(previousBlockHashStateRootHash); + var blockChainStates = new BlockChainStates(store, new TrieStateStore(keyValueStore)); - var previousBlockHash = BlockHash.FromString(previousBlockHashValue); - var previousStates = new World(blockChainStates.GetWorldState(previousBlockHash)); + var previousStates = new World(blockChainStates.GetWorldState(previousStateRootHash)); var actions = transaction.Actions .Select(ToAction) diff --git a/NineChronicles.Headless.Executable/Commands/TxCommand.cs b/NineChronicles.Headless.Executable/Commands/TxCommand.cs index 823073764..6c6ddaf94 100644 --- a/NineChronicles.Headless.Executable/Commands/TxCommand.cs +++ b/NineChronicles.Headless.Executable/Commands/TxCommand.cs @@ -190,34 +190,6 @@ public void MigrationActivatedAccountsState() _console.Out.WriteLine(ByteUtil.Hex(raw)); } - [Command(Description = "Create MigrationAvatarState action and dump it.")] - public void MigrationAvatarState( - [Argument("directory-path", Description = "path of the directory contained hex-encoded avatar states.")] - string directoryPath, - [Argument("output-path", Description = "path of the output file dumped action.")] - string outputPath - ) - { - var files = Directory.GetFiles(directoryPath, "*", SearchOption.AllDirectories); - var avatarStates = files.Select(a => - { - var raw = File.ReadAllText(a); - return (Dictionary)_codec.Decode(ByteUtil.ParseHex(raw)); - }).ToList(); - var action = new MigrationAvatarState() - { - avatarStates = avatarStates - }; - - var encoded = new List( - (Text)nameof(Nekoyume.Action.MigrationAvatarState), - action.PlainValue - ); - - byte[] raw = _codec.Encode(encoded); - File.WriteAllText(outputPath, ByteUtil.Hex(raw)); - } - [Command(Description = "Create AddRedeemCode action and dump it.")] public void AddRedeemCode( [Argument("TABLE-PATH", Description = "A table file path for RedeemCodeListSheet")] diff --git a/NineChronicles.Headless.Executable/Configuration.cs b/NineChronicles.Headless.Executable/Configuration.cs index 29470f69d..bc2fbbcf5 100644 --- a/NineChronicles.Headless.Executable/Configuration.cs +++ b/NineChronicles.Headless.Executable/Configuration.cs @@ -52,6 +52,9 @@ public class Configuration public bool? RpcRemoteServer { get; set; } public bool? RpcHttpServer { get; set; } + // RemoteKeyValueService + public bool RemoteKeyValueService { get; set; } = false; + // GraphQL Server public bool GraphQLServer { get; set; } public string? GraphQLHost { get; set; } @@ -84,6 +87,7 @@ public class Configuration public string[]? ConsensusSeedStrings { get; set; } public ushort? ConsensusPort { get; set; } public double? ConsensusTargetBlockIntervalMilliseconds { get; set; } + public int? ConsensusProposeSecondBase { get; set; } public int? MaxTransactionPerBlock { get; set; } @@ -91,8 +95,6 @@ public class Configuration public double SentryTraceSampleRate { get; set; } = 0.01; - public StateServiceManagerServiceOptions? StateServiceManagerService { get; set; } - public AccessControlServiceOptions? AccessControlService { get; set; } public int ArenaParticipantsSyncInterval { get; set; } = 1000; @@ -143,10 +145,12 @@ public void Overwrite( string? consensusPrivateKeyString, string[]? consensusSeedStrings, double? consensusTargetBlockIntervalMilliseconds, + int? consensusProposeSecondBase, int? maxTransactionPerBlock, string? sentryDsn, double? sentryTraceSampleRate, - int? arenaParticipantsSyncInterval + int? arenaParticipantsSyncInterval, + bool? remoteKeyValueService ) { AppProtocolVersionString = appProtocolVersionString ?? AppProtocolVersionString; @@ -195,10 +199,12 @@ public void Overwrite( ConsensusSeedStrings = consensusSeedStrings ?? ConsensusSeedStrings; ConsensusPrivateKeyString = consensusPrivateKeyString ?? ConsensusPrivateKeyString; ConsensusTargetBlockIntervalMilliseconds = consensusTargetBlockIntervalMilliseconds ?? ConsensusTargetBlockIntervalMilliseconds; + ConsensusProposeSecondBase = consensusProposeSecondBase ?? ConsensusProposeSecondBase; MaxTransactionPerBlock = maxTransactionPerBlock ?? MaxTransactionPerBlock; SentryDsn = sentryDsn ?? SentryDsn; SentryTraceSampleRate = sentryTraceSampleRate ?? SentryTraceSampleRate; ArenaParticipantsSyncInterval = arenaParticipantsSyncInterval ?? ArenaParticipantsSyncInterval; + RemoteKeyValueService = remoteKeyValueService ?? RemoteKeyValueService; } } } diff --git a/NineChronicles.Headless.Executable/NineChronicles.Headless.Executable.csproj b/NineChronicles.Headless.Executable/NineChronicles.Headless.Executable.csproj index 433336e26..6de9ef47e 100644 --- a/NineChronicles.Headless.Executable/NineChronicles.Headless.Executable.csproj +++ b/NineChronicles.Headless.Executable/NineChronicles.Headless.Executable.csproj @@ -21,6 +21,8 @@ + + @@ -46,7 +48,6 @@ - diff --git a/NineChronicles.Headless.Executable/Program.cs b/NineChronicles.Headless.Executable/Program.cs index 9fd2fc96e..fe5ec281a 100644 --- a/NineChronicles.Headless.Executable/Program.cs +++ b/NineChronicles.Headless.Executable/Program.cs @@ -205,6 +205,9 @@ public async Task Run( [Option("consensus-target-block-interval", Description = "A target block interval used in consensus context. The unit is millisecond.")] double? consensusTargetBlockIntervalMilliseconds = null, + [Option("consensus-propose-second-base", + Description = "A propose second base for consensus context timeout. The unit is second.")] + int? consensusProposeSecondBase = null, [Option("maximum-transaction-per-block", Description = "Maximum transactions allowed in a block. null by default.")] int? maxTransactionPerBlock = null, @@ -219,6 +222,8 @@ public async Task Run( int? arenaParticipantsSyncInterval = null, [Option(Description = "arena participants list sync enable")] bool arenaParticipantsSync = true, + [Option(Description = "[DANGER] Turn on RemoteKeyValueService to debug.")] + bool remoteKeyValueService = false, [Ignore] CancellationToken? cancellationToken = null ) { @@ -304,8 +309,8 @@ public async Task Run( logActionRenders, confirmations, txLifeTime, messageTimeout, tipTimeout, demandBuffer, skipPreload, minimumBroadcastTarget, bucketSize, chainTipStaleBehaviorType, txQuotaPerSigner, maximumPollPeers, - consensusPort, consensusPrivateKeyString, consensusSeedStrings, consensusTargetBlockIntervalMilliseconds, - maxTransactionPerBlock, sentryDsn, sentryTraceSampleRate, arenaParticipantsSyncInterval + consensusPort, consensusPrivateKeyString, consensusSeedStrings, consensusTargetBlockIntervalMilliseconds, consensusProposeSecondBase, + maxTransactionPerBlock, sentryDsn, sentryTraceSampleRate, arenaParticipantsSyncInterval, remoteKeyValueService ); #if SENTRY || ! DEBUG @@ -363,11 +368,6 @@ public async Task Run( ); } - if (headlessConfig.StateServiceManagerService is { } stateServiceManagerServiceOptions) - { - await DownloadStateServices(stateServiceManagerServiceOptions); - } - try { IHostBuilder hostBuilder = Host.CreateDefaultBuilder(); @@ -406,6 +406,7 @@ public async Task Run( consensusPrivateKeyString: headlessConfig.ConsensusPrivateKeyString, consensusSeedStrings: headlessConfig.ConsensusSeedStrings, consensusTargetBlockIntervalMilliseconds: headlessConfig.ConsensusTargetBlockIntervalMilliseconds, + consensusProposeSecondBase: headlessConfig.ConsensusProposeSecondBase, maximumPollPeers: headlessConfig.MaximumPollPeers, actionEvaluatorConfiguration: actionEvaluatorConfiguration ); @@ -460,7 +461,7 @@ IActionLoader MakeSingleActionLoader() : new PrivateKey(ByteUtil.ParseHex(headlessConfig.MinerPrivateKeyString)); TimeSpan minerBlockInterval = TimeSpan.FromMilliseconds(headlessConfig.MinerBlockIntervalMilliseconds); var nineChroniclesProperties = - new NineChroniclesNodeServiceProperties(actionLoader, headlessConfig.StateServiceManagerService, headlessConfig.AccessControlService) + new NineChroniclesNodeServiceProperties(actionLoader, headlessConfig.AccessControlService) { MinerPrivateKey = minerPrivateKey, Libplanet = properties, @@ -575,6 +576,7 @@ IActionLoader MakeSingleActionLoader() SecretToken = secretToken, NoCors = headlessConfig.NoCors, UseMagicOnion = headlessConfig.RpcServer, + UseRemoteKeyValueService = headlessConfig.RemoteKeyValueService, HttpOptions = headlessConfig.RpcServer && headlessConfig.RpcHttpServer == true ? new GraphQLNodeServiceProperties.MagicOnionHttpOptions( $"{headlessConfig.RpcListenHost}:{headlessConfig.RpcListenPort}") @@ -614,36 +616,5 @@ IActionLoader MakeSingleActionLoader() static void ConfigureSentryOptions(SentryOptions o) { } - - private static Task DownloadStateServices(StateServiceManagerServiceOptions options) - { - Log.Information("Downloading StateServices..."); - - if (Directory.Exists(options.StateServicesDownloadPath)) - { - Directory.Delete(options.StateServicesDownloadPath, true); - } - - Directory.CreateDirectory(options.StateServicesDownloadPath); - - async Task DownloadStateService(string url) - { - var hashed = - Convert.ToHexString(HashDigest.DeriveFrom(Encoding.UTF8.GetBytes(url)).ToByteArray()); - var logger = Log.ForContext("StateService", hashed); - using var httpClient = new HttpClient(); - var downloadPath = Path.Join(options.StateServicesDownloadPath, hashed + ".zip"); - var extractPath = Path.Join(options.StateServicesDownloadPath, hashed); - logger.Debug("Downloading..."); - await File.WriteAllBytesAsync(downloadPath, await httpClient.GetByteArrayAsync(url)); - logger.Debug("Finished downloading."); - logger.Debug("Extracting..."); - ZipFile.ExtractToDirectory(downloadPath, extractPath); - logger.Debug("Finished extracting."); - } - - return Task.WhenAll(options.StateServices.Select(stateService => DownloadStateService(stateService.Path))) - .ContinueWith(_ => Log.Information("Finished downloading StateServices...")); - } } } diff --git a/NineChronicles.Headless.Executable/Store/AnonymousStore.cs b/NineChronicles.Headless.Executable/Store/AnonymousStore.cs new file mode 100644 index 000000000..d19a4c82c --- /dev/null +++ b/NineChronicles.Headless.Executable/Store/AnonymousStore.cs @@ -0,0 +1,240 @@ +using System; +using System.Collections.Generic; +using Libplanet.Crypto; +using Libplanet.Store; +using Libplanet.Types.Blocks; +using Libplanet.Types.Tx; + +namespace NineChronicles.Headless.Executable.Store; + +public class AnonymousStore : IStore +{ +#pragma warning disable CS8618 + public Func> ListChainIds { get; set; } + public Action DeleteChainId { get; set; } + public Func GetCanonicalChainId { get; set; } + public Action SetCanonicalChainId { get; set; } + public Func CountIndex { get; set; } + public Func> IterateIndexes { get; set; } + public Func IndexBlockHash { get; set; } + public Func AppendIndex { get; set; } + public Action ForkBlockIndexes { get; set; } + public Func GetTransaction { get; set; } + public Action PutTransaction { get; set; } + public Func> IterateBlockHashes { get; set; } + public Func GetBlock { get; set; } + public Func GetBlockIndex { get; set; } + public Func GetBlockDigest { get; set; } + public Action PutBlock { get; set; } + public Func DeleteBlock { get; set; } + public Func ContainsBlock { get; set; } + public Action PutTxExecution { get; set; } + public Func GetTxExecution { get; set; } + public Action PutTxIdBlockHashIndex { get; set; } + public Func GetFirstTxIdBlockHashIndex { get; set; } + public Func> IterateTxIdBlockHashIndex { get; set; } + public Action DeleteTxIdBlockHashIndex { get; set; } + public Func>> ListTxNonces { get; set; } + public Func GetTxNonce { get; set; } + public Action IncreaseTxNonce { get; set; } + public Func ContainsTransaction { get; set; } + public Func CountBlocks { get; set; } + public Action ForkTxNonces { get; set; } + public Action PruneOutdatedChains { get; set; } + public Func GetChainBlockCommit { get; set; } + public Action PutChainBlockCommit { get; set; } + public Func GetBlockCommit { get; set; } + public Action PutBlockCommit { get; set; } + public Action DeleteBlockCommit { get; set; } + public Func> GetBlockCommitHashes { get; set; } +#pragma warning restore CS8618 + + void IDisposable.Dispose() + { + } + + IEnumerable IStore.ListChainIds() + { + return ListChainIds(); + } + + void IStore.DeleteChainId(Guid chainId) + { + DeleteChainId(chainId); + } + + Guid? IStore.GetCanonicalChainId() + { + return GetCanonicalChainId(); + } + + void IStore.SetCanonicalChainId(Guid chainId) + { + SetCanonicalChainId(chainId); + } + + long IStore.CountIndex(Guid chainId) + { + return CountIndex(chainId); + } + + IEnumerable IStore.IterateIndexes(Guid chainId, int offset, int? limit) + { + return IterateIndexes(chainId, offset, limit); + } + + BlockHash? IStore.IndexBlockHash(Guid chainId, long index) + { + return IndexBlockHash(chainId, index); + } + + long IStore.AppendIndex(Guid chainId, BlockHash hash) + { + return AppendIndex(chainId, hash); + } + + void IStore.ForkBlockIndexes(Guid sourceChainId, Guid destinationChainId, BlockHash branchpoint) + { + ForkBlockIndexes(sourceChainId, destinationChainId, branchpoint); + } + + Transaction? IStore.GetTransaction(TxId txid) + { + return GetTransaction(txid); + } + + void IStore.PutTransaction(Transaction tx) + { + PutTransaction(tx); + } + + IEnumerable IStore.IterateBlockHashes() + { + return IterateBlockHashes(); + } + + Block? IStore.GetBlock(BlockHash blockHash) + { + return GetBlock(blockHash); + } + + long? IStore.GetBlockIndex(BlockHash blockHash) + { + return GetBlockIndex(blockHash); + } + + BlockDigest? IStore.GetBlockDigest(BlockHash blockHash) + { + return GetBlockDigest(blockHash); + } + + void IStore.PutBlock(Block block) + { + PutBlock(block); + } + + bool IStore.DeleteBlock(BlockHash blockHash) + { + return DeleteBlock(blockHash); + } + + bool IStore.ContainsBlock(BlockHash blockHash) + { + return ContainsBlock(blockHash); + } + + void IStore.PutTxExecution(TxExecution txExecution) + { + PutTxExecution(txExecution); + } + + TxExecution? IStore.GetTxExecution(BlockHash blockHash, TxId txid) + { + return GetTxExecution(blockHash, txid); + } + + void IStore.PutTxIdBlockHashIndex(TxId txId, BlockHash blockHash) + { + PutTxIdBlockHashIndex(txId, blockHash); + } + + BlockHash? IStore.GetFirstTxIdBlockHashIndex(TxId txId) + { + return GetFirstTxIdBlockHashIndex(txId); + } + + IEnumerable IStore.IterateTxIdBlockHashIndex(TxId txId) + { + return IterateTxIdBlockHashIndex(txId); + } + + void IStore.DeleteTxIdBlockHashIndex(TxId txId, BlockHash blockHash) + { + DeleteTxIdBlockHashIndex(txId, blockHash); + } + + IEnumerable> IStore.ListTxNonces(Guid chainId) + { + return ListTxNonces(chainId); + } + + long IStore.GetTxNonce(Guid chainId, Address address) + { + return GetTxNonce(chainId, address); + } + + void IStore.IncreaseTxNonce(Guid chainId, Address signer, long delta) + { + IncreaseTxNonce(chainId, signer, delta); + } + + bool IStore.ContainsTransaction(TxId txId) + { + return ContainsTransaction(txId); + } + + long IStore.CountBlocks() + { + return CountBlocks(); + } + + void IStore.ForkTxNonces(Guid sourceChainId, Guid destinationChainId) + { + ForkTxNonces(sourceChainId, destinationChainId); + } + + void IStore.PruneOutdatedChains(bool noopWithoutCanon) + { + PruneOutdatedChains(noopWithoutCanon); + } + + BlockCommit? IStore.GetChainBlockCommit(Guid chainId) + { + return GetChainBlockCommit(chainId); + } + + void IStore.PutChainBlockCommit(Guid chainId, BlockCommit blockCommit) + { + PutChainBlockCommit(chainId, blockCommit); + } + + BlockCommit? IStore.GetBlockCommit(BlockHash blockHash) + { + return GetBlockCommit(blockHash); + } + + void IStore.PutBlockCommit(BlockCommit blockCommit) + { + PutBlockCommit(blockCommit); + } + + void IStore.DeleteBlockCommit(BlockHash blockHash) + { + DeleteBlockCommit(blockHash); + } + + IEnumerable IStore.GetBlockCommitHashes() + { + return GetBlockCommitHashes(); + } +} diff --git a/NineChronicles.Headless.Executable/Store/ReplicableKeyValueStore.cs b/NineChronicles.Headless.Executable/Store/ReplicableKeyValueStore.cs new file mode 100644 index 000000000..ceaf0f903 --- /dev/null +++ b/NineChronicles.Headless.Executable/Store/ReplicableKeyValueStore.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.Linq; +using Libplanet.Store.Trie; + +namespace NineChronicles.Headless.Executable.Store; + +public class ReplicableKeyValueStore : IKeyValueStore +{ + private readonly IKeyValueStore _readKvStore; + private readonly IKeyValueStore _writeKvStore; + + public ReplicableKeyValueStore(IKeyValueStore readKvStore, IKeyValueStore writeKvStore) + { + _readKvStore = readKvStore; + _writeKvStore = writeKvStore; + } + + public void Dispose() + { + _readKvStore.Dispose(); + _writeKvStore.Dispose(); + } + + public byte[] Get(in KeyBytes key) + { + try + { + if (_writeKvStore.Get(key) is { } cachedValue) + { + return cachedValue; + } + } + catch (KeyNotFoundException) + { + if (_readKvStore.Get(key) is { } value) + { + _writeKvStore.Set(key, value); + return value; + } + } + + throw new KeyNotFoundException(); + } + + public void Set(in KeyBytes key, byte[] value) + { + _writeKvStore.Set(key, value); + } + + public void Set(IDictionary values) + { + _writeKvStore.Set(values); + } + + public void Delete(in KeyBytes key) + { + _writeKvStore.Delete(key); + } + + public void Delete(IEnumerable keys) + { + _writeKvStore.Delete(keys); + } + + public bool Exists(in KeyBytes key) + { + return _writeKvStore.Exists(key) || _readKvStore.Exists(key); + } + + public IEnumerable ListKeys() + { + return _readKvStore.ListKeys().Concat(_writeKvStore.ListKeys()).Distinct(); + } +} diff --git a/NineChronicles.Headless.Executable/appsettings-schema.json b/NineChronicles.Headless.Executable/appsettings-schema.json index 55a6e225a..5360a9d00 100644 --- a/NineChronicles.Headless.Executable/appsettings-schema.json +++ b/NineChronicles.Headless.Executable/appsettings-schema.json @@ -177,6 +177,9 @@ } }, "required": ["StateServices", "StateServicesDownloadPath", "RemoteBlockChainStatesEndpoint"] + }, + "RemoteKeyValueService": { + "type": "boolean" } } } diff --git a/NineChronicles.Headless.Executable/appsettings.json b/NineChronicles.Headless.Executable/appsettings.json index 4cce07805..3e4d70044 100644 --- a/NineChronicles.Headless.Executable/appsettings.json +++ b/NineChronicles.Headless.Executable/appsettings.json @@ -80,6 +80,7 @@ "RpcListenHost": "127.0.0.1", "RpcListenPort": 31238, "RpcRemoteServer": true, + "RemoteKeyValueService": false, "GraphQLServer": true, "GraphQLHost": "127.0.0.1", "GraphQLPort": 31280, diff --git a/NineChronicles.Headless.Tests/GraphTypes/States/Models/CrystalMonsterCollectionMultiplierSheetTypeTest.cs b/NineChronicles.Headless.Tests/GraphTypes/States/Models/CrystalMonsterCollectionMultiplierSheetTypeTest.cs index 491f17b83..165dee6a8 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/States/Models/CrystalMonsterCollectionMultiplierSheetTypeTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/States/Models/CrystalMonsterCollectionMultiplierSheetTypeTest.cs @@ -69,6 +69,11 @@ public async Task Query() ["level"] = 7, ["multiplier"] = 300, }, + new Dictionary + { + ["level"] = 8, + ["multiplier"] = 300, + } }; var expected = new Dictionary { { "orderedList", list } }; Assert.Equal(expected, data); diff --git a/NineChronicles.Headless/BlockChainService.cs b/NineChronicles.Headless/BlockChainService.cs index 18989f137..db7e565ad 100644 --- a/NineChronicles.Headless/BlockChainService.cs +++ b/NineChronicles.Headless/BlockChainService.cs @@ -199,7 +199,7 @@ public async UnaryResult> GetAvatarStatesByBlockHash( var addresses = addressBytesList.Select(a => new Address(a)).ToList(); var taskList = addresses.Select(address => Task.Run(() => { - var value = worldState.GetFullAvatarStateRaw(address); + var value = GetFullAvatarStateRaw(worldState, address); result.TryAdd(address.ToByteArray(), _codec.Encode(value ?? Null.Value)); })); @@ -217,7 +217,7 @@ public async UnaryResult> GetAvatarStatesByStateRootH var result = new ConcurrentDictionary(); var taskList = addresses.Select(address => Task.Run(() => { - var value = worldState.GetFullAvatarStateRaw(address); + var value = GetFullAvatarStateRaw(worldState, address); result.TryAdd(address.ToByteArray(), _codec.Encode(value ?? Null.Value)); })); @@ -447,5 +447,33 @@ public UnaryResult RemoveClient(byte[] addressBytes) _publisher.RemoveClient(address).Wait(); return new UnaryResult(true); } + + // Returning value is a list of [ Avatar, Inventory, QuestList, WorldInformation ] + private static IValue GetFullAvatarStateRaw(IWorldState worldState, Address address) + { + var serializedAvatarRaw = worldState.GetAccountState(Addresses.Avatar).GetState(address); + if (serializedAvatarRaw is not List) + { + Log.Warning( + "Avatar state ({AvatarAddress}) should be " + + "List but: {Raw}", + address.ToHex(), + serializedAvatarRaw); + return null; + } + + var serializedInventoryRaw = + worldState.GetAccountState(Addresses.Inventory).GetState(address); + var serializedQuestListRaw = + worldState.GetAccountState(Addresses.QuestList).GetState(address); + var serializedWorldInformationRaw = + worldState.GetAccountState(Addresses.WorldInformation).GetState(address); + + return new List( + serializedAvatarRaw, + serializedInventoryRaw!, + serializedQuestListRaw!, + serializedWorldInformationRaw!); + } } } diff --git a/NineChronicles.Headless/GraphQLService.cs b/NineChronicles.Headless/GraphQLService.cs index 6dd982ed3..94d4cfc45 100644 --- a/NineChronicles.Headless/GraphQLService.cs +++ b/NineChronicles.Headless/GraphQLService.cs @@ -8,6 +8,7 @@ using Grpc.Net.Client; using Libplanet.Crypto; using Libplanet.Explorer.Schemas; +using Libplanet.Store.Remote.Server; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core; @@ -35,6 +36,8 @@ public class GraphQLService public const string UseMagicOnionKey = "useMagicOnion"; + public const string UseRemoteKeyValueServiceKey = "useRemoteKeyValueService"; + public const string MagicOnionTargetKey = "magicOnionTarget"; private StandaloneContext StandaloneContext { get; } @@ -81,6 +84,11 @@ public IHostBuilder Configure(IHostBuilder hostBuilder) dictionary[UseMagicOnionKey] = string.Empty; } + if (GraphQlNodeServiceProperties.UseRemoteKeyValueService) + { + dictionary[UseRemoteKeyValueServiceKey] = string.Empty; + } + if (GraphQlNodeServiceProperties.HttpOptions is { } options) { dictionary[MagicOnionTargetKey] = options.Target; @@ -241,6 +249,12 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseEndpoints(endpoints => { endpoints.MapControllers(); + + if (Configuration[UseRemoteKeyValueServiceKey] is not null) + { + endpoints.MapGrpcService(); + } + if (!(Configuration[UseMagicOnionKey] is null)) { endpoints.MapMagicOnionService(); @@ -261,6 +275,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) .MethodHandlers, "/_/"); } } + endpoints.MapHealthChecks("/health-check"); }); diff --git a/NineChronicles.Headless/GraphQLServiceExtensions.cs b/NineChronicles.Headless/GraphQLServiceExtensions.cs index c742dbc0f..d25679287 100644 --- a/NineChronicles.Headless/GraphQLServiceExtensions.cs +++ b/NineChronicles.Headless/GraphQLServiceExtensions.cs @@ -54,6 +54,8 @@ public static IServiceCollection AddLibplanetScalarTypes(this IServiceCollection services.TryAddSingleton>(); services.TryAddSingleton>(); services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); return services; } @@ -79,6 +81,7 @@ public static IServiceCollection AddLibplanetExplorer(this IServiceCollection se services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(_ => new StateQuery() { Name = "LibplanetStateQuery", diff --git a/NineChronicles.Headless/GraphTypes/Diff/DiffGraphType.cs b/NineChronicles.Headless/GraphTypes/Diff/DiffGraphType.cs new file mode 100644 index 000000000..5c79edecb --- /dev/null +++ b/NineChronicles.Headless/GraphTypes/Diff/DiffGraphType.cs @@ -0,0 +1,12 @@ +using GraphQL.Types; + +namespace NineChronicles.Headless.GraphTypes.Diff; + +public class DiffGraphType : UnionGraphType +{ + public DiffGraphType() + { + Type(); + Type(); + } +} diff --git a/NineChronicles.Headless/GraphTypes/Diff/IDiffType.cs b/NineChronicles.Headless/GraphTypes/Diff/IDiffType.cs new file mode 100644 index 000000000..88235ac74 --- /dev/null +++ b/NineChronicles.Headless/GraphTypes/Diff/IDiffType.cs @@ -0,0 +1,6 @@ +namespace NineChronicles.Headless.GraphTypes.Diff; + +public interface IDiffType +{ + string Path { get; } +} diff --git a/NineChronicles.Headless/GraphTypes/Diff/RootStateDiffType.cs b/NineChronicles.Headless/GraphTypes/Diff/RootStateDiffType.cs new file mode 100644 index 000000000..39ddfa909 --- /dev/null +++ b/NineChronicles.Headless/GraphTypes/Diff/RootStateDiffType.cs @@ -0,0 +1,33 @@ +using GraphQL.Types; + +namespace NineChronicles.Headless.GraphTypes.Diff; + +public class RootStateDiffType : ObjectGraphType +{ + public class Value : IDiffType + { + public string Path { get; } + public StateDiffType.Value[] Diffs { get; } + + public Value(string path, StateDiffType.Value[] diffs) + { + Path = path; + Diffs = diffs; + } + } + + public RootStateDiffType() + { + Name = "RootStateDiff"; + + Field>( + "Path", + description: "The path to the root state difference." + ); + + Field>>>( + "Diffs", + description: "List of state differences under this root." + ); + } +} diff --git a/NineChronicles.Headless/GraphTypes/Diff/StateDiffType.cs b/NineChronicles.Headless/GraphTypes/Diff/StateDiffType.cs new file mode 100644 index 000000000..407165f2a --- /dev/null +++ b/NineChronicles.Headless/GraphTypes/Diff/StateDiffType.cs @@ -0,0 +1,48 @@ +using Bencodex; +using Bencodex.Types; +using GraphQL.Types; +using Libplanet.Common; + +namespace NineChronicles.Headless.GraphTypes.Diff; + +public class StateDiffType : ObjectGraphType +{ + public class Value : IDiffType + { + public string Path { get; } + public IValue BaseState { get; } + public IValue? ChangedState { get; } + + public Value(string path, IValue baseState, IValue? changedState) + { + Path = path; + BaseState = baseState; + ChangedState = changedState; + } + } + + public StateDiffType() + { + Name = "StateDiff"; + + Field>( + "Path", + description: "The path of the state difference." + ); + + Field>( + "BaseState", + description: "The base state before changes.", + resolve: context => ByteUtil.Hex(new Codec().Encode(context.Source.BaseState)) + ); + + Field( + "ChangedState", + description: "The state after changes.", + resolve: context => + context.Source.ChangedState is null + ? null + : ByteUtil.Hex(new Codec().Encode(context.Source.ChangedState)) + ); + } +} diff --git a/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs b/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs index 463441863..8598ecd4c 100644 --- a/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs +++ b/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs @@ -23,6 +23,9 @@ using Nekoyume.Model; using Nekoyume.Module; using NineChronicles.Headless.GraphTypes.States; +using NineChronicles.Headless.GraphTypes.Diff; +using System.Security.Cryptography; +using System.Text; using static NineChronicles.Headless.NCActionUtils; using Transaction = Libplanet.Types.Tx.Transaction; @@ -43,13 +46,20 @@ public StandaloneQuery(StandaloneContext standaloneContext, IConfiguration confi { Name = "hash", Description = "Offset block hash for query.", + }, + new QueryArgument + { + Name = "index", + Description = "Offset block index for query." }), resolve: context => { - BlockHash? blockHash = context.GetArgument("hash") switch + BlockHash blockHash = (context.GetArgument("hash"), context.GetArgument("index")) switch { - byte[] bytes => new BlockHash(bytes), - null => standaloneContext.BlockChain?.Tip?.Hash, + ({ } bytes, null) => new BlockHash(bytes), + (null, { } index) => standaloneContext.BlockChain[index].Hash, + (not null, not null) => throw new ArgumentException("Only one of 'hash' and 'index' must be given."), + (null, null) => standaloneContext.BlockChain.Tip.Hash, }; if (!(standaloneContext.BlockChain is { } chain)) @@ -59,20 +69,104 @@ public StandaloneQuery(StandaloneContext standaloneContext, IConfiguration confi return new StateContext( chain.GetWorldState(blockHash), - blockHash switch - { - BlockHash bh => chain[bh].Index, - null => chain.Tip!.Index, - }, + chain[blockHash].Index, stateMemoryCache ); } ); + Field>>>( + name: "diffs", + description: "This field allows you to query the diffs between two blocks." + + " `baseIndex` is the reference block index, and changedIndex is the block index from which to check" + + " what changes have occurred relative to `baseIndex`." + + " Both indices must not be higher than the current block on the chain nor lower than the genesis block index (0)." + + " The difference between the two blocks must be greater than zero for a valid comparison and less than ten for performance reasons.", + arguments: new QueryArguments( + new QueryArgument> + { + Name = "baseIndex", + Description = "The index of the reference block from which the state is retrieved." + }, + new QueryArgument> + { + Name = "changedIndex", + Description = "The index of the target block for comparison." + } + ), + resolve: context => + { + if (!(standaloneContext.BlockChain is BlockChain blockChain)) + { + throw new ExecutionError( + $"{nameof(StandaloneContext)}.{nameof(StandaloneContext.BlockChain)} was not set yet!" + ); + } + + var baseIndex = context.GetArgument("baseIndex"); + var changedIndex = context.GetArgument("changedIndex"); + + var blockInterval = Math.Abs(changedIndex - baseIndex); + if (blockInterval >= 10 || blockInterval == 0) + { + throw new ExecutionError( + "Interval between baseIndex and changedIndex should not be greater than 10 or zero" + ); + } + + var baseBlockStateRootHash = blockChain[baseIndex].StateRootHash.ToString(); + var changedBlockStateRootHash = blockChain[changedIndex].StateRootHash.ToString(); + + var baseStateRootHash = HashDigest.FromString(baseBlockStateRootHash); + var targetStateRootHash = HashDigest.FromString( + changedBlockStateRootHash + ); + + var stateStore = standaloneContext.StateStore; + var baseTrieModel = stateStore.GetStateRoot(baseStateRootHash); + var targetTrieModel = stateStore.GetStateRoot(targetStateRootHash); + + IDiffType[] diffs = baseTrieModel + .Diff(targetTrieModel) + .Select(x => + { + if (x.TargetValue is not null) + { + var baseSubTrieModel = stateStore.GetStateRoot(new HashDigest((Binary)x.SourceValue)); + var targetSubTrieModel = stateStore.GetStateRoot(new HashDigest((Binary)x.TargetValue)); + var subDiff = baseSubTrieModel + .Diff(targetSubTrieModel) + .Select(diff => + { + return new StateDiffType.Value( + Encoding.Default.GetString(diff.Path.ByteArray.ToArray()), + diff.SourceValue, + diff.TargetValue); + }).ToArray(); + return (IDiffType)new RootStateDiffType.Value( + Encoding.Default.GetString(x.Path.ByteArray.ToArray()), + subDiff + ); + } + else + { + return new StateDiffType.Value( + Encoding.Default.GetString(x.Path.ByteArray.ToArray()), + x.SourceValue, + x.TargetValue + ); + } + }).ToArray(); + + return diffs; + } + ); + Field( name: "state", arguments: new QueryArguments( new QueryArgument { Name = "hash", Description = "The hash of the block used to fetch state from chain." }, + new QueryArgument { Name = "index", Description = "The index of the block used to fetch state from chain." }, new QueryArgument> { Name = "accountAddress", Description = "The address of account to fetch from the chain." }, new QueryArgument> { Name = "address", Description = "The address of state to fetch from the account." } ), @@ -84,10 +178,14 @@ public StandaloneQuery(StandaloneContext standaloneContext, IConfiguration confi $"{nameof(StandaloneContext)}.{nameof(StandaloneContext.BlockChain)} was not set yet!"); } - var blockHashByteArray = context.GetArgument("hash"); - var blockHash = blockHashByteArray is null - ? blockChain.Tip.Hash - : new BlockHash(blockHashByteArray); + var blockHash = (context.GetArgument("hash"), context.GetArgument("index")) switch + { + (not null, not null) => throw new ArgumentException( + "Only one of 'hash' and 'index' must be given."), + (null, { } index) => blockChain[index].Hash, + ({ } bytes, null) => new BlockHash(bytes), + (null, null) => blockChain.Tip.Hash, + }; var accountAddress = context.GetArgument
("accountAddress"); var address = context.GetArgument
("address"); diff --git a/NineChronicles.Headless/HostBuilderExtensions.cs b/NineChronicles.Headless/HostBuilderExtensions.cs index ff4940bf9..0767f4bbc 100644 --- a/NineChronicles.Headless/HostBuilderExtensions.cs +++ b/NineChronicles.Headless/HostBuilderExtensions.cs @@ -35,12 +35,8 @@ NineChroniclesNodeService service services.AddSingleton(provider => service.Swarm); services.AddSingleton(provider => service.BlockChain); services.AddSingleton(provider => service.Store); - - if (properties.StateServiceManagerService is { } stateServiceManagerServiceOptions) - { - var stateServiceManagerService = new StateServiceManagerService(stateServiceManagerServiceOptions); - services.AddSingleton(provider => stateServiceManagerService); - } + services.AddSingleton(provider => Serilog.Log.Logger); + services.AddSingleton(provider => service.StateKeyValueStore); if (properties.Libplanet is { } libplanetNodeServiceProperties) { diff --git a/NineChronicles.Headless/NineChronicles.Headless.csproj b/NineChronicles.Headless/NineChronicles.Headless.csproj index 5e66fcf52..6586f21b1 100644 --- a/NineChronicles.Headless/NineChronicles.Headless.csproj +++ b/NineChronicles.Headless/NineChronicles.Headless.csproj @@ -22,6 +22,7 @@ + diff --git a/NineChronicles.Headless/NineChroniclesNodeService.cs b/NineChronicles.Headless/NineChroniclesNodeService.cs index 18f784cdf..ceef8bcb4 100644 --- a/NineChronicles.Headless/NineChroniclesNodeService.cs +++ b/NineChronicles.Headless/NineChroniclesNodeService.cs @@ -14,6 +14,7 @@ using Libplanet.Headless.Hosting; using Libplanet.Net; using Libplanet.Store; +using Libplanet.Store.Trie; using Microsoft.Extensions.Hosting; using Nekoyume.Blockchain; using Nekoyume.Blockchain.Policy; @@ -53,6 +54,8 @@ public class NineChroniclesNodeService : IHostedService, IDisposable public IStore Store => NodeService.Store; + public IKeyValueStore StateKeyValueStore => NodeService.StateKeyValueStore; + public PrivateKey? MinerPrivateKey { get; set; } static NineChroniclesNodeService() @@ -279,6 +282,7 @@ internal void ConfigureContext(StandaloneContext standaloneContext) standaloneContext.NineChroniclesNodeService = this; standaloneContext.BlockChain = Swarm.BlockChain; standaloneContext.Store = Store; + standaloneContext.StateStore = NodeService.StateStore; standaloneContext.Swarm = Swarm; standaloneContext.CurrencyFactory = new CurrencyFactory(() => standaloneContext.BlockChain.GetWorldState(standaloneContext.BlockChain.Tip.Hash)); diff --git a/NineChronicles.Headless/Properties/GraphQLNodeServiceProperties.cs b/NineChronicles.Headless/Properties/GraphQLNodeServiceProperties.cs index 9f793ea65..8a9242007 100644 --- a/NineChronicles.Headless/Properties/GraphQLNodeServiceProperties.cs +++ b/NineChronicles.Headless/Properties/GraphQLNodeServiceProperties.cs @@ -13,6 +13,7 @@ public class GraphQLNodeServiceProperties public bool NoCors { get; set; } public bool UseMagicOnion { get; set; } + public bool UseRemoteKeyValueService { get; set; } public MagicOnionHttpOptions? HttpOptions { get; set; } diff --git a/NineChronicles.Headless/Properties/NineChroniclesNodeServiceProperties.cs b/NineChronicles.Headless/Properties/NineChroniclesNodeServiceProperties.cs index 5e365ccdc..f60740a03 100644 --- a/NineChronicles.Headless/Properties/NineChroniclesNodeServiceProperties.cs +++ b/NineChronicles.Headless/Properties/NineChroniclesNodeServiceProperties.cs @@ -7,6 +7,7 @@ using Libplanet.Net; using Libplanet.Headless.Hosting; using Libplanet.Headless; +using Libplanet.Net.Consensus; using Nekoyume; namespace NineChronicles.Headless.Properties @@ -14,10 +15,9 @@ namespace NineChronicles.Headless.Properties public class NineChroniclesNodeServiceProperties { public NineChroniclesNodeServiceProperties( - IActionLoader actionLoader, StateServiceManagerServiceOptions? stateServiceManagerServiceOptions, AccessControlServiceOptions? accessControlServiceOptions) + IActionLoader actionLoader, AccessControlServiceOptions? accessControlServiceOptions) { ActionLoader = actionLoader; - StateServiceManagerService = stateServiceManagerServiceOptions; AccessControlServiceOptions = accessControlServiceOptions; } @@ -57,8 +57,6 @@ public NineChroniclesNodeServiceProperties( public IActionLoader ActionLoader { get; init; } - public StateServiceManagerServiceOptions? StateServiceManagerService { get; } - public AccessControlServiceOptions? AccessControlServiceOptions { get; } public static LibplanetNodeServiceProperties @@ -93,6 +91,7 @@ public static LibplanetNodeServiceProperties string? consensusPrivateKeyString = null, string[]? consensusSeedStrings = null, double? consensusTargetBlockIntervalMilliseconds = null, + int? consensusProposeSecondBase = null, IActionEvaluatorConfiguration? actionEvaluatorConfiguration = null) { var swarmPrivateKey = string.IsNullOrEmpty(swarmPrivateKeyString) @@ -109,6 +108,10 @@ public static LibplanetNodeServiceProperties var peers = peerStrings.Select(PropertyParser.ParsePeer).ToImmutableArray(); var consensusSeeds = consensusSeedStrings?.Select(PropertyParser.ParsePeer).ToImmutableList(); + var consensusContextTimeoutOption = consensusProposeSecondBase.HasValue + ? new ContextTimeoutOption(consensusProposeSecondBase.Value) + : new ContextTimeoutOption(); + return new LibplanetNodeServiceProperties { Host = swarmHost, @@ -144,6 +147,7 @@ public static LibplanetNodeServiceProperties ConsensusSeeds = consensusSeeds, ConsensusPrivateKey = consensusPrivateKey, ConsensusTargetBlockIntervalMilliseconds = consensusTargetBlockIntervalMilliseconds, + ContextTimeoutOption = consensusContextTimeoutOption, ActionEvaluatorConfiguration = actionEvaluatorConfiguration ?? new DefaultActionEvaluatorConfiguration(), }; } diff --git a/NineChronicles.Headless/Properties/StateServiceManagerServiceOptions.cs b/NineChronicles.Headless/Properties/StateServiceManagerServiceOptions.cs deleted file mode 100644 index 9b08cacae..000000000 --- a/NineChronicles.Headless/Properties/StateServiceManagerServiceOptions.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace NineChronicles.Headless.Properties; - -public class StateServiceManagerServiceOptions -{ - [Required] - public StateService[] StateServices { get; set; } = null!; - - [Required] - public string StateServicesDownloadPath { get; set; } = null!; - - [Required] - public string RemoteBlockChainStatesEndpoint { get; set; } = null!; - - public class StateService - { - [Required] - public string Path { get; set; } = null!; - - [Required] - public ushort Port { get; set; } - } -} diff --git a/NineChronicles.Headless/Services/RedisAccessControlService.cs b/NineChronicles.Headless/Services/RedisAccessControlService.cs index 4dce4a4f3..78a3aec7c 100644 --- a/NineChronicles.Headless/Services/RedisAccessControlService.cs +++ b/NineChronicles.Headless/Services/RedisAccessControlService.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using StackExchange.Redis; using Libplanet.Crypto; using Nekoyume.Blockchain; @@ -23,18 +24,17 @@ public RedisAccessControlService(string storageUri) _db = redis.GetDatabase(); } - public int? GetTxQuota(Address address) + public async Task GetTxQuotaAsync(Address address) { try { - RedisValue result = _db.StringGet(address.ToString()); - + RedisValue result = await _db.StringGetAsync(address.ToString()); return !result.IsNull ? Convert.ToInt32(result) : null; } - catch (RedisTimeoutException) + catch (RedisTimeoutException ex) { Log.ForContext("Source", nameof(IAccessControlService)) - .Error("\"{Address}\" Redis timeout.", address); + .Error(ex, "\"{Address}\" Redis timeout encountered.", address); return null; } } diff --git a/NineChronicles.Headless/Services/RestAPIAccessControlService.cs b/NineChronicles.Headless/Services/RestAPIAccessControlService.cs index 733159334..73c39d306 100644 --- a/NineChronicles.Headless/Services/RestAPIAccessControlService.cs +++ b/NineChronicles.Headless/Services/RestAPIAccessControlService.cs @@ -21,7 +21,7 @@ public RestAPIAccessControlService(string baseUrl) }; } - public int? GetTxQuota(Address address) + public Task GetTxQuotaAsync(Address address) { try { @@ -31,7 +31,7 @@ public RestAPIAccessControlService(string baseUrl) if (response.IsSuccessStatusCode) { string resultString = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - return Convert.ToInt32(resultString); + return Task.FromResult(Convert.ToInt32(resultString)); } } catch (TaskCanceledException) @@ -45,7 +45,7 @@ public RestAPIAccessControlService(string baseUrl) .Error(ex, "HttpRequestException occurred for \"{Address}\".", address); } - return null; + return Task.FromResult(null); } } } diff --git a/NineChronicles.Headless/Services/SQLiteAccessControlService.cs b/NineChronicles.Headless/Services/SQLiteAccessControlService.cs index 1ddfaac65..1d370432d 100644 --- a/NineChronicles.Headless/Services/SQLiteAccessControlService.cs +++ b/NineChronicles.Headless/Services/SQLiteAccessControlService.cs @@ -1,4 +1,5 @@ using System; +using System.Threading.Tasks; using Microsoft.Data.Sqlite; using Libplanet.Crypto; using Nekoyume.Blockchain; @@ -34,7 +35,7 @@ public SQLiteAccessControlService(string connectionString) } } - public int? GetTxQuota(Address address) + public Task GetTxQuotaAsync(Address address) { try { @@ -49,7 +50,7 @@ public SQLiteAccessControlService(string connectionString) if (queryResult != null && queryResult != DBNull.Value) { - return Convert.ToInt32(queryResult); + return Task.FromResult(Convert.ToInt32(queryResult)); } } catch (Exception ex) @@ -57,7 +58,7 @@ public SQLiteAccessControlService(string connectionString) Log.Error(ex, "An error occurred while getting transaction quota."); } - return null; + return Task.FromResult(null); } private void ExecuteNonQuery(SqliteConnection connection, string commandText) diff --git a/NineChronicles.Headless/Services/StateService.cs b/NineChronicles.Headless/Services/StateService.cs deleted file mode 100644 index 0f2620b65..000000000 --- a/NineChronicles.Headless/Services/StateService.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; - -namespace NineChronicles.Headless.Services; - -public class StateService : IDisposable -{ - public StateService(string path, int port, string remoteBlockChainStatesEndpoint) - { - Process = new Process(); - Process.StartInfo.FileName = "dotnet"; - Process.StartInfo.Arguments = $"{path} --urls=http://localhost:{port}"; - Process.StartInfo.EnvironmentVariables["RemoteBlockChainStatesEndpoint"] = remoteBlockChainStatesEndpoint; - Process.Start(); - } - - public Task StartAsync(CancellationToken token) => Process.WaitForExitAsync(token); - - public Task StopAsync(CancellationToken token) - { - Process.Kill(); - return Task.CompletedTask; - } - - public void Dispose() => Process.Dispose(); - - private Process Process { get; } -} diff --git a/NineChronicles.Headless/Services/StateServiceManagerService.cs b/NineChronicles.Headless/Services/StateServiceManagerService.cs deleted file mode 100644 index b4f89a29b..000000000 --- a/NineChronicles.Headless/Services/StateServiceManagerService.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using GraphQL; -using Libplanet.Common; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; -using NineChronicles.Headless.Properties; - -namespace NineChronicles.Headless.Services; - -public class StateServiceManagerService : IHostedService, IDisposable -{ - private IEnumerable StateServices { get; init; } - - public StateServiceManagerService(StateServiceManagerServiceOptions options) - { - if (options.StateServices is null || options.StateServices.Any(x => x.Path is null)) - { - throw new ArgumentException(nameof(options)); - } - - string ToStateServiceDLLPath(string url) - { - return Path.Join( - options.StateServicesDownloadPath, - Convert.ToHexString(HashDigest.DeriveFrom(Encoding.UTF8.GetBytes(url)).ToByteArray()), - "Lib9c.StateService.dll"); - } - - StateServices = options.StateServices.Select(conf => - conf.Path is null - ? throw new ArgumentException(nameof(options)) - : new StateService(ToStateServiceDLLPath(conf.Path), conf.Port, options.RemoteBlockChainStatesEndpoint)).ToList(); - } - - public Task StartAsync(CancellationToken cancellationToken) => - Task.WhenAny(StateServices.Select(s => s.StartAsync(cancellationToken))); - - public Task StopAsync(CancellationToken cancellationToken) => - Task.WhenAny(StateServices.Select(s => s.StopAsync(cancellationToken))); - - public void Dispose() => - Task.Run(() => - { - foreach (StateService stateService in StateServices) - { - stateService.Dispose(); - } - }); -} diff --git a/NineChronicles.Headless/StandaloneContext.cs b/NineChronicles.Headless/StandaloneContext.cs index 8faad9946..6ad387e9a 100644 --- a/NineChronicles.Headless/StandaloneContext.cs +++ b/NineChronicles.Headless/StandaloneContext.cs @@ -18,6 +18,7 @@ public class StandaloneContext private BlockChain? _blockChain; private IKeyStore? _keyStore; private IStore? _store; + private IStateStore? _stateStore; private Swarm? _swarm; public BlockChain BlockChain @@ -64,6 +65,13 @@ public IStore Store internal set => _store = value; } + public IStateStore StateStore + { + get => _stateStore ?? + throw new InvalidOperationException($"{nameof(StateStore)} property is not set yet."); + internal set => _stateStore = value; + } + public Swarm Swarm { get => _swarm ?? diff --git a/hooks/pre-commit b/hooks/pre-commit old mode 100644 new mode 100755 index 05af56bc1..38f266e1f --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -2,4 +2,7 @@ dotnet format \ --exclude Lib9c \ - --exclude NineChronicles.RPC.Shared + --exclude NineChronicles.RPC.Shared \ + -v=d \ + --no-restore \ + --verify-no-changes