From 9b88a8252695da64f41684d8f4c231f2f2e13b48 Mon Sep 17 00:00:00 2001 From: Jiwon Date: Thu, 23 May 2024 10:26:06 +0900 Subject: [PATCH] Implement diff query (#2479) --- .vscode/launch.json | 19 ++++ .vscode/tasks.json | 41 +++++++++ .../GraphTypes/Diff/DiffGraphType.cs | 12 +++ .../GraphTypes/Diff/IDiffType.cs | 6 ++ .../GraphTypes/Diff/RootStateDiffType.cs | 33 +++++++ .../GraphTypes/Diff/StateDiffType.cs | 48 ++++++++++ .../GraphTypes/StandaloneQuery.cs | 90 +++++++++++++++++++ .../NineChroniclesNodeService.cs | 1 + NineChronicles.Headless/StandaloneContext.cs | 8 ++ 9 files changed, 258 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 NineChronicles.Headless/GraphTypes/Diff/DiffGraphType.cs create mode 100644 NineChronicles.Headless/GraphTypes/Diff/IDiffType.cs create mode 100644 NineChronicles.Headless/GraphTypes/Diff/RootStateDiffType.cs create mode 100644 NineChronicles.Headless/GraphTypes/Diff/StateDiffType.cs 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/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 3718a3217..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; @@ -72,6 +75,93 @@ public StandaloneQuery(StandaloneContext standaloneContext, IConfiguration confi } ); + 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( diff --git a/NineChronicles.Headless/NineChroniclesNodeService.cs b/NineChronicles.Headless/NineChroniclesNodeService.cs index e9ca14af7..ceef8bcb4 100644 --- a/NineChronicles.Headless/NineChroniclesNodeService.cs +++ b/NineChronicles.Headless/NineChroniclesNodeService.cs @@ -282,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/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 ??