diff --git a/.github/workflows/deploy_gh_pages.yaml b/.github/workflows/deploy_gh_pages.yaml index 85e36ddc8..2212312e7 100644 --- a/.github/workflows/deploy_gh_pages.yaml +++ b/.github/workflows/deploy_gh_pages.yaml @@ -50,12 +50,20 @@ jobs: -G "https://9c-dx.s3.ap-northeast-2.amazonaws.com/empty-genesis-block-20230511" & sleep 60s graphql-inspector introspect http://localhost:30000/graphql --write schema.graphql - - name: Build + - name: Build GraphQL Document run: | yarn global add spectaql - spectaql ./spectaql-config.yaml + spectaql --target-dir public/graphql ./spectaql-config.yaml + - name: Build CLI Document + run: | + mkdir -p public/cli + dotnet run --project NineChronicles.Headless.Executable -- \ + docs \ + public/cli + - name: Copy Landing Page to deploy + run: cp Docs/resources/landing.html public/index.html - name: Copy GraphQL Schema to deploy - run: cp schema.graphql doc + run: cp schema.graphql public/schema.graphql - name: Deploy uses: peaceiris/actions-gh-pages@v3 with: diff --git a/.github/workflows/push_docker_image.yml b/.github/workflows/push_docker_image.yml index 90f4bfd83..d603c9d2b 100644 --- a/.github/workflows/push_docker_image.yml +++ b/.github/workflows/push_docker_image.yml @@ -29,6 +29,8 @@ jobs: docker: - repo: planetariumhq/ninechronicles-headless dockerfile: Dockerfile + - repo: planetariumhq/access-control-center + dockerfile: Dockerfile.ACC if: github.ref_type == 'branch' runs-on: ubuntu-latest steps: diff --git a/Dockerfile.ACC b/Dockerfile.ACC new file mode 100644 index 000000000..40861ddc3 --- /dev/null +++ b/Dockerfile.ACC @@ -0,0 +1,37 @@ +# Use the SDK image to build the app +FROM mcr.microsoft.com/dotnet/sdk:6.0-jammy AS build-env +WORKDIR /app +ARG COMMIT + +# Copy csproj and restore as distinct layers +COPY ./Lib9c/Lib9c/Lib9c.csproj ./Lib9c/ +COPY ./NineChronicles.Headless/NineChronicles.Headless.csproj ./NineChronicles.Headless/ +COPY ./NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj ./NineChronicles.Headless.AccessControlCenter/ +RUN dotnet restore Lib9c +RUN dotnet restore NineChronicles.Headless +RUN dotnet restore NineChronicles.Headless.AccessControlCenter + +# Copy everything else and build +COPY . ./ +RUN dotnet publish NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj \ + -c Release \ + -r linux-x64 \ + -o out \ + --self-contained \ + --version-suffix $COMMIT + +# Build runtime image +FROM mcr.microsoft.com/dotnet/aspnet:6.0 +WORKDIR /app +RUN apt-get update && apt-get install -y libc6-dev +COPY --from=build-env /app/out . + +# Install native deps & utilities for production +RUN apt-get update \ + && apt-get install -y --allow-unauthenticated \ + libc6-dev jq curl \ + && rm -rf /var/lib/apt/lists/* + +VOLUME /data + +ENTRYPOINT ["dotnet", "NineChronicles.Headless.AccessControlCenter.dll"] diff --git a/Dockerfile.ACC.amd64 b/Dockerfile.ACC.amd64 new file mode 100644 index 000000000..eeb7814d7 --- /dev/null +++ b/Dockerfile.ACC.amd64 @@ -0,0 +1,37 @@ +# Use the SDK image to build the app +FROM mcr.microsoft.com/dotnet/sdk:6.0-jammy AS build-env +WORKDIR /app +ARG COMMIT + +# Copy csproj and restore as distinct layers +COPY ./Lib9c/Lib9c/Lib9c.csproj ./Lib9c/ +COPY ./NineChronicles.Headless/NineChronicles.Headless.csproj ./NineChronicles.Headless/ +COPY ./NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj ./NineChronicles.Headless.AccessControlCenter/ +RUN dotnet restore Lib9c +RUN dotnet restore NineChronicles.Headless +RUN dotnet restore NineChronicles.Headless.AccessControlCenter + +# Copy everything else and build +COPY . ./ +RUN dotnet publish NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj \ + -c Release \ + -r linux-x64 \ + -o out \ + --self-contained \ + --version-suffix $COMMIT + +# Build runtime image +FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim +WORKDIR /app +RUN apt-get update && apt-get install -y libc6-dev +COPY --from=build-env /app/out . + +# Install native deps & utilities for production +RUN apt-get update \ + && apt-get install -y --allow-unauthenticated \ + libc6-dev jq curl \ + && rm -rf /var/lib/apt/lists/* + +VOLUME /data + +ENTRYPOINT ["dotnet", "NineChronicles.Headless.AccessControlCenter.dll"] diff --git a/Dockerfile.ACC.arm64v8 b/Dockerfile.ACC.arm64v8 new file mode 100644 index 000000000..510a04d02 --- /dev/null +++ b/Dockerfile.ACC.arm64v8 @@ -0,0 +1,37 @@ +# Use the SDK image to build the app +FROM mcr.microsoft.com/dotnet/sdk:6.0-jammy AS build-env +WORKDIR /app +ARG COMMIT + +# Copy csproj and restore as distinct layers +COPY ./Lib9c/Lib9c/Lib9c.csproj ./Lib9c/ +COPY ./NineChronicles.Headless/NineChronicles.Headless.csproj ./NineChronicles.Headless/ +COPY ./NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj ./NineChronicles.Headless.AccessControlCenter/ +RUN dotnet restore Lib9c +RUN dotnet restore NineChronicles.Headless +RUN dotnet restore NineChronicles.Headless.AccessControlCenter + +# Copy everything else and build +COPY . ./ +RUN dotnet publish NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj \ + -c Release \ + -r linux-arm64 \ + -o out \ + --self-contained \ + --version-suffix $COMMIT + +# Build runtime image +FROM --platform=linux/arm64 mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim-arm64v8 +WORKDIR /app +RUN apt-get update && apt-get install -y libc6-dev +COPY --from=build-env /app/out . + +# Install native deps & utilities for production +RUN apt-get update \ + && apt-get install -y --allow-unauthenticated \ + libc6-dev jq curl \ + && rm -rf /var/lib/apt/lists/* + +VOLUME /data + +ENTRYPOINT ["dotnet", "NineChronicles.Headless.AccessControlCenter.dll"] diff --git a/Docs/resources/landing.html b/Docs/resources/landing.html new file mode 100644 index 000000000..ebe92854f --- /dev/null +++ b/Docs/resources/landing.html @@ -0,0 +1,25 @@ + + + + NineChronicles Headless + + + + + +
+
+

NineChronicles.Headless

+ A headless node to validate, network, operate NineChronicles chain. +
+ +
+
+ + diff --git a/Lib9c b/Lib9c index 2cd67a880..1969277b9 160000 --- a/Lib9c +++ b/Lib9c @@ -1 +1 @@ -Subproject commit 2cd67a880d316eee55a59390a6c783683f55c2ca +Subproject commit 1969277b9c5dedcd3d6e5cd46a48d5f259f2afca diff --git a/Libplanet.Extensions.ForkableActionEvaluator.Tests/ForkableActionEvaluatorTest.cs b/Libplanet.Extensions.ForkableActionEvaluator.Tests/ForkableActionEvaluatorTest.cs index cd5714f01..3e40b03c0 100644 --- a/Libplanet.Extensions.ForkableActionEvaluator.Tests/ForkableActionEvaluatorTest.cs +++ b/Libplanet.Extensions.ForkableActionEvaluator.Tests/ForkableActionEvaluatorTest.cs @@ -5,11 +5,8 @@ using Libplanet.Types.Blocks; using Libplanet.Common; using Libplanet.Crypto; -using Libplanet.Extensions.ActionEvaluatorCommonComponents; using Libplanet.Types.Tx; -using ActionEvaluation = Libplanet.Extensions.ActionEvaluatorCommonComponents.ActionEvaluation; using ArgumentOutOfRangeException = System.ArgumentOutOfRangeException; -using Random = Libplanet.Extensions.ActionEvaluatorCommonComponents.Random; namespace Libplanet.Extensions.ForkableActionEvaluator.Tests; @@ -24,11 +21,11 @@ public void ForkEvaluation() ((101L, long.MaxValue), new PostActionEvaluator()), }); - Assert.Equal((Text)"PRE", Assert.Single(evaluator.Evaluate(new MockBlock(0))).Action); - Assert.Equal((Text)"PRE", Assert.Single(evaluator.Evaluate(new MockBlock(99))).Action); - Assert.Equal((Text)"PRE", Assert.Single(evaluator.Evaluate(new MockBlock(100))).Action); - Assert.Equal((Text)"POST", Assert.Single(evaluator.Evaluate(new MockBlock(101))).Action); - Assert.Equal((Text)"POST", Assert.Single(evaluator.Evaluate(new MockBlock(long.MaxValue))).Action); + Assert.Equal((Text)"PRE", Assert.Single(evaluator.Evaluate(new MockBlock(0), null)).Action); + Assert.Equal((Text)"PRE", Assert.Single(evaluator.Evaluate(new MockBlock(99), null)).Action); + Assert.Equal((Text)"PRE", Assert.Single(evaluator.Evaluate(new MockBlock(100), null)).Action); + Assert.Equal((Text)"POST", Assert.Single(evaluator.Evaluate(new MockBlock(101), null)).Action); + Assert.Equal((Text)"POST", Assert.Single(evaluator.Evaluate(new MockBlock(long.MaxValue), null)).Action); } [Fact] @@ -64,26 +61,23 @@ public void CheckPairs() class PostActionEvaluator : IActionEvaluator { public IActionLoader ActionLoader => throw new NotSupportedException(); - public IReadOnlyList Evaluate(IPreEvaluationBlock block) + public IReadOnlyList Evaluate(IPreEvaluationBlock block, HashDigest? baseStateroothash) { - return new IActionEvaluation[] + return new ICommittedActionEvaluation[] { - new ActionEvaluation( + new CommittedActionEvaluation( (Text)"POST", - new ActionContext( - null, + new CommittedActionContext( default, null, default, 0, 0, false, - new AccountStateDelta(), - new Random(0), - null, + default, + 0, false), - new AccountStateDelta(), - null) + default) }; } } @@ -91,26 +85,23 @@ public IReadOnlyList Evaluate(IPreEvaluationBlock block) class PreActionEvaluator : IActionEvaluator { public IActionLoader ActionLoader => throw new NotSupportedException(); - public IReadOnlyList Evaluate(IPreEvaluationBlock block) + public IReadOnlyList Evaluate(IPreEvaluationBlock block, HashDigest? baseStateRootHash) { - return new IActionEvaluation[] + return new ICommittedActionEvaluation[] { - new ActionEvaluation( + new CommittedActionEvaluation( (Text)"PRE", - new ActionContext( - null, + new CommittedActionContext( default, null, default, 0, 0, false, - new AccountStateDelta(), - new Random(0), - null, + default, + 0, false), - new AccountStateDelta(), - null) + default) }; } } diff --git a/Libplanet.Extensions.ForkableActionEvaluator/ForkableActionEvaluator.cs b/Libplanet.Extensions.ForkableActionEvaluator/ForkableActionEvaluator.cs index 13c60a5ea..651794876 100644 --- a/Libplanet.Extensions.ForkableActionEvaluator/ForkableActionEvaluator.cs +++ b/Libplanet.Extensions.ForkableActionEvaluator/ForkableActionEvaluator.cs @@ -1,5 +1,7 @@ +using System.Security.Cryptography; using Libplanet.Action; using Libplanet.Action.Loader; +using Libplanet.Common; using Libplanet.Types.Blocks; namespace Libplanet.Extensions.ForkableActionEvaluator; @@ -15,9 +17,10 @@ public ForkableActionEvaluator(IEnumerable<((long StartIndex, long EndIndex) Ran public IActionLoader ActionLoader => throw new NotSupportedException(); - public IReadOnlyList Evaluate(IPreEvaluationBlock block) + public IReadOnlyList Evaluate( + IPreEvaluationBlock block, HashDigest? baseStateRootHash) { var actionEvaluator = _router.GetEvaluator(block.Index); - return actionEvaluator.Evaluate(block); + return actionEvaluator.Evaluate(block, baseStateRootHash); } } diff --git a/Libplanet.Headless.Tests/Hosting/LibplanetNodeServiceTest.cs b/Libplanet.Headless.Tests/Hosting/LibplanetNodeServiceTest.cs index 938e5e305..7ccd5cd94 100644 --- a/Libplanet.Headless.Tests/Hosting/LibplanetNodeServiceTest.cs +++ b/Libplanet.Headless.Tests/Hosting/LibplanetNodeServiceTest.cs @@ -25,13 +25,14 @@ public void Constructor() { var policy = new BlockPolicy(); var stagePolicy = new VolatileStagePolicy(); + var stateStore = new TrieStateStore(new MemoryKeyValueStore()); var blockChainStates = new BlockChainStates( new MemoryStore(), - new TrieStateStore(new MemoryKeyValueStore())); + stateStore); var actionLoader = new SingleActionLoader(typeof(DummyAction)); var actionEvaluator = new ActionEvaluator( _ => policy.BlockAction, - blockChainStates, + stateStore, actionLoader); var genesisBlock = BlockChain.ProposeGenesisBlock(actionEvaluator); var service = new LibplanetNodeService( diff --git a/Libplanet.Headless/Hosting/LibplanetNodeService.cs b/Libplanet.Headless/Hosting/LibplanetNodeService.cs index 76ee633c6..cc5a3d55a 100644 --- a/Libplanet.Headless/Hosting/LibplanetNodeService.cs +++ b/Libplanet.Headless/Hosting/LibplanetNodeService.cs @@ -98,8 +98,7 @@ public LibplanetNodeService( (Store, StateStore) = LoadStore( Properties.StorePath, Properties.StoreType, - Properties.StoreStatesCacheSize, - Properties.NoReduceStore); + Properties.StoreStatesCacheSize); var chainIds = Store.ListChainIds().ToList(); Log.Debug($"Number of chain ids: {chainIds.Count()}"); @@ -122,10 +121,10 @@ IActionEvaluator BuildActionEvaluator(IActionEvaluatorConfiguration actionEvalua return actionEvaluatorConfiguration switch { RemoteActionEvaluatorConfiguration remoteActionEvaluatorConfiguration => new RemoteActionEvaluator( - new Uri(remoteActionEvaluatorConfiguration.StateServiceEndpoint), blockChainStates), + new Uri(remoteActionEvaluatorConfiguration.StateServiceEndpoint)), DefaultActionEvaluatorConfiguration _ => new ActionEvaluator( _ => blockPolicy.BlockAction, - blockChainStates: blockChainStates, + stateStore: StateStore, actionTypeLoader: actionLoader ), ForkableActionEvaluatorConfiguration forkableActionEvaluatorConfiguration => new @@ -303,7 +302,7 @@ public override async Task StopAsync(CancellationToken cancellationToken) } } - protected (IStore, IStateStore) LoadStore(string path, string type, int statesCacheSize, bool noReduceStore = false) + protected (IStore, IStateStore) LoadStore(string path, string type, int statesCacheSize) { IStore store = null; if (type == "rocksdb") @@ -344,10 +343,6 @@ public override async Task StopAsync(CancellationToken cancellationToken) } store ??= new DefaultStore(path, flush: false); - if (!noReduceStore) - { - store = new ReducedStore(store); - } IKeyValueStore stateKeyValueStore = new RocksDBKeyValueStore(Path.Combine(path, "states")); IStateStore stateStore = new TrieStateStore(stateKeyValueStore); diff --git a/Libplanet.Headless/ReducedStore.cs b/Libplanet.Headless/ReducedStore.cs deleted file mode 100644 index 389597d0b..000000000 --- a/Libplanet.Headless/ReducedStore.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using Bencodex.Types; -using Libplanet.Crypto; -using Libplanet.Store; -using Libplanet.Types.Blocks; -using Libplanet.Types.Tx; - -namespace Libplanet.Headless -{ - /// - /// A decorator that reduce space consumption by omitting input calls which - /// are unused by Nine Chronicles. - /// Calls on this will be forwarded to its , except for: - /// - /// - /// - /// - public sealed class ReducedStore : IStore - { - public ReducedStore(IStore internalStore) - { - InternalStore = internalStore; - } - - public IStore InternalStore { get; } - - public long AppendIndex(Guid chainId, BlockHash hash) => - InternalStore.AppendIndex(chainId, hash); - - public bool ContainsBlock(BlockHash blockHash) => - InternalStore.ContainsBlock(blockHash); - - public bool ContainsTransaction(TxId txId) => - InternalStore.ContainsTransaction(txId); - - public long CountBlocks() => - InternalStore.CountBlocks(); - - public long CountIndex(Guid chainId) => - InternalStore.CountIndex(chainId); - - public bool DeleteBlock(BlockHash blockHash) => - InternalStore.DeleteBlock(blockHash); - - public void DeleteChainId(Guid chainId) => - InternalStore.DeleteChainId(chainId); - - public void ForkBlockIndexes( - Guid sourceChainId, - Guid destinationChainId, - BlockHash branchpoint - ) => - InternalStore.ForkBlockIndexes(sourceChainId, destinationChainId, branchpoint); - - public void ForkTxNonces(Guid sourceChainId, Guid destinationChainId) => - InternalStore.ForkTxNonces(sourceChainId, destinationChainId); - - public Block GetBlock(BlockHash blockHash) - => InternalStore.GetBlock(blockHash); - - public BlockDigest? GetBlockDigest(BlockHash blockHash) => - InternalStore.GetBlockDigest(blockHash); - - public long? GetBlockIndex(BlockHash blockHash) => - InternalStore.GetBlockIndex(blockHash); - - public Guid? GetCanonicalChainId() => - InternalStore.GetCanonicalChainId(); - - public Transaction GetTransaction(TxId txid) => - InternalStore.GetTransaction(txid); - - public TxExecution GetTxExecution(BlockHash blockHash, TxId txid) => - InternalStore.GetTxExecution(blockHash, txid); - - public long GetTxNonce(Guid chainId, Address address) => - InternalStore.GetTxNonce(chainId, address); - - public void IncreaseTxNonce(Guid chainId, Address signer, long delta = 1) => - InternalStore.IncreaseTxNonce(chainId, signer, delta); - - public BlockHash? IndexBlockHash(Guid chainId, long index) => - InternalStore.IndexBlockHash(chainId, index); - - public IEnumerable IterateBlockHashes() => - InternalStore.IterateBlockHashes(); - - public IEnumerable IterateIndexes( - Guid chainId, - int offset = 0, - int? limit = null - ) => - InternalStore.IterateIndexes(chainId, offset, limit); - - public IEnumerable ListChainIds() => - InternalStore.ListChainIds(); - - public IEnumerable> ListTxNonces(Guid chainId) => - InternalStore.ListTxNonces(chainId); - - public void PutBlock(Block block) => - InternalStore.PutBlock(block); - - public void PutTransaction(Transaction tx) => - InternalStore.PutTransaction(tx); - - public void PutTxExecution(TxSuccess txSuccess) - { - // Omit TxSuccess.UpdatedStates as it is unused by Nine Chronicles and too big. - TxSuccess reducedTxSuccess = new TxSuccess( - txSuccess.BlockHash, - txSuccess.TxId, - updatedStates: txSuccess.UpdatedStates.ToImmutableDictionary(pair => pair.Key, _ => (IValue)Null.Value), - updatedFungibleAssets: txSuccess.UpdatedFungibleAssets - ); - InternalStore.PutTxExecution(reducedTxSuccess); - } - - public void PutTxExecution(TxFailure txFailure) => - InternalStore.PutTxExecution(txFailure); - - public void SetCanonicalChainId(Guid chainId) => - InternalStore.SetCanonicalChainId(chainId); - - public void PutTxIdBlockHashIndex(TxId txId, BlockHash blockHash) => - InternalStore.PutTxIdBlockHashIndex(txId, blockHash); - - public BlockHash? GetFirstTxIdBlockHashIndex(TxId txId) => - InternalStore.GetFirstTxIdBlockHashIndex(txId); - - public IEnumerable IterateTxIdBlockHashIndex(TxId txId) => - InternalStore.IterateTxIdBlockHashIndex(txId); - - public void DeleteTxIdBlockHashIndex(TxId txId, BlockHash blockHash) => - InternalStore.DeleteTxIdBlockHashIndex(txId, blockHash); - - public void PruneOutdatedChains(bool noopWithoutCanon = false) => - InternalStore.PruneOutdatedChains(noopWithoutCanon); - - public BlockCommit GetChainBlockCommit(Guid chainId) => - InternalStore.GetChainBlockCommit(chainId); - - public void PutChainBlockCommit(Guid chainId, BlockCommit blockCommit) => - InternalStore.PutChainBlockCommit(chainId, blockCommit); - - public BlockCommit GetBlockCommit(BlockHash blockHash) => - InternalStore.GetBlockCommit(blockHash); - - public void PutBlockCommit(BlockCommit blockCommit) => - InternalStore.PutBlockCommit(blockCommit); - - public void DeleteBlockCommit(BlockHash blockHash) => - InternalStore.DeleteBlockCommit(blockHash); - - public IEnumerable GetBlockCommitHashes() => - InternalStore.GetBlockCommitHashes(); - - public void Dispose() => InternalStore.Dispose(); - } -} diff --git a/NineChronicles.Headless.AccessControlCenter/AccService.cs b/NineChronicles.Headless.AccessControlCenter/AccService.cs new file mode 100644 index 000000000..167beac3b --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/AccService.cs @@ -0,0 +1,108 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.OpenApi.Models; +using NineChronicles.Headless.AccessControlCenter.AccessControlService; + +namespace NineChronicles.Headless.AccessControlCenter +{ + public class AccService + { + public AccService(Configuration configuration) + { + Configuration = configuration; + } + + public Configuration Configuration { get; } + + public IHostBuilder Configure(IHostBuilder hostBuilder, int port) + { + return hostBuilder.ConfigureWebHostDefaults(builder => + { + builder.UseStartup(x => new RestApiStartup(Configuration)); + builder.ConfigureKestrel(options => + { + options.ListenAnyIP( + port, + listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http1AndHttp2; + } + ); + }); + }); + } + + internal class RestApiStartup + { + public RestApiStartup(Configuration configuration) + { + Configuration = configuration; + } + + public Configuration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + + services.AddSwaggerGen(c => + { + c.SwaggerDoc( + "v1", + new OpenApiInfo { Title = "Access Control Center API", Version = "v1" } + ); + c.DocInclusionPredicate( + (docName, apiDesc) => + { + var controllerType = + apiDesc.ActionDescriptor + as Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor; + if (controllerType != null) + { + var assemblyName = controllerType.ControllerTypeInfo.Assembly + .GetName() + .Name; + var namespaceName = controllerType.ControllerTypeInfo.Namespace; + return namespaceName?.StartsWith( + "NineChronicles.Headless.AccessControlCenter" + ) ?? false; + } + return false; + } + ); + }); + + var accessControlService = MutableAccessControlServiceFactory.Create( + Enum.Parse( + Configuration.AccessControlServiceType, + true + ), + Configuration.AccessControlServiceConnectionString + ); + + services.AddSingleton(accessControlService); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "Access Control Center API V1"); + }); + + app.UseRouting(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } + } +} diff --git a/NineChronicles.Headless.AccessControlCenter/AccessControlService/IMutableAccessControlService.cs b/NineChronicles.Headless.AccessControlCenter/AccessControlService/IMutableAccessControlService.cs new file mode 100644 index 000000000..b5d52f697 --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/AccessControlService/IMutableAccessControlService.cs @@ -0,0 +1,13 @@ +using Libplanet.Crypto; +using System.Collections.Generic; +using Nekoyume.Blockchain; + +namespace NineChronicles.Headless.AccessControlCenter.AccessControlService +{ + public interface IMutableAccessControlService : IAccessControlService + { + void AddTxQuota(Address address, int quota); + void RemoveTxQuota(Address address); + List
ListTxQuotaAddresses(int offset, int limit); + } +} diff --git a/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableAccessControlServiceFactory.cs b/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableAccessControlServiceFactory.cs new file mode 100644 index 000000000..f9f98ac68 --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableAccessControlServiceFactory.cs @@ -0,0 +1,33 @@ +using System; + +namespace NineChronicles.Headless.AccessControlCenter.AccessControlService +{ + public static class MutableAccessControlServiceFactory + { + public enum StorageType + { + /// + /// Use Redis + /// + Redis, + + /// + /// Use SQLite + /// + SQLite + } + + public static IMutableAccessControlService Create( + StorageType storageType, + string connectionString + ) + { + return storageType switch + { + StorageType.Redis => new MutableRedisAccessControlService(connectionString), + StorageType.SQLite => new MutableSqliteAccessControlService(connectionString), + _ => throw new ArgumentOutOfRangeException(nameof(storageType), storageType, null) + }; + } + } +} diff --git a/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableRedisAccessControlService.cs b/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableRedisAccessControlService.cs new file mode 100644 index 000000000..9cb412d88 --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableRedisAccessControlService.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Linq; +using Libplanet.Crypto; +using NineChronicles.Headless.Services; +using StackExchange.Redis; + +namespace NineChronicles.Headless.AccessControlCenter.AccessControlService +{ + public class MutableRedisAccessControlService + : RedisAccessControlService, + IMutableAccessControlService + { + public MutableRedisAccessControlService(string storageUri) + : base(storageUri) + { + } + + public void AddTxQuota(Address address, int quota) + { + _db.StringSet(address.ToString(), quota.ToString()); + } + + public void RemoveTxQuota(Address address) + { + _db.KeyDelete(address.ToString()); + } + + public List
ListTxQuotaAddresses(int offset, int limit) + { + var server = _db.Multiplexer.GetServer(_db.Multiplexer.GetEndPoints().First()); + + var result = (RedisResult[]?) + server.Execute("SCAN", offset.ToString(), "COUNT", limit.ToString()); + if (result != null) + { + RedisKey[] keys = (RedisKey[])result[1]!; + return keys.Select(k => new Address(k.ToString())).ToList(); + } + + return new List
(); + } + } +} diff --git a/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableSqliteAccessControlService.cs b/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableSqliteAccessControlService.cs new file mode 100644 index 000000000..daa2a9c37 --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableSqliteAccessControlService.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using Microsoft.Data.Sqlite; +using Libplanet.Crypto; +using NineChronicles.Headless.Services; + +namespace NineChronicles.Headless.AccessControlCenter.AccessControlService +{ + public class MutableSqliteAccessControlService : SQLiteAccessControlService, IMutableAccessControlService + { + private const string AddTxQuotaSql = + "INSERT OR IGNORE INTO txquotalist (address, quota) VALUES (@Address, @Quota)"; + private const string RemoveTxQuotaSql = "DELETE FROM txquotalist WHERE address=@Address"; + + public MutableSqliteAccessControlService(string connectionString) : base(connectionString) + { + } + + public void AddTxQuota(Address address, int quota) + { + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + + using var command = connection.CreateCommand(); + command.CommandText = AddTxQuotaSql; + command.Parameters.AddWithValue("@Address", address.ToString()); + command.Parameters.AddWithValue("@Quota", quota); + command.ExecuteNonQuery(); + } + + public void RemoveTxQuota(Address address) + { + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + + using var command = connection.CreateCommand(); + command.CommandText = RemoveTxQuotaSql; + command.Parameters.AddWithValue("@Address", address.ToString()); + command.ExecuteNonQuery(); + } + + public List
ListTxQuotaAddresses(int offset, int limit) + { + var txQuotaAddresses = new List
(); + + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + + using var command = connection.CreateCommand(); + command.CommandText = $"SELECT address FROM txquotalist LIMIT @Limit OFFSET @Offset"; + command.Parameters.AddWithValue("@Limit", limit); + command.Parameters.AddWithValue("@Offset", offset); + + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + txQuotaAddresses.Add(new Address(reader.GetString(0))); + } + + return txQuotaAddresses; + } + } +} diff --git a/NineChronicles.Headless.AccessControlCenter/Configuration.cs b/NineChronicles.Headless.AccessControlCenter/Configuration.cs new file mode 100644 index 000000000..22ca923df --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/Configuration.cs @@ -0,0 +1,11 @@ +namespace NineChronicles.Headless.AccessControlCenter +{ + public class Configuration + { + public int Port { get; set; } + + public string AccessControlServiceType { get; set; } = null!; + + public string AccessControlServiceConnectionString { get; set; } = null!; + } +} diff --git a/NineChronicles.Headless.AccessControlCenter/Controllers/AccessControlServiceController.cs b/NineChronicles.Headless.AccessControlCenter/Controllers/AccessControlServiceController.cs new file mode 100644 index 000000000..40117f900 --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/Controllers/AccessControlServiceController.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using NineChronicles.Headless.AccessControlCenter.AccessControlService; +using System.Linq; +using Libplanet.Crypto; + +namespace NineChronicles.Headless.AccessControlCenter.Controllers +{ + [ApiController] + public class AccessControlServiceController : ControllerBase + { + private readonly IMutableAccessControlService _accessControlService; + + public AccessControlServiceController(IMutableAccessControlService accessControlService) + { + _accessControlService = accessControlService; + } + + [HttpGet("entries/{address}")] + public ActionResult GetTxQuota(string address) + { + return _accessControlService.GetTxQuota(new Address(address)); + } + + [HttpPost("entries/add-tx-quota/{address}/{quota:int}")] + public ActionResult AddTxQuota(string address, int quota) + { + var maxQuota = 10; + if (quota > maxQuota) + { + return BadRequest($"The quota cannot exceed {maxQuota}."); + } + + _accessControlService.AddTxQuota(new Address(address), quota); + return Ok(); + } + + [HttpPost("entries/remove-tx-quota/{address}")] + public ActionResult RemoveTxQuota(string address) + { + _accessControlService.RemoveTxQuota(new Address(address)); + return Ok(); + } + + [HttpGet("entries")] + public ActionResult> ListBlockedAddresses(int offset, int limit) + { + var maxLimit = 10; + if (_accessControlService is MutableRedisAccessControlService) + { + maxLimit = 10; + } + else if (_accessControlService is MutableSqliteAccessControlService) + { + maxLimit = 100; + } + if (limit > maxLimit) + { + return BadRequest($"The limit cannot exceed {maxLimit}."); + } + + return _accessControlService + .ListTxQuotaAddresses(offset, limit) + .Select(a => a.ToString()) + .ToList(); + } + } +} diff --git a/NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj b/NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj new file mode 100644 index 000000000..1ca96b941 --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj @@ -0,0 +1,33 @@ + + + net6 + true + ..\NineChronicles.Headless.Common.ruleset + enable + Debug;Release;DevEx + AnyCPU + true + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + diff --git a/NineChronicles.Headless.AccessControlCenter/Program.cs b/NineChronicles.Headless.AccessControlCenter/Program.cs new file mode 100644 index 000000000..c506b85dc --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/Program.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace NineChronicles.Headless.AccessControlCenter +{ + public static class Program + { + public static void Main(string[] args) + { + // Get configuration + string configPath = + Environment.GetEnvironmentVariable("ACC_CONFIG_FILE") ?? "appsettings.json"; + + var configurationBuilder = new ConfigurationBuilder() + .AddJsonFile(configPath) + .AddEnvironmentVariables("ACC_"); + IConfiguration config = configurationBuilder.Build(); + + var acsConfig = new Configuration(); + config.Bind(acsConfig); + + var service = new AccService(acsConfig); + var hostBuilder = service.Configure(Host.CreateDefaultBuilder(), acsConfig.Port); + var host = hostBuilder.Build(); + host.Run(); + } + } +} diff --git a/NineChronicles.Headless.AccessControlCenter/appsettings.json b/NineChronicles.Headless.AccessControlCenter/appsettings.json new file mode 100644 index 000000000..fbf1f5da2 --- /dev/null +++ b/NineChronicles.Headless.AccessControlCenter/appsettings.json @@ -0,0 +1,5 @@ +{ + "Port": "31259", + "AccessControlServiceType": "redis", + "AccessControlServiceConnectionString": "localhost:6379" +} diff --git a/NineChronicles.Headless.Executable.Tests/Commands/AccountCommandTest.cs b/NineChronicles.Headless.Executable.Tests/Commands/AccountCommandTest.cs index 08d7a2759..a190c5cdc 100644 --- a/NineChronicles.Headless.Executable.Tests/Commands/AccountCommandTest.cs +++ b/NineChronicles.Headless.Executable.Tests/Commands/AccountCommandTest.cs @@ -49,7 +49,7 @@ public void Balance(StoreType storeType) IBlockPolicy blockPolicy = new BlockPolicySource().GetPolicy(); ActionEvaluator actionEvaluator = new ActionEvaluator( _ => blockPolicy.BlockAction, - new BlockChainStates(store, stateStore), + stateStore, new NCActionLoader()); BlockChain chain = BlockChain.Create( blockPolicy, diff --git a/NineChronicles.Headless.Executable.Tests/Commands/ChainCommandTest.cs b/NineChronicles.Headless.Executable.Tests/Commands/ChainCommandTest.cs index dc21068eb..a14f5c529 100644 --- a/NineChronicles.Headless.Executable.Tests/Commands/ChainCommandTest.cs +++ b/NineChronicles.Headless.Executable.Tests/Commands/ChainCommandTest.cs @@ -59,7 +59,7 @@ public void Tip(StoreType storeType) { var actionEvaluator = new ActionEvaluator( _ => new BlockPolicy().BlockAction, - new BlockChainStates(new MemoryStore(), new TrieStateStore(new MemoryKeyValueStore())), + new TrieStateStore(new MemoryKeyValueStore()), new NCActionLoader()); Block genesisBlock = BlockChain.ProposeGenesisBlock(actionEvaluator); IStore store = storeType.CreateStore(_storePath); @@ -93,7 +93,7 @@ public void Inspect(StoreType storeType) IBlockPolicy blockPolicy = new BlockPolicySource().GetTestPolicy(); ActionEvaluator actionEvaluator = new ActionEvaluator( _ => blockPolicy.BlockAction, - new BlockChainStates(store, stateStore), + stateStore, new NCActionLoader()); Block genesisBlock = BlockChain.ProposeGenesisBlock( actionEvaluator, @@ -154,7 +154,7 @@ public void Truncate(StoreType storeType) IBlockPolicy blockPolicy = new BlockPolicySource().GetTestPolicy(); ActionEvaluator actionEvaluator = new ActionEvaluator( _ => blockPolicy.BlockAction, - new BlockChainStates(store, stateStore), + stateStore, new NCActionLoader()); Block genesisBlock = BlockChain.ProposeGenesisBlock( actionEvaluator, @@ -233,7 +233,7 @@ public void PruneState(StoreType storeType) IBlockPolicy blockPolicy = new BlockPolicySource().GetPolicy(); ActionEvaluator actionEvaluator = new ActionEvaluator( _ => blockPolicy.BlockAction, - new BlockChainStates(store, stateStore), + stateStore, new NCActionLoader()); BlockChain chain = BlockChain.Create( blockPolicy, @@ -242,7 +242,16 @@ public void PruneState(StoreType storeType) stateStore, genesisBlock, actionEvaluator); + + // Additional pruning is now required since in-between commits are made + store.Dispose(); + stateStore.Dispose(); + _command.PruneStates(storeType, _storePath); + store = storeType.CreateStore(_storePath); + stateKeyValueStore = new RocksDBKeyValueStore(statesPath); + stateStore = new TrieStateStore(stateKeyValueStore); int prevStatesCount = stateKeyValueStore.ListKeys().Count(); + stateKeyValueStore.Set( new KeyBytes("alpha"), ByteUtil.ParseHex("00")); @@ -250,6 +259,7 @@ public void PruneState(StoreType storeType) new KeyBytes("beta"), ByteUtil.ParseHex("00")); Assert.Equal(prevStatesCount + 2, stateKeyValueStore.ListKeys().Count()); + store.Dispose(); stateStore.Dispose(); _command.PruneStates(storeType, _storePath); @@ -275,7 +285,7 @@ public void Snapshot(StoreType storeType) IBlockPolicy blockPolicy = new BlockPolicySource().GetPolicy(); ActionEvaluator actionEvaluator = new ActionEvaluator( _ => blockPolicy.BlockAction, - new BlockChainStates(store, stateStore), + stateStore, new NCActionLoader()); BlockChain chain = BlockChain.Create( blockPolicy, diff --git a/NineChronicles.Headless.Executable.Tests/Store/StoreExtensionsTest.cs b/NineChronicles.Headless.Executable.Tests/Store/StoreExtensionsTest.cs index 4626eab45..d5d77b991 100644 --- a/NineChronicles.Headless.Executable.Tests/Store/StoreExtensionsTest.cs +++ b/NineChronicles.Headless.Executable.Tests/Store/StoreExtensionsTest.cs @@ -30,7 +30,7 @@ public void GetGenesisBlock(StoreType storeType) IStore store = storeType.CreateStore(_storePath); IActionEvaluator actionEvaluator = new ActionEvaluator( _ => new BlockPolicy().BlockAction, - new BlockChainStates(new MemoryStore(), new TrieStateStore(new MemoryKeyValueStore())), + new TrieStateStore(new MemoryKeyValueStore()), new NCActionLoader()); Block genesisBlock = BlockChain.ProposeGenesisBlock(actionEvaluator); Guid chainId = Guid.NewGuid(); diff --git a/NineChronicles.Headless.Executable.sln b/NineChronicles.Headless.Executable.sln index 03b81380f..b14931789 100644 --- a/NineChronicles.Headless.Executable.sln +++ b/NineChronicles.Headless.Executable.sln @@ -78,6 +78,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Extensions.Remote EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Extensions.RemoteBlockChainStates", "Lib9c\.Libplanet.Extensions.RemoteBlockChainStates\Libplanet.Extensions.RemoteBlockChainStates.csproj", "{8F9E5505-C157-4DF3-A419-FF0108731397}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NineChronicles.Headless.AccessControlCenter", "NineChronicles.Headless.AccessControlCenter\NineChronicles.Headless.AccessControlCenter.csproj", "{162C0F4B-A1D9-4132-BC34-31F1247BC26B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -703,6 +705,24 @@ Global {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 + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Debug|x64.Build.0 = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Debug|x86.ActiveCfg = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Debug|x86.Build.0 = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.DevEx|Any CPU.ActiveCfg = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.DevEx|Any CPU.Build.0 = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.DevEx|x64.ActiveCfg = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.DevEx|x64.Build.0 = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.DevEx|x86.ActiveCfg = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.DevEx|x86.Build.0 = Debug|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Release|Any CPU.Build.0 = Release|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Release|x64.ActiveCfg = Release|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Release|x64.Build.0 = Release|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Release|x86.ActiveCfg = Release|Any CPU + {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/NineChronicles.Headless.Executable/Commands/ChainCommand.cs b/NineChronicles.Headless.Executable/Commands/ChainCommand.cs index 22ebe8425..11184493a 100644 --- a/NineChronicles.Headless.Executable/Commands/ChainCommand.cs +++ b/NineChronicles.Headless.Executable/Commands/ChainCommand.cs @@ -131,7 +131,7 @@ public void Inspect( var blockChainStates = new BlockChainStates(store, stateStore); var actionEvaluator = new ActionEvaluator( _ => blockPolicy.BlockAction, - blockChainStates, + stateStore, new NCActionLoader()); BlockChain chain = new BlockChain( blockPolicy, @@ -284,7 +284,7 @@ public void Truncate( } snapshotTipHash = hash; - } while (!stateStore.ContainsStateRoot(store.GetBlock(snapshotTipHash).StateRootHash)); + } while (!stateStore.GetStateRoot(store.GetBlock(snapshotTipHash).StateRootHash).Recorded); var forkedId = Guid.NewGuid(); @@ -484,7 +484,7 @@ public void Snapshot( } snapshotTipHash = hash; - } while (!stateStore.ContainsStateRoot(store.GetBlock(snapshotTipHash).StateRootHash)); + } while (!stateStore.GetStateRoot(store.GetBlock(snapshotTipHash).StateRootHash).Recorded); var forkedId = Guid.NewGuid(); diff --git a/NineChronicles.Headless.Executable/Commands/MarketCommand.cs b/NineChronicles.Headless.Executable/Commands/MarketCommand.cs index a7ea80af1..8aaa5f649 100644 --- a/NineChronicles.Headless.Executable/Commands/MarketCommand.cs +++ b/NineChronicles.Headless.Executable/Commands/MarketCommand.cs @@ -108,8 +108,8 @@ public void Query( IEnumerable<(Transaction, ActionBase)> actions = block.Transactions .Reverse() .Where(tx => includeFails || - !(chain.GetTxExecution(block.Hash, tx.Id) is { } e) || - e is TxSuccess) + !(chain.GetTxExecution(block.Hash, tx.Id) is { } e) || + !e.Fail) .SelectMany(tx => tx.Actions is { } ca ? ca.Reverse().Select(a => (tx, ToAction(a))) : Enumerable.Empty<(Transaction, ActionBase)>()); diff --git a/NineChronicles.Headless.Executable/Commands/ReplayCommand.Privates.cs b/NineChronicles.Headless.Executable/Commands/ReplayCommand.Privates.cs index 9581815f3..861b678c0 100644 --- a/NineChronicles.Headless.Executable/Commands/ReplayCommand.Privates.cs +++ b/NineChronicles.Headless.Executable/Commands/ReplayCommand.Privates.cs @@ -29,8 +29,6 @@ public partial class ReplayCommand : CoconaLiteConsoleAppBase /// private sealed class ActionContext : IActionContext { - private readonly int _randomSeed; - public ActionContext( Address signer, TxId? txid, @@ -48,8 +46,7 @@ public ActionContext( BlockProtocolVersion = blockProtocolVersion; Rehearsal = rehearsal; PreviousState = previousState; - Random = new Random(randomSeed); - _randomSeed = randomSeed; + RandomSeed = randomSeed; } public Address Signer { get; } @@ -66,33 +63,19 @@ public ActionContext( public IAccount PreviousState { get; } - public IRandom Random { get; } + public int RandomSeed { get; } public bool BlockAction => TxId is null; - public void PutLog(string log) - { - // NOTE: Not implemented yet. See also Lib9c.Tests.Action.ActionContext.PutLog(). - } - public void UseGas(long gas) { } - public IActionContext GetUnconsumedContext() => - new ActionContext( - Signer, - TxId, - Miner, - BlockIndex, - BlockProtocolVersion, - PreviousState, - _randomSeed, - Rehearsal); - public long GasUsed() => 0; public long GasLimit() => 0; + + public IRandom GetRandom() => new Random(RandomSeed); } private sealed class Random : System.Random, IRandom @@ -145,8 +128,14 @@ public ValidatorSet GetValidatorSet(BlockHash? offset) public IAccountState GetAccountState(BlockHash? offset) { - return new LocalCacheAccountState(_rocksDb, _source.GetAccountState, offset); + return new LocalCacheAccountState( + _rocksDb, + _source.GetAccountState, + offset); } + + public IAccountState GetAccountState(HashDigest? hash) + => _source.GetAccountState(hash); } private sealed class LocalCacheAccountState : IAccountState @@ -158,11 +147,11 @@ private sealed class LocalCacheAccountState : IAccountState public LocalCacheAccountState( RocksDb rocksDb, - Func sourceAccountStateGetter, + Func sourceAccountStateGetterWithBlockHash, BlockHash? offset) { _rocksDb = rocksDb; - _sourceAccountStateGetter = sourceAccountStateGetter; + _sourceAccountStateGetter = sourceAccountStateGetterWithBlockHash; _offset = offset; } diff --git a/NineChronicles.Headless.Executable/Commands/ReplayCommand.cs b/NineChronicles.Headless.Executable/Commands/ReplayCommand.cs index 9fb831a5a..9467301f8 100644 --- a/NineChronicles.Headless.Executable/Commands/ReplayCommand.cs +++ b/NineChronicles.Headless.Executable/Commands/ReplayCommand.cs @@ -271,7 +271,7 @@ public int Blocks( try { var rootHash = blockChain.DetermineBlockStateRootHash(block, - out IReadOnlyList actionEvaluations); + out IReadOnlyList actionEvaluations); if (verbose) { @@ -300,9 +300,10 @@ public int Blocks( _console.Out.WriteLine(msg); outputSw?.WriteLine(msg); - var actionEvaluator = GetActionEvaluator(blockChain); - var actionEvaluations = actionEvaluator.Evaluate(block); - LoggingActionEvaluations(actionEvaluations, outputSw); + var actionEvaluator = GetActionEvaluator(stateStore); + var actionEvaluations = blockChain.DetermineBlockStateRootHash(block, + out IReadOnlyList failedActionEvaluations); + LoggingActionEvaluations(failedActionEvaluations, outputSw); msg = $"- block #{block.Index} evaluating failed with "; _console.Out.Write(msg); @@ -482,7 +483,7 @@ private static (FileStream? fs, StreamWriter? sw) GetOutputFileStream( var blockChainStates = new BlockChainStates(store, stateStore); var actionEvaluator = new ActionEvaluator( _ => policy.BlockAction, - blockChainStates, + stateStore, new NCActionLoader()); return ( store, @@ -523,13 +524,13 @@ private Transaction LoadTx(string txPath) return TxMarshaler.UnmarshalTransaction(txDict); } - private ActionEvaluator GetActionEvaluator(BlockChain blockChain) + private ActionEvaluator GetActionEvaluator(IStateStore stateStore) { var policy = new BlockPolicySource().GetPolicy(); IActionLoader actionLoader = new NCActionLoader(); return new ActionEvaluator( _ => policy.BlockAction, - blockChainStates: blockChain, + stateStore: stateStore, actionTypeLoader: actionLoader); } @@ -558,7 +559,7 @@ private void LoggingAboutIncompleteBlockStatesException( } private void LoggingActionEvaluations( - IReadOnlyList actionEvaluations, + IReadOnlyList actionEvaluations, TextWriter? textWriter) { var count = actionEvaluations.Count; diff --git a/NineChronicles.Headless.Executable/Commands/StateCommand.cs b/NineChronicles.Headless.Executable/Commands/StateCommand.cs index 235e7bbd8..b88506bb6 100644 --- a/NineChronicles.Headless.Executable/Commands/StateCommand.cs +++ b/NineChronicles.Headless.Executable/Commands/StateCommand.cs @@ -104,12 +104,13 @@ IStateStore stateStore (int)(top.Index - bottom.Index + 1L) ); + var sStore = new TrieStateStore(new Libplanet.Store.Trie.MemoryKeyValueStore()); var blockChainStates = new BlockChainStates( new MemoryStore(), - new TrieStateStore(new Libplanet.Store.Trie.MemoryKeyValueStore())); + sStore); var actionEvaluator = new ActionEvaluator( _ => policy.BlockAction, - blockChainStates, + sStore, new NCActionLoader()); foreach (BlockHash blockHash in blockHashes) @@ -132,31 +133,20 @@ IStateStore stateStore block.Index, block.Hash ); - IReadOnlyList delta; HashDigest stateRootHash = block.Index < 1 ? BlockChain.DetermineGenesisStateRootHash( actionEvaluator, preEvalBlock, - out delta) + out _) : chain.DetermineBlockStateRootHash( preEvalBlock, - out delta); + out _); DateTimeOffset now = DateTimeOffset.Now; if (invalidStateRootHashBlock is null && !stateRootHash.Equals(block.StateRootHash)) { - string blockDump = DumpBencodexToFile( - block.MarshalBlock(), - $"block_{block.Index}_{block.Hash}" - ); - string deltaDump = DumpBencodexToFile( - new Dictionary( - GetTotalDelta(delta, ToStateKey, ToFungibleAssetKey, ToTotalSupplyKey, ValidatorSetKey)), - $"delta_{block.Index}_{block.Hash}" - ); string message = $"Unexpected state root hash for block #{block.Index} {block.Hash}.\n" + - $" Expected: {block.StateRootHash}\n Actual: {stateRootHash}\n" + - $" Block file: {blockDump}\n Evaluated delta file: {deltaDump}\n"; + $" Expected: {block.StateRootHash}\n Actual: {stateRootHash}\n"; if (!bypassStateRootHashCheck) { throw new CommandExitedException(message, 1); diff --git a/NineChronicles.Headless.Executable/Configuration.cs b/NineChronicles.Headless.Executable/Configuration.cs index edba40765..28b2d40cc 100644 --- a/NineChronicles.Headless.Executable/Configuration.cs +++ b/NineChronicles.Headless.Executable/Configuration.cs @@ -89,6 +89,8 @@ public class Configuration public StateServiceManagerServiceOptions? StateServiceManagerService { get; set; } + public AccessControlServiceOptions? AccessControlService { get; set; } + public void Overwrite( string? appProtocolVersionString, string[]? trustedAppProtocolVersionSignerStrings, diff --git a/NineChronicles.Headless.Executable/NineChronicles.Headless.Executable.csproj b/NineChronicles.Headless.Executable/NineChronicles.Headless.Executable.csproj index 95b3ae808..433336e26 100644 --- a/NineChronicles.Headless.Executable/NineChronicles.Headless.Executable.csproj +++ b/NineChronicles.Headless.Executable/NineChronicles.Headless.Executable.csproj @@ -20,6 +20,7 @@ + diff --git a/NineChronicles.Headless.Executable/Program.cs b/NineChronicles.Headless.Executable/Program.cs index c7cab8112..9828513a3 100644 --- a/NineChronicles.Headless.Executable/Program.cs +++ b/NineChronicles.Headless.Executable/Program.cs @@ -43,6 +43,7 @@ namespace NineChronicles.Headless.Executable { + [HasSubCommands(typeof(Cocona.Docs.DocumentCommand), "docs")] [HasSubCommands(typeof(AccountCommand), "account")] [HasSubCommands(typeof(ValidationCommand), "validation")] [HasSubCommands(typeof(ChainCommand), "chain")] @@ -436,7 +437,7 @@ IActionLoader MakeSingleActionLoader() : new PrivateKey(ByteUtil.ParseHex(headlessConfig.MinerPrivateKeyString)); TimeSpan minerBlockInterval = TimeSpan.FromMilliseconds(headlessConfig.MinerBlockIntervalMilliseconds); var nineChroniclesProperties = - new NineChroniclesNodeServiceProperties(actionLoader, headlessConfig.StateServiceManagerService) + new NineChroniclesNodeServiceProperties(actionLoader, headlessConfig.StateServiceManagerService, headlessConfig.AccessControlService) { MinerPrivateKey = minerPrivateKey, Libplanet = properties, @@ -480,6 +481,7 @@ IActionLoader MakeSingleActionLoader() standaloneContext.NineChroniclesNodeService!.ActionRenderer, standaloneContext.NineChroniclesNodeService!.ExceptionRenderer, standaloneContext.NineChroniclesNodeService!.NodeStatusRenderer, + standaloneContext.NineChroniclesNodeService!.BlockChain, IPAddress.Loopback.ToString(), rpcProperties.RpcListenPort, context, @@ -509,6 +511,7 @@ IActionLoader MakeSingleActionLoader() standaloneContext.NineChroniclesNodeService!.ActionRenderer, standaloneContext.NineChroniclesNodeService!.ExceptionRenderer, standaloneContext.NineChroniclesNodeService!.NodeStatusRenderer, + standaloneContext.NineChroniclesNodeService!.BlockChain, IPAddress.Loopback.ToString(), 0, context, diff --git a/NineChronicles.Headless.Executable/appsettings.json b/NineChronicles.Headless.Executable/appsettings.json index 87b56cfff..aef98715b 100644 --- a/NineChronicles.Headless.Executable/appsettings.json +++ b/NineChronicles.Headless.Executable/appsettings.json @@ -125,8 +125,8 @@ }, "MultiAccountManaging": { "EnableManaging": false, - "ManagementTimeMinutes": 10, - "TxIntervalMinutes": 10, + "ManagementTimeMinutes": 60, + "TxIntervalMinutes": 60, "ThresholdCount": 29 } } diff --git a/NineChronicles.Headless.Tests/GraphQLStartupTest.cs b/NineChronicles.Headless.Tests/GraphQLStartupTest.cs index 2568550be..c64ea79d0 100644 --- a/NineChronicles.Headless.Tests/GraphQLStartupTest.cs +++ b/NineChronicles.Headless.Tests/GraphQLStartupTest.cs @@ -4,8 +4,8 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using static NineChronicles.Headless.Tests.GraphQLTestUtils; using Xunit; +using static NineChronicles.Headless.Tests.GraphQLTestUtils; namespace NineChronicles.Headless.Tests { @@ -23,6 +23,7 @@ public GraphQLStartupTest() new ActionRenderer(), new ExceptionRenderer(), new NodeStatusRenderer(), + standaloneContext!.BlockChain, "", 0, new RpcContext(), diff --git a/NineChronicles.Headless.Tests/GraphQLTestUtils.cs b/NineChronicles.Headless.Tests/GraphQLTestUtils.cs index ce2ad8c6b..67e8eb60a 100644 --- a/NineChronicles.Headless.Tests/GraphQLTestUtils.cs +++ b/NineChronicles.Headless.Tests/GraphQLTestUtils.cs @@ -82,7 +82,7 @@ public static StandaloneContext CreateStandaloneContext() var policy = new BlockPolicy(); var actionEvaluator = new ActionEvaluator( _ => policy.BlockAction, - new BlockChainStates(store, stateStore), + stateStore, new NCActionLoader()); var genesisBlock = BlockChain.ProposeGenesisBlock(actionEvaluator); var blockchain = BlockChain.Create( @@ -113,7 +113,7 @@ PrivateKey minerPrivateKey var policy = new BlockPolicy(); var actionEvaluator = new ActionEvaluator( _ => policy.BlockAction, - new BlockChainStates(store, stateStore), + stateStore, new NCActionLoader()); var genesisBlock = BlockChain.ProposeGenesisBlock( actionEvaluator, diff --git a/NineChronicles.Headless.Tests/GraphTypes/GraphQLTestBase.cs b/NineChronicles.Headless.Tests/GraphTypes/GraphQLTestBase.cs index b5533d1fa..4643f7e9c 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/GraphQLTestBase.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/GraphQLTestBase.cs @@ -58,7 +58,7 @@ public GraphQLTestBase(ITestOutputHelper output) var blockAction = new RewardGold(); var actionEvaluator = new ActionEvaluator( _ => blockAction, - new BlockChainStates(new MemoryStore(), new TrieStateStore(new MemoryKeyValueStore())), + new TrieStateStore(new MemoryKeyValueStore()), new NCActionLoader()); var genesisBlock = BlockChain.ProposeGenesisBlock( actionEvaluator, @@ -115,6 +115,7 @@ public GraphQLTestBase(ITestOutputHelper output) ncService.ActionRenderer, ncService.ExceptionRenderer, ncService.NodeStatusRenderer, + ncService.BlockChain, "", 0, new RpcContext(), diff --git a/NineChronicles.Headless.Tests/GraphTypes/StandaloneMutationTest.cs b/NineChronicles.Headless.Tests/GraphTypes/StandaloneMutationTest.cs index 3f9d1325d..14e1e7f49 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/StandaloneMutationTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/StandaloneMutationTest.cs @@ -1007,7 +1007,7 @@ private Block MakeGenesisBlock( { var actionEvaluator = new ActionEvaluator( _ => ServiceBuilder.BlockPolicy.BlockAction, - new BlockChainStates(new MemoryStore(), new TrieStateStore(new MemoryKeyValueStore())), + new TrieStateStore(new MemoryKeyValueStore()), new NCActionLoader()); return BlockChain.ProposeGenesisBlock( actionEvaluator, diff --git a/NineChronicles.Headless.Tests/GraphTypes/StandaloneQueryTest.cs b/NineChronicles.Headless.Tests/GraphTypes/StandaloneQueryTest.cs index 62dad6f21..a7847392f 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/StandaloneQueryTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/StandaloneQueryTest.cs @@ -118,7 +118,7 @@ public async Task NodeStatus() var apv = AppProtocolVersion.Sign(apvPrivateKey, 0); var actionEvaluator = new ActionEvaluator( _ => null, - new BlockChainStates(new MemoryStore(), new TrieStateStore(new MemoryKeyValueStore())), + new TrieStateStore(new MemoryKeyValueStore()), new NCActionLoader()); var genesisBlock = BlockChain.ProposeGenesisBlock(actionEvaluator); @@ -446,7 +446,7 @@ public async Task ActivationStatus(bool existsActivatedAccounts) }.ToList()); var actionEvaluator = new ActionEvaluator( _ => null, - new BlockChainStates(new MemoryStore(), new TrieStateStore(new MemoryKeyValueStore())), + new TrieStateStore(new MemoryKeyValueStore()), new NCActionLoader()); Block genesis = BlockChain.ProposeGenesisBlock( @@ -840,7 +840,7 @@ public async Task ActivationKeyNonce(bool trim) }; var actionEvaluator = new ActionEvaluator( _ => null, - new BlockChainStates(new MemoryStore(), new TrieStateStore(new MemoryKeyValueStore())), + new TrieStateStore(new MemoryKeyValueStore()), new NCActionLoader()); Block genesis = BlockChain.ProposeGenesisBlock( @@ -925,7 +925,7 @@ public async Task ActivationKeyNonce_Throw_ExecutionError(string code, string ms var actionEvaluator = new ActionEvaluator( _ => null, - new BlockChainStates(new MemoryStore(), new TrieStateStore(new MemoryKeyValueStore())), + new TrieStateStore(new MemoryKeyValueStore()), new NCActionLoader()); Block genesis = BlockChain.ProposeGenesisBlock( @@ -1006,7 +1006,7 @@ public async Task Balance() var actionEvaluator = new ActionEvaluator( _ => null, - new BlockChainStates(new MemoryStore(), new TrieStateStore(new MemoryKeyValueStore())), + new TrieStateStore(new MemoryKeyValueStore()), new NCActionLoader()); Block genesis = BlockChain.ProposeGenesisBlock( @@ -1111,7 +1111,7 @@ private NineChroniclesNodeService MakeNineChroniclesNodeService(PrivateKey priva }.ToList()); var actionEvaluator = new ActionEvaluator( _ => blockPolicy.BlockAction, - new BlockChainStates(new MemoryStore(), new TrieStateStore(new MemoryKeyValueStore())), + new TrieStateStore(new MemoryKeyValueStore()), new NCActionLoader()); Block genesis = BlockChain.ProposeGenesisBlock( diff --git a/NineChronicles.Headless.Tests/GraphTypes/StandaloneSubscriptionTest.cs b/NineChronicles.Headless.Tests/GraphTypes/StandaloneSubscriptionTest.cs index 27b8cec53..c441f7d8b 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/StandaloneSubscriptionTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/StandaloneSubscriptionTest.cs @@ -54,7 +54,6 @@ public async Task SubscribeTipChangedEvent() BlockChain.Append(block, GenerateBlockCommit(block.Index, block.Hash, GenesisValidators)); // var data = (Dictionary)((ExecutionNode) result.Data!).ToValue()!; - Assert.Equal(index, BlockChain.Tip.Index); await Task.Delay(TimeSpan.FromSeconds(1)); @@ -136,7 +135,7 @@ public async Task SubscribePreloadProgress() var apv = AppProtocolVersion.Sign(apvPrivateKey, 0); var actionEvaluator = new ActionEvaluator( _ => null, - new BlockChainStates(new MemoryStore(), new TrieStateStore(new MemoryKeyValueStore())), + new TrieStateStore(new MemoryKeyValueStore()), new SingleActionLoader(typeof(EmptyAction))); var genesisBlock = BlockChain.ProposeGenesisBlock( actionEvaluator, diff --git a/NineChronicles.Headless.Tests/GraphTypes/States/Models/CrystalMonsterCollectionMultiplierSheetTypeTest.cs b/NineChronicles.Headless.Tests/GraphTypes/States/Models/CrystalMonsterCollectionMultiplierSheetTypeTest.cs index a1a20e7a5..491f17b83 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/States/Models/CrystalMonsterCollectionMultiplierSheetTypeTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/States/Models/CrystalMonsterCollectionMultiplierSheetTypeTest.cs @@ -59,6 +59,16 @@ public async Task Query() ["level"] = 5, ["multiplier"] = 300, }, + new Dictionary + { + ["level"] = 6, + ["multiplier"] = 300, + }, + new Dictionary + { + ["level"] = 7, + ["multiplier"] = 300, + }, }; var expected = new Dictionary { { "orderedList", list } }; Assert.Equal(expected, data); diff --git a/NineChronicles.Headless.Tests/GraphTypes/TransactionHeadlessQueryTest.cs b/NineChronicles.Headless.Tests/GraphTypes/TransactionHeadlessQueryTest.cs index 5716a9479..a3bd08316 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/TransactionHeadlessQueryTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/TransactionHeadlessQueryTest.cs @@ -46,7 +46,7 @@ public TransactionHeadlessQueryTest() IBlockPolicy policy = NineChroniclesNodeService.GetTestBlockPolicy(); var actionEvaluator = new ActionEvaluator( _ => policy.BlockAction, - new BlockChainStates(_store, _stateStore), + _stateStore, new NCActionLoader()); Block genesisBlock = BlockChain.ProposeGenesisBlock( actionEvaluator, diff --git a/NineChronicles.Headless/ActionEvaluationPublisher.cs b/NineChronicles.Headless/ActionEvaluationPublisher.cs index 21a8bd643..623da1aa9 100644 --- a/NineChronicles.Headless/ActionEvaluationPublisher.cs +++ b/NineChronicles.Headless/ActionEvaluationPublisher.cs @@ -3,12 +3,14 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics; using System.Diagnostics.Metrics; using System.IO; using System.IO.Compression; using System.Linq; using System.Reactive.Concurrency; using System.Reactive.Linq; +using System.Reactive.Subjects; using System.Runtime.Serialization; using System.Threading; using System.Threading.Tasks; @@ -16,8 +18,9 @@ using Bencodex.Types; using Grpc.Core; using Grpc.Net.Client; -using Lib9c.Abstractions; using Lib9c.Renderers; +using Libplanet.Action.State; +using Libplanet.Blockchain; using Libplanet.Common; using Libplanet.Crypto; using Libplanet.Types.Blocks; @@ -28,9 +31,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Nekoyume.Action; -using Nekoyume.Model.State; using Nekoyume.Shared.Hubs; -using Sentry; using Serilog; namespace NineChronicles.Headless @@ -44,6 +45,7 @@ public class ActionEvaluationPublisher : BackgroundService private readonly ActionRenderer _actionRenderer; private readonly ExceptionRenderer _exceptionRenderer; private readonly NodeStatusRenderer _nodeStatusRenderer; + private readonly IBlockChainStates _blockChainStates; private readonly ConcurrentDictionary _clients = new(); private readonly ConcurrentDictionary _clientsByDevice = new(); @@ -59,6 +61,7 @@ public ActionEvaluationPublisher( ActionRenderer actionRenderer, ExceptionRenderer exceptionRenderer, NodeStatusRenderer nodeStatusRenderer, + IBlockChainStates blockChainStates, string host, int port, RpcContext context, @@ -68,6 +71,7 @@ public ActionEvaluationPublisher( _actionRenderer = actionRenderer; _exceptionRenderer = exceptionRenderer; _nodeStatusRenderer = nodeStatusRenderer; + _blockChainStates = blockChainStates; _host = host; _port = port; _context = context; @@ -120,7 +124,7 @@ public async Task AddClient(Address clientAddress) }; GrpcChannel channel = GrpcChannel.ForAddress($"http://{_host}:{_port}", options); - Client client = await Client.CreateAsync(channel, clientAddress, _context, _sentryTraces); + Client client = await Client.CreateAsync(channel, _blockChainStates, clientAddress, _context, _sentryTraces); if (_clients.TryAdd(clientAddress, client)) { if (clientAddress == default) @@ -370,6 +374,7 @@ private void DFS(string node, HashSet ips, HashSet ids, Concurre private sealed class Client : IAsyncDisposable { private readonly IActionEvaluationHub _hub; + private readonly IBlockChainStates _blockChainStates; private readonly RpcContext _context; private readonly Address _clientAddress; @@ -378,17 +383,22 @@ private sealed class Client : IAsyncDisposable private IDisposable? _everyExceptionSubscribe; private IDisposable? _nodeStatusSubscribe; + private Subject _NCActionRenderSubject { get; } + = new Subject(); + public ImmutableHashSet
TargetAddresses { get; set; } public readonly ConcurrentDictionary SentryTraces; private Client( IActionEvaluationHub hub, + IBlockChainStates blockChainStates, Address clientAddress, RpcContext context, ConcurrentDictionary sentryTraces) { _hub = hub; + _blockChainStates = blockChainStates; _clientAddress = clientAddress; _context = context; TargetAddresses = ImmutableHashSet
.Empty; @@ -397,6 +407,7 @@ private Client( public static async Task CreateAsync( GrpcChannel channel, + IBlockChainStates blockChainStates, Address clientAddress, RpcContext context, ConcurrentDictionary sentryTraces) @@ -407,7 +418,7 @@ public static async Task CreateAsync( ); await hub.JoinAsync(clientAddress.ToHex()); - return new Client(hub, clientAddress, context, sentryTraces); + return new Client(hub, blockChainStates, clientAddress, context, sentryTraces); } public void Subscribe( @@ -438,7 +449,6 @@ await _hub.BroadcastRenderBlockAsync( ); _actionEveryRenderSubscribe = actionRenderer.EveryRender() - .Where(ContainsAddressToBroadcast) .SubscribeOn(NewThreadScheduler.Default) .ObserveOn(NewThreadScheduler.Default) .Subscribe( @@ -446,56 +456,27 @@ await _hub.BroadcastRenderBlockAsync( { try { + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); ActionBase? pa = ev.Action is RewardGold ? null : ev.Action; var extra = new Dictionary(); - - var previousStates = ev.PreviousState; - if (pa is IBattleArenaV1 battleArena) + IAccountState output = _blockChainStates.GetAccountState(ev.OutputState); + IAccountState input = _blockChainStates.GetAccountState(ev.PreviousState); + AccountDiff diff = AccountDiff.Create(input, output); + var updatedAddresses = diff.StateDiffs.Keys + .Union(diff.FungibleAssetValueDiffs.Select(kv => kv.Key.Item1)) + .Append(ev.Signer) + .ToHashSet(); + if (!TargetAddresses.Any(updatedAddresses.Contains)) { - var enemyAvatarAddress = battleArena.EnemyAvatarAddress; - if (previousStates.GetState(enemyAvatarAddress) is { } eAvatar) - { - const string inventoryKey = "inventory"; - previousStates = previousStates.SetState(enemyAvatarAddress, eAvatar); - if (previousStates.GetState(enemyAvatarAddress.Derive(inventoryKey)) is { } inventory) - { - previousStates = previousStates.SetState( - enemyAvatarAddress.Derive(inventoryKey), - inventory); - } - } - - var enemyItemSlotStateAddress = - ItemSlotState.DeriveAddress(battleArena.EnemyAvatarAddress, - Nekoyume.Model.EnumType.BattleType.Arena); - if (previousStates.GetState(enemyItemSlotStateAddress) is { } eItemSlot) - { - previousStates = previousStates.SetState(enemyItemSlotStateAddress, eItemSlot); - } - - var enemyRuneSlotStateAddress = - RuneSlotState.DeriveAddress(battleArena.EnemyAvatarAddress, - Nekoyume.Model.EnumType.BattleType.Arena); - if (previousStates.GetState(enemyRuneSlotStateAddress) is { } eRuneSlot) - { - previousStates = previousStates.SetState(enemyRuneSlotStateAddress, eRuneSlot); - var runeSlot = new RuneSlotState(eRuneSlot as List); - var enemyRuneSlotInfos = runeSlot.GetEquippedRuneSlotInfos(); - var runeAddresses = enemyRuneSlotInfos.Select(info => - RuneState.DeriveAddress(battleArena.EnemyAvatarAddress, info.RuneId)); - foreach (var address in runeAddresses) - { - if (previousStates.GetState(address) is { } rune) - { - previousStates = previousStates.SetState(address, rune); - } - } - } + return; } - var eval = new NCActionEvaluation(pa, ev.Signer, ev.BlockIndex, ev.OutputState, ev.Exception, previousStates, ev.RandomSeed, extra); + var encodeElapsedMilliseconds = stopwatch.ElapsedMilliseconds; + + var eval = new NCActionEvaluation(pa, ev.Signer, ev.BlockIndex, ev.OutputState, ev.Exception, ev.PreviousState, ev.RandomSeed, extra); var encoded = MessagePackSerializer.Serialize(eval); var c = new MemoryStream(); await using (var df = new DeflateStream(c, CompressionLevel.Fastest)) @@ -513,6 +494,21 @@ await _hub.BroadcastRenderBlockAsync( ); await _hub.BroadcastRenderAsync(compressed); + stopwatch.Stop(); + + var broadcastElapsedMilliseconds = stopwatch.ElapsedMilliseconds - encodeElapsedMilliseconds; + Log + .ForContext("tag", "Metric") + .ForContext("subtag", "ActionEvaluationPublisherElapse") + .Information( + "[{ClientAddress}], #{BlockIndex}, {Action}," + + " {EncodeElapsedMilliseconds}, {BroadcastElapsedMilliseconds}, {TotalElapsedMilliseconds}", + _clientAddress, + ev.BlockIndex, + ev.Action.GetType(), + encodeElapsedMilliseconds, + broadcastElapsedMilliseconds, + encodeElapsedMilliseconds + broadcastElapsedMilliseconds); } catch (SerializationException se) { @@ -589,25 +585,6 @@ public async ValueTask DisposeAsync() _nodeStatusSubscribe?.Dispose(); await _hub.DisposeAsync(); } - - private bool ContainsAddressToBroadcast(ActionEvaluation ev) - { - return _context.RpcRemoteSever - ? ContainsAddressToBroadcastRemoteClient(ev) - : ContainsAddressToBroadcastLocal(ev); - } - - private bool ContainsAddressToBroadcastLocal(ActionEvaluation ev) - { - var updatedAddresses = ev.OutputState.Delta.UpdatedAddresses; - return _context.AddressesToSubscribe.Any(updatedAddresses.Add(ev.Signer).Contains); - } - - private bool ContainsAddressToBroadcastRemoteClient(ActionEvaluation ev) - { - var updatedAddresses = ev.OutputState.Delta.UpdatedAddresses; - return TargetAddresses.Any(updatedAddresses.Add(ev.Signer).Contains); - } } } } diff --git a/NineChronicles.Headless/BlockChainService.cs b/NineChronicles.Headless/BlockChainService.cs index c28425588..71c94bdb4 100644 --- a/NineChronicles.Headless/BlockChainService.cs +++ b/NineChronicles.Headless/BlockChainService.cs @@ -4,10 +4,13 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Security.Cryptography; using System.Threading.Tasks; using Bencodex; using Bencodex.Types; +using Libplanet.Action.State; using Libplanet.Blockchain; +using Libplanet.Common; using Libplanet.Crypto; using Libplanet.Headless.Hosting; using Libplanet.Net; @@ -116,12 +119,21 @@ public UnaryResult GetState(byte[] addressBytes, byte[] blockHashBytes) { var address = new Address(addressBytes); var hash = new BlockHash(blockHashBytes); - IValue state = _blockChain.GetStates(new[] { address }, hash)[0]; + IValue state = _blockChain.GetAccountState(hash).GetState(address); // FIXME: Null과 null 구분해서 반환해야 할 듯 byte[] encoded = _codec.Encode(state ?? Null.Value); return new UnaryResult(encoded); } + public UnaryResult GetStateBySrh(byte[] addressBytes, byte[] stateRootHashBytes) + { + var stateRootHash = new HashDigest(stateRootHashBytes); + var address = new Address(addressBytes); + IValue state = _blockChain.GetAccountState(stateRootHash).GetState(address); + byte[] encoded = _codec.Encode(state ?? Null.Value); + return new UnaryResult(encoded); + } + public async UnaryResult> GetAvatarStates(IEnumerable addressBytesList, byte[] blockHashBytes) { var hash = new BlockHash(blockHashBytes); @@ -140,6 +152,26 @@ public async UnaryResult> GetAvatarStates(IEnumerable return result.ToDictionary(kv => kv.Key, kv => kv.Value); } + public async UnaryResult> GetAvatarStatesBySrh( + IEnumerable addressBytesList, + byte[] stateRootHashBytes) + { + var stateRootHash = new HashDigest(stateRootHashBytes); + var accountState = _blockChain.GetAccountState(stateRootHash); + var result = new ConcurrentDictionary(); + var addresses = addressBytesList.Select(a => new Address(a)).ToList(); + var rawAvatarStates = accountState.GetRawAvatarStates(addresses); + var taskList = rawAvatarStates + .Select(pair => Task.Run(() => + { + result.TryAdd(pair.Key.ToByteArray(), _codec.Encode(pair.Value)); + })) + .ToList(); + + await Task.WhenAll(taskList); + return result.ToDictionary(kv => kv.Key, kv => kv.Value); + } + public UnaryResult> GetStateBulk(IEnumerable addressBytesList, byte[] blockHashBytes) { var hash = new BlockHash(blockHashBytes); @@ -154,13 +186,48 @@ public UnaryResult> GetStateBulk(IEnumerable return new UnaryResult>(result); } + public UnaryResult> GetStateBulkBySrh( + IEnumerable addressBytesList, + byte[] stateRootHashBytes) + { + var stateRootHash = new HashDigest(stateRootHashBytes); + var result = new Dictionary(); + Address[] addresses = addressBytesList.Select(b => new Address(b)).ToArray(); + IReadOnlyList values = _blockChain.GetAccountState(stateRootHash).GetStates(addresses); + for (int i = 0; i < addresses.Length; i++) + { + result.TryAdd(addresses[i].ToByteArray(), _codec.Encode(values[i] ?? Null.Value)); + } + + return new UnaryResult>(result); + } + public UnaryResult GetBalance(byte[] addressBytes, byte[] currencyBytes, byte[] blockHashBytes) { var address = new Address(addressBytes); var serializedCurrency = (Bencodex.Types.Dictionary)_codec.Decode(currencyBytes); Currency currency = CurrencyExtensions.Deserialize(serializedCurrency); var hash = new BlockHash(blockHashBytes); - FungibleAssetValue balance = _blockChain.GetBalance(address, currency, hash); + FungibleAssetValue balance = _blockChain.GetAccountState(hash).GetBalance(address, currency); + byte[] encoded = _codec.Encode( + new Bencodex.Types.List( + new IValue[] + { + balance.Currency.Serialize(), + (Integer) balance.RawValue, + } + ) + ); + return new UnaryResult(encoded); + } + + public UnaryResult GetBalanceBySrh(byte[] addressBytes, byte[] currencyBytes, byte[] stateRootHashBytes) + { + var address = new Address(addressBytes); + var stateRootHash = new HashDigest(stateRootHashBytes); + var serializedCurrency = (Bencodex.Types.Dictionary)_codec.Decode(currencyBytes); + Currency currency = CurrencyExtensions.Deserialize(serializedCurrency); + FungibleAssetValue balance = _blockChain.GetAccountState(stateRootHash).GetBalance(address, currency); byte[] encoded = _codec.Encode( new Bencodex.Types.List( new IValue[] diff --git a/NineChronicles.Headless/Controllers/GraphQLController.cs b/NineChronicles.Headless/Controllers/GraphQLController.cs index 501af000f..88dacd21f 100644 --- a/NineChronicles.Headless/Controllers/GraphQLController.cs +++ b/NineChronicles.Headless/Controllers/GraphQLController.cs @@ -18,6 +18,8 @@ using NineChronicles.Headless.Requests; using Serilog; using Lib9c.Renderers; +using Libplanet.Action.State; +using System.Collections.Immutable; namespace NineChronicles.Headless.Controllers { @@ -233,7 +235,13 @@ private void NotifyAction(ActionEvaluation eval) return; } Address address = StandaloneContext.NineChroniclesNodeService.MinerPrivateKey.PublicKey.ToAddress(); - if (eval.OutputState.Delta.UpdatedAddresses.Contains(address) || eval.Signer == address) + var input = StandaloneContext.NineChroniclesNodeService.BlockChain.GetAccountState(eval.PreviousState); + var output = StandaloneContext.NineChroniclesNodeService.BlockChain.GetAccountState(eval.OutputState); + var diff = AccountDiff.Create(input, output); + var updatedAddresses = diff.FungibleAssetValueDiffs + .Select(pair => pair.Key.Item1) + .Concat(diff.StateDiffs.Keys).ToImmutableHashSet(); + if (updatedAddresses.Contains(address) || eval.Signer == address) { if (eval.Signer == address) { diff --git a/NineChronicles.Headless/GraphQLServiceExtensions.cs b/NineChronicles.Headless/GraphQLServiceExtensions.cs index 9820b3d93..4679ef4e7 100644 --- a/NineChronicles.Headless/GraphQLServiceExtensions.cs +++ b/NineChronicles.Headless/GraphQLServiceExtensions.cs @@ -2,7 +2,6 @@ using System.Linq; using System.Reflection; using GraphQL.Types; -using Libplanet.Action; using Libplanet.Explorer.GraphTypes; using Libplanet.Explorer.Interfaces; using Libplanet.Explorer.Queries; @@ -36,8 +35,6 @@ public static IServiceCollection AddLibplanetScalarTypes(this IServiceCollection services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); - services.TryAddSingleton(); - services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); @@ -48,6 +45,7 @@ public static IServiceCollection AddLibplanetScalarTypes(this IServiceCollection services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); return services; } diff --git a/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs b/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs index a5d9688d5..ae7394372 100644 --- a/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs +++ b/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs @@ -120,33 +120,28 @@ public StandaloneQuery(StandaloneContext standaloneContext, IConfiguration confi var recipient = context.GetArgument("recipient"); - IEnumerable txs = digest.TxIds + IEnumerable blockTxs = digest.TxIds .Select(bytes => new TxId(bytes)) .Select(store.GetTransaction); - var pairs = txs - .Where(tx => - tx.Actions!.Count == 1 && - store.GetTxExecution(blockHash, tx.Id) is TxSuccess) - .Select(tx => (tx.Id, ToAction(tx.Actions.First()))) - .Where(pair => - pair.Item2 is ITransferAsset transferAssset && - transferAssset.Amount.Currency.Ticker == "NCG") + var filtered = blockTxs + .Where(tx => tx.Actions.Count == 1) + .Select(tx => (store.GetTxExecution(blockHash, tx.Id), ToAction(tx.Actions[0]))) + .Where(pair => pair.Item1 is { } && pair.Item2 is ITransferAsset) .Select(pair => (pair.Item1, (ITransferAsset)pair.Item2)) - .Where(pair => (!(recipient is { } r) || pair.Item2.Recipient == r)); + .Where(pair => !pair.Item1.Fail && + (!recipient.HasValue || pair.Item2.Recipient == recipient) && + pair.Item2.Amount.Currency.Ticker == "NCG"); + + var histories = filtered.Select(pair => + new TransferNCGHistory( + pair.Item1.BlockHash, + pair.Item1.TxId, + pair.Item2.Sender, + pair.Item2.Recipient, + pair.Item2.Amount, + pair.Item2.Memo)); - TransferNCGHistory ToTransferNCGHistory((TxId TxId, ITransferAsset Transfer) pair) - { - return new TransferNCGHistory( - blockHash, - pair.TxId, - pair.Transfer.Sender, - pair.Transfer.Recipient, - pair.Transfer.Amount, - pair.Transfer.Memo); - } - - var histories = pairs.Select(ToTransferNCGHistory); return histories; }); diff --git a/NineChronicles.Headless/GraphTypes/StandaloneSubscription.cs b/NineChronicles.Headless/GraphTypes/StandaloneSubscription.cs index 7ff4b2054..bf3c025b7 100644 --- a/NineChronicles.Headless/GraphTypes/StandaloneSubscription.cs +++ b/NineChronicles.Headless/GraphTypes/StandaloneSubscription.cs @@ -15,6 +15,7 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using System.Reactive.Disposables; +using System.Text.RegularExpressions; using Bencodex.Types; using Libplanet.Crypto; using Libplanet.Types.Assets; @@ -145,7 +146,7 @@ public StandaloneSubscription(StandaloneContext standaloneContext) Arguments = new QueryArguments( new QueryArgument> { - Description = "A type of action in transaction.", + Description = "A regular expression to filter transactions based on action type.", Name = "actionType", } ), @@ -304,7 +305,7 @@ private IObservable SubscribeTx(IResolveFieldContext context) return false; } - return typeId == actionType; + return Regex.IsMatch(typeId, actionType); })) .Select(transaction => new Tx { @@ -321,29 +322,13 @@ private IObservable SubscribeTx(IResolveFieldContext context) } var txExecution = store.GetTxExecution(blockHash, transaction.Id); var txExecutedBlock = chain[blockHash]; - - return txExecution switch - { - TxSuccess success => new TxResult( - TxStatus.SUCCESS, - txExecutedBlock.Index, - txExecutedBlock.Hash.ToString(), - null, - success.UpdatedStates - .Select(kv => new KeyValuePair( - kv.Key, - kv.Value)) - .ToImmutableDictionary(), - success.UpdatedFungibleAssets), - TxFailure failure => new TxResult( - TxStatus.FAILURE, + return new TxResult( + txExecution.Fail ? TxStatus.FAILURE : TxStatus.SUCCESS, txExecutedBlock.Index, txExecutedBlock.Hash.ToString(), - failure.ExceptionName, - null, - null), - _ => null - }; + txExecution.InputState, + txExecution.OutputState, + txExecution.ExceptionNames); } private void RenderBlock((Block OldTip, Block NewTip) pair) @@ -465,7 +450,7 @@ private void RenderMonsterCollectionStateSubject(ActionEvaluation eval) var agentState = new AgentState(agentDict); Address deriveAddress = MonsterCollectionState.DeriveAddress(address, agentState.MonsterCollectionRound); var subject = subjects.stateSubject; - if (eval.OutputState.GetState(deriveAddress) is Dictionary state) + if (service.BlockChain.GetAccountState(eval.OutputState).GetState(deriveAddress) is Dictionary state) { subject.OnNext(new MonsterCollectionState(state)); } diff --git a/NineChronicles.Headless/GraphTypes/TransactionHeadlessQuery.cs b/NineChronicles.Headless/GraphTypes/TransactionHeadlessQuery.cs index 5be72bb01..86f227fe0 100644 --- a/NineChronicles.Headless/GraphTypes/TransactionHeadlessQuery.cs +++ b/NineChronicles.Headless/GraphTypes/TransactionHeadlessQuery.cs @@ -216,29 +216,13 @@ public TransactionHeadlessQuery(StandaloneContext standaloneContext) { TxExecution execution = blockChain.GetTxExecution(txExecutedBlockHash, txId); Block txExecutedBlock = blockChain[txExecutedBlockHash]; - return execution switch - { - TxSuccess txSuccess => new TxResult( - TxStatus.SUCCESS, - txExecutedBlock.Index, - txExecutedBlock.Hash.ToString(), - null, - txSuccess.UpdatedStates - .Select(kv => new KeyValuePair( - kv.Key, - kv.Value)) - .ToImmutableDictionary(), - txSuccess.UpdatedFungibleAssets), - TxFailure txFailure => new TxResult( - TxStatus.FAILURE, - txExecutedBlock.Index, - txExecutedBlock.Hash.ToString(), - txFailure.ExceptionName, - null, - null), - _ => throw new NotImplementedException( - $"{nameof(execution)} is not expected concrete class.") - }; + return new TxResult( + execution.Fail ? TxStatus.FAILURE : TxStatus.SUCCESS, + txExecutedBlock.Index, + txExecutedBlock.Hash.ToString(), + execution.InputState, + execution.OutputState, + execution.ExceptionNames); } catch (Exception) { diff --git a/NineChronicles.Headless/GraphTypes/TransactionType.cs b/NineChronicles.Headless/GraphTypes/TransactionType.cs index 3a539f735..d56d4391a 100644 --- a/NineChronicles.Headless/GraphTypes/TransactionType.cs +++ b/NineChronicles.Headless/GraphTypes/TransactionType.cs @@ -1,3 +1,4 @@ +using System; using GraphQL.Types; using Libplanet.Explorer.GraphTypes; using Libplanet.Types.Tx; @@ -48,6 +49,15 @@ public TransactionType() description: "A list of actions in this transaction.", resolve: context => context.Source.Actions ); + + Field>( + name: "SerializedPayload", + description: "A serialized tx payload in base64 string.", + resolve: x => + { + byte[] bytes = x.Source.Serialize(); + return Convert.ToBase64String(bytes); + }); } } } diff --git a/NineChronicles.Headless/HostBuilderExtensions.cs b/NineChronicles.Headless/HostBuilderExtensions.cs index 65afd87f9..25e37cc22 100644 --- a/NineChronicles.Headless/HostBuilderExtensions.cs +++ b/NineChronicles.Headless/HostBuilderExtensions.cs @@ -33,6 +33,7 @@ NineChroniclesNodeService service { return builder.ConfigureServices(services => { + services.AddOptions(); services.AddHostedService(provider => service); services.AddSingleton(provider => service); services.AddSingleton(provider => service.Swarm); diff --git a/NineChronicles.Headless/Middleware/HttpMultiAccountManagementMiddleware.cs b/NineChronicles.Headless/Middleware/HttpMultiAccountManagementMiddleware.cs index 977c8dfb1..5f5498e7d 100644 --- a/NineChronicles.Headless/Middleware/HttpMultiAccountManagementMiddleware.cs +++ b/NineChronicles.Headless/Middleware/HttpMultiAccountManagementMiddleware.cs @@ -61,108 +61,88 @@ public async Task InvokeAsync(HttpContext context) var remoteIp = context.Connection.RemoteIpAddress!.ToString(); var body = await new StreamReader(context.Request.Body).ReadToEndAsync(); context.Request.Body.Seek(0, SeekOrigin.Begin); - if (_options.Value.EnableManaging) + if (_options.Value.EnableManaging && body.Contains("stageTransaction")) { - if (body.Contains("agent(address:\\\"") || body.Contains("agent(address: \\\"")) + try { - try - { - var agent = new Address(body.Split("\\\"")[1].Split("0x")[1]); - UpdateIpSignerList(remoteIp, agent); - } - catch (Exception ex) - { - _logger.Error( - "[GRAPHQL-MULTI-ACCOUNT-MANAGER] Error message: {message} Stacktrace: {stackTrace}", - ex.Message, - ex.StackTrace); - } - } + var pattern = "64313.*6565"; + var txPayload = Regex.Match(body, pattern).ToString(); + byte[] bytes = ByteUtil.ParseHex(txPayload); + Transaction tx = Transaction.Deserialize(bytes); + var agent = tx.Signer; + var action = NCActionUtils.ToAction(tx.Actions.Actions.First()); - if (body.Contains("stageTransaction")) - { - try + // Only monitoring actions not used in the launcher + if (action is not Stake + and not ClaimStakeReward + and not TransferAsset) { - var pattern = "64313.*6565"; - var txPayload = Regex.Match(body, pattern).ToString(); - byte[] bytes = ByteUtil.ParseHex(txPayload); - Transaction tx = Transaction.Deserialize(bytes); - var agent = tx.Signer; - var action = NCActionUtils.ToAction(tx.Actions.Actions.First()); - - // Only monitoring actions not used in the launcher - if (action is not Stake - and not ClaimStakeReward - and not TransferAsset) + if (_ipSignerList.ContainsKey(remoteIp)) { - if (_ipSignerList.ContainsKey(remoteIp)) + if (_ipSignerList[remoteIp].Count > _options.Value.ThresholdCount) { - if (_ipSignerList[remoteIp].Count > _options.Value.ThresholdCount) - { - _logger.Information( - "[GRAPHQL-MULTI-ACCOUNT-MANAGER] IP: {IP} List Count: {Count}, AgentAddresses: {Agent}", - remoteIp, - _ipSignerList[remoteIp].Count, - _ipSignerList[remoteIp]); + _logger.Information( + "[GRAPHQL-MULTI-ACCOUNT-MANAGER] IP: {IP} List Count: {Count}, AgentAddresses: {Agent}", + remoteIp, + _ipSignerList[remoteIp].Count, + _ipSignerList[remoteIp]); - if (!MultiAccountManagementList.ContainsKey(agent)) + if (!MultiAccountManagementList.ContainsKey(agent)) + { + if (!MultiAccountTxIntervalTracker.ContainsKey(agent)) { - if (!MultiAccountTxIntervalTracker.ContainsKey(agent)) - { - _logger.Information($"[GRAPHQL-MULTI-ACCOUNT-MANAGER] Adding agent {agent} to the agent tracker."); - MultiAccountTxIntervalTracker.Add(agent, DateTimeOffset.Now); - } - else - { - if ((DateTimeOffset.Now - MultiAccountTxIntervalTracker[agent]).Minutes >= _options.Value.TxIntervalMinutes) - { - _logger.Information($"[GRAPHQL-MULTI-ACCOUNT-MANAGER] Resetting Agent {agent}'s time because it has been more than {_options.Value.TxIntervalMinutes} minutes since the last transaction."); - MultiAccountTxIntervalTracker[agent] = DateTimeOffset.Now; - } - else - { - _logger.Information($"[GRAPHQL-MULTI-ACCOUNT-MANAGER] Managing Agent {agent} for {_options.Value.ManagementTimeMinutes} minutes due to {_ipSignerList[remoteIp].Count} associated accounts."); - ManageMultiAccount(agent); - MultiAccountTxIntervalTracker[agent] = DateTimeOffset.Now; - await CancelRequestAsync(context); - return; - } - } + _logger.Information($"[GRAPHQL-MULTI-ACCOUNT-MANAGER] Adding agent {agent} to the agent tracker."); + MultiAccountTxIntervalTracker.Add(agent, DateTimeOffset.Now); } else { - var currentManagedTime = (DateTimeOffset.Now - MultiAccountManagementList[agent]).Minutes; - if (currentManagedTime > _options.Value.ManagementTimeMinutes) + if ((DateTimeOffset.Now - MultiAccountTxIntervalTracker[agent]).Minutes >= _options.Value.TxIntervalMinutes) { - _logger.Information($"[GRAPHQL-MULTI-ACCOUNT-MANAGER] Restoring Agent {agent} after {_options.Value.ManagementTimeMinutes} minutes."); - RestoreMultiAccount(agent); - MultiAccountTxIntervalTracker[agent] = DateTimeOffset.Now.AddMinutes(-_options.Value.TxIntervalMinutes); - _logger.Information($"[GRAPHQL-MULTI-ACCOUNT-MANAGER] Current time: {DateTimeOffset.Now} Added time: {DateTimeOffset.Now.AddMinutes(-_options.Value.TxIntervalMinutes)}."); + _logger.Information($"[GRAPHQL-MULTI-ACCOUNT-MANAGER] Resetting Agent {agent}'s time because it has been more than {_options.Value.TxIntervalMinutes} minutes since the last transaction."); + MultiAccountTxIntervalTracker[agent] = DateTimeOffset.Now; } else { - _logger.Information($"[GRAPHQL-MULTI-ACCOUNT-MANAGER] Agent {agent} is in managed status for the next {_options.Value.ManagementTimeMinutes - currentManagedTime} minutes."); + _logger.Information($"[GRAPHQL-MULTI-ACCOUNT-MANAGER] Managing Agent {agent} for {_options.Value.ManagementTimeMinutes} minutes due to {_ipSignerList[remoteIp].Count} associated accounts."); + ManageMultiAccount(agent); + MultiAccountTxIntervalTracker[agent] = DateTimeOffset.Now; await CancelRequestAsync(context); return; } } } - } - else - { - UpdateIpSignerList(remoteIp, agent); + else + { + var currentManagedTime = (DateTimeOffset.Now - MultiAccountManagementList[agent]).Minutes; + if (currentManagedTime > _options.Value.ManagementTimeMinutes) + { + _logger.Information($"[GRAPHQL-MULTI-ACCOUNT-MANAGER] Restoring Agent {agent} after {_options.Value.ManagementTimeMinutes} minutes."); + RestoreMultiAccount(agent); + MultiAccountTxIntervalTracker[agent] = DateTimeOffset.Now.AddMinutes(-_options.Value.TxIntervalMinutes); + _logger.Information($"[GRAPHQL-MULTI-ACCOUNT-MANAGER] Current time: {DateTimeOffset.Now} Added time: {DateTimeOffset.Now.AddMinutes(-_options.Value.TxIntervalMinutes)}."); + } + else + { + _logger.Information($"[GRAPHQL-MULTI-ACCOUNT-MANAGER] Agent {agent} is in managed status for the next {_options.Value.ManagementTimeMinutes - currentManagedTime} minutes."); + await CancelRequestAsync(context); + return; + } + } } } - - } - catch (Exception ex) - { - _logger.Error( - "[GRAPHQL-MULTI-ACCOUNT-MANAGER] Error message: {message} Stacktrace: {stackTrace}", - ex.Message, - ex.StackTrace); + else + { + UpdateIpSignerList(remoteIp, agent); + } } } + catch (Exception ex) + { + _logger.Error( + "[GRAPHQL-MULTI-ACCOUNT-MANAGER] Error message: {message} Stacktrace: {stackTrace}", + ex.Message, + ex.StackTrace); + } } } diff --git a/NineChronicles.Headless/NineChronicles.Headless.csproj b/NineChronicles.Headless/NineChronicles.Headless.csproj index 27814de54..6bd8f6ce0 100644 --- a/NineChronicles.Headless/NineChronicles.Headless.csproj +++ b/NineChronicles.Headless/NineChronicles.Headless.csproj @@ -39,7 +39,9 @@ + + diff --git a/NineChronicles.Headless/NineChroniclesNodeService.cs b/NineChronicles.Headless/NineChroniclesNodeService.cs index 9e075af5b..181f14e6e 100644 --- a/NineChronicles.Headless/NineChroniclesNodeService.cs +++ b/NineChronicles.Headless/NineChroniclesNodeService.cs @@ -14,11 +14,13 @@ using Libplanet.Headless.Hosting; using Libplanet.Net; using Libplanet.Store; +using Libplanet.Types.Blocks; using Microsoft.Extensions.Hosting; using Nekoyume.Blockchain; using Nekoyume.Blockchain.Policy; using NineChronicles.Headless.Properties; using NineChronicles.Headless.Utils; +using NineChronicles.Headless.Services; using NineChronicles.RPC.Shared.Exceptions; using Nito.AsyncEx; using Serilog; @@ -77,14 +79,27 @@ public NineChroniclesNodeService( bool ignorePreloadFailure = false, bool strictRendering = false, TimeSpan txLifeTime = default, - int txQuotaPerSigner = 10 + int txQuotaPerSigner = 10, + AccessControlServiceOptions? acsOptions = null ) { MinerPrivateKey = minerPrivateKey; Properties = properties; LogEventLevel logLevel = LogEventLevel.Debug; - IStagePolicy stagePolicy = new NCStagePolicy(txLifeTime, txQuotaPerSigner); + + IAccessControlService? accessControlService = null; + + if (acsOptions != null) + { + accessControlService = AccessControlServiceFactory.Create( + acsOptions.GetStorageType(), + acsOptions.AccessControlServiceConnectionString + ); + } + + IStagePolicy stagePolicy = new NCStagePolicy( + txLifeTime, txQuotaPerSigner, accessControlService); BlockRenderer = new BlockRenderer(); ActionRenderer = new ActionRenderer(); @@ -200,7 +215,8 @@ StandaloneContext context ignorePreloadFailure: properties.IgnorePreloadFailure, strictRendering: properties.StrictRender, txLifeTime: properties.TxLifeTime, - txQuotaPerSigner: properties.TxQuotaPerSigner + txQuotaPerSigner: properties.TxQuotaPerSigner, + acsOptions: properties.AccessControlServiceOptions ); service.ConfigureContext(context); var meter = new Meter("NineChronicles"); @@ -277,8 +293,7 @@ internal void ConfigureContext(StandaloneContext standaloneContext) standaloneContext.Store = Store; standaloneContext.Swarm = Swarm; standaloneContext.CurrencyFactory = - new CurrencyFactory( - () => standaloneContext.BlockChain.GetAccountState(standaloneContext.BlockChain.Tip.Hash)); + new CurrencyFactory(() => standaloneContext.BlockChain.GetAccountState(standaloneContext.BlockChain.Tip.Hash)); standaloneContext.FungibleAssetValueFactory = new FungibleAssetValueFactory(standaloneContext.CurrencyFactory); BootstrapEnded.WaitAsync().ContinueWith((task) => diff --git a/NineChronicles.Headless/Properties/AccessControlServiceOptions.cs b/NineChronicles.Headless/Properties/AccessControlServiceOptions.cs new file mode 100644 index 000000000..3cb185f2d --- /dev/null +++ b/NineChronicles.Headless/Properties/AccessControlServiceOptions.cs @@ -0,0 +1,20 @@ +using System; +using System.ComponentModel.DataAnnotations; +using NineChronicles.Headless.Services; + +namespace NineChronicles.Headless.Properties +{ + public class AccessControlServiceOptions + { + [Required] + public string AccessControlServiceType { get; set; } = null!; + + [Required] + public string AccessControlServiceConnectionString { get; set; } = null!; + + public AccessControlServiceFactory.StorageType GetStorageType() + { + return Enum.Parse(AccessControlServiceType, true); + } + } +} diff --git a/NineChronicles.Headless/Properties/NineChroniclesNodeServiceProperties.cs b/NineChronicles.Headless/Properties/NineChroniclesNodeServiceProperties.cs index 38a47a8a4..b69914881 100644 --- a/NineChronicles.Headless/Properties/NineChroniclesNodeServiceProperties.cs +++ b/NineChronicles.Headless/Properties/NineChroniclesNodeServiceProperties.cs @@ -12,10 +12,12 @@ namespace NineChronicles.Headless.Properties { public class NineChroniclesNodeServiceProperties { - public NineChroniclesNodeServiceProperties(IActionLoader actionLoader, StateServiceManagerServiceOptions? stateServiceManagerServiceOptions) + public NineChroniclesNodeServiceProperties( + IActionLoader actionLoader, StateServiceManagerServiceOptions? stateServiceManagerServiceOptions, AccessControlServiceOptions? accessControlServiceOptions) { ActionLoader = actionLoader; StateServiceManagerService = stateServiceManagerServiceOptions; + AccessControlServiceOptions = accessControlServiceOptions; } /// @@ -54,6 +56,8 @@ public NineChroniclesNodeServiceProperties(IActionLoader actionLoader, StateServ public StateServiceManagerServiceOptions? StateServiceManagerService { get; } + public AccessControlServiceOptions? AccessControlServiceOptions { get; } + public static LibplanetNodeServiceProperties GenerateLibplanetNodeServiceProperties( string? appProtocolVersionToken = null, diff --git a/NineChronicles.Headless/Services/AccessControlServiceFactory.cs b/NineChronicles.Headless/Services/AccessControlServiceFactory.cs new file mode 100644 index 000000000..0ff8e476a --- /dev/null +++ b/NineChronicles.Headless/Services/AccessControlServiceFactory.cs @@ -0,0 +1,34 @@ +using System; +using Nekoyume.Blockchain; + +namespace NineChronicles.Headless.Services +{ + public static class AccessControlServiceFactory + { + public enum StorageType + { + /// + /// Use Redis + /// + Redis, + + /// + /// Use SQLite + /// + SQLite + } + + public static IAccessControlService Create( + StorageType storageType, + string connectionString + ) + { + return storageType switch + { + StorageType.Redis => new RedisAccessControlService(connectionString), + StorageType.SQLite => new SQLiteAccessControlService(connectionString), + _ => throw new ArgumentOutOfRangeException(nameof(storageType), storageType, null) + }; + } + } +} diff --git a/NineChronicles.Headless/Services/RedisAccessControlService.cs b/NineChronicles.Headless/Services/RedisAccessControlService.cs new file mode 100644 index 000000000..40769292d --- /dev/null +++ b/NineChronicles.Headless/Services/RedisAccessControlService.cs @@ -0,0 +1,32 @@ +using System; +using StackExchange.Redis; +using Libplanet.Crypto; +using Nekoyume.Blockchain; +using Serilog; + +namespace NineChronicles.Headless.Services +{ + public class RedisAccessControlService : IAccessControlService + { + protected IDatabase _db; + + public RedisAccessControlService(string storageUri) + { + var redis = ConnectionMultiplexer.Connect(storageUri); + _db = redis.GetDatabase(); + } + + public int? GetTxQuota(Address address) + { + RedisValue result = _db.StringGet(address.ToString()); + if (!result.IsNull) + { + Log.ForContext("Source", nameof(IAccessControlService)) + .Debug("\"{Address}\" Tx Quota: {Quota}", address, result); + return Convert.ToInt32(result); + } + + return null; + } + } +} diff --git a/NineChronicles.Headless/Services/SQLiteAccessControlService.cs b/NineChronicles.Headless/Services/SQLiteAccessControlService.cs new file mode 100644 index 000000000..f3e7263c8 --- /dev/null +++ b/NineChronicles.Headless/Services/SQLiteAccessControlService.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.Data.Sqlite; +using Libplanet.Crypto; +using Nekoyume.Blockchain; +using Serilog; + +namespace NineChronicles.Headless.Services +{ + public class SQLiteAccessControlService : IAccessControlService + { + private const string CreateTableSql = + "CREATE TABLE IF NOT EXISTS txquotalist (address VARCHAR(42), quota INT)"; + private const string GetTxQuotaSql = + "SELECT quota FROM txquotalist WHERE address=@Address"; + + protected readonly string _connectionString; + + public SQLiteAccessControlService(string connectionString) + { + _connectionString = connectionString; + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + + using var command = connection.CreateCommand(); + command.CommandText = CreateTableSql; + command.ExecuteNonQuery(); + } + + public int? GetTxQuota(Address address) + { + using var connection = new SqliteConnection(_connectionString); + connection.Open(); + + using var command = connection.CreateCommand(); + command.CommandText = GetTxQuotaSql; + command.Parameters.AddWithValue("@Address", address.ToString()); + + var queryResult = command.ExecuteScalar(); + + if (queryResult != null) + { + return Convert.ToInt32(queryResult); + } + + return null; + } + } +} diff --git a/NineChronicles.RPC.Shared b/NineChronicles.RPC.Shared index 2dcbbb19a..cff68421f 160000 --- a/NineChronicles.RPC.Shared +++ b/NineChronicles.RPC.Shared @@ -1 +1 @@ -Subproject commit 2dcbbb19a0c90f3f41de03506802bbd527c0aaba +Subproject commit cff68421fabb098f3d0bfd20fdef55755db309e4 diff --git a/README.md b/README.md index 28430b15f..08982cd5a 100644 --- a/README.md +++ b/README.md @@ -3,166 +3,16 @@ [![Planetarium Discord invite](https://img.shields.io/discord/539405872346955788?color=6278DA&label=Planetarium&logo=discord&logoColor=white)](https://discord.gg/JyujU8E4SD) [![Planetarium-Dev Discord Invite](https://img.shields.io/discord/928926944937013338?color=6278DA&label=Planetarium-dev&logo=discord&logoColor=white)](https://discord.gg/RYJDyFRYY7) -## Table of Contents - -- [Run](#run) -- [Docker Build](#docker-build) - * [Format](#format) -- [How to run NineChronicles Headless on AWS EC2 instance using Docker](#how-to-run-ninechronicles-headless-on-aws-ec2-instance-using-docker) - * [On Your AWS EC2 Instance](#on-your-aws-ec2-instance) - * [Building Your Own Docker Image from Your Local Machine](#building-your-own-docker-image-from-your-local-machine) -- [Nine Chronicles GraphQL API Documentation](#nine-chronicles-graphql-api-documentation) -- [Create A New Genesis Block](#create-a-new-genesis-block) - ## Run +If you want to run node to interact with mainnet, you can run the below command line: + ``` -$ dotnet run --project ./NineChronicles.Headless.Executable/ -- --help - -Usage: NineChronicles.Headless.Executable [command] -Usage: NineChronicles.Headless.Executable - -// basic -[--app-protocol-version ] -[--trusted-app-protocol-version-signer ...] -[--genesis-block-path ] -[--host ] -[--port ] -[--swarm-private-key ] - -// Policy -[--skip-preload] -[--chain-tip-stale-behavior-type ] -[--confirmations ] -[--tx-life-time ] -[--message-timeout ] -[--tip-timeout ] -[--demand-buffer ] -[--tx-quota-per-signer ] -[--maximum-poll-peers ] - -// Store -[--store-type ] -[--store-path ] -[--no-reduce-store] - -// Network -[--network-type ] -[--ice-server ...] -[--peer ...] -[--static-peer ...] -[--minimum-broadcast-target ] -[--bucket-size ] - -// render -[--nonblock-renderer] -[--nonblock-renderer-queue ] -[--strict-rendering] -[--log-action-renders] - -// consensus -[--consensus-port ] -[--consensus-private-key ] -[--consensus-seed ...] - -// RPC -[--rpc-server] -[--rpc-listen-host ] -[--rpc-listen-port ] -[--rpc-remote-server] -[--rpc-http-server] - -// GraphQL -[--graphql-server] -[--graphql-host ] -[--graphql-port ] -[--graphql-secret-token-path ] -[--no-cors] - -// Sentry -[--sentry-dsn ] -[--sentry-trace-sample-rate ] - -// ETC -[--config ] -[--help] -[--version] - -// Miner (Deprecated) -[--no-miner] -[--miner-count ] -[--miner-private-key ] -[--miner.block-interval ] - - -NineChronicles.Headless.Executable - -Commands: - account - validation - chain - key - apv - action - state - tx - market - genesis - replay - -Options: - -V, --app-protocol-version App protocol version token. - -G, --genesis-block-path Genesis block path of blockchain. Blockchain is recognized by its genesis block. - -H, --host Hostname of this node for another nodes to access. This is not listening host like 0.0.0.0 - -P, --port Port of this node for another nodes to access. - --swarm-private-key The private key used for signing messages and to specify your node. If you leave this null, a randomly generated value will be used. - --no-miner Disable block mining. - --miner-count The number of miner task(thread). - --miner-private-key The private key used for mining blocks. Must not be null if you want to turn on mining with libplanet-node. - --miner.block-interval The miner's break time after mining a block. The unit is millisecond. - --store-type The type of storage to store blockchain data. If not provided, "LiteDB" will be used as default. Available type: ["rocksdb", "memory"] - --store-path Path of storage. This value is required if you use persistent storage e.g. "rocksdb" - --no-reduce-store Do not reduce storage. Enabling this option will use enormous disk spaces. - -I, --ice-server ... ICE server to NAT traverse. - --peer ... Seed peer list to communicate to another nodes. - -T, --trusted-app-protocol-version-signer ... Trustworthy signers who claim new app protocol versions - --rpc-server Use this option if you want to make unity clients to communicate with this server with RPC - --rpc-listen-host RPC listen host - --rpc-listen-port RPC listen port - --rpc-remote-server Do a role as RPC remote server? If you enable this option, multiple Unity clients can connect to your RPC server. - --rpc-http-server If you enable this option with "rpcRemoteServer" option at the same time, RPC server will use HTTP/1, not gRPC. - --graphql-server Use this option if you want to enable GraphQL server to enable querying data. - --graphql-host GraphQL listen host - --graphql-port GraphQL listen port - --graphql-secret-token-path The path to write GraphQL secret token. If you want to protect this headless application, you should use this option and take it into headers. - --no-cors Run without CORS policy. - --confirmations The number of required confirmations to recognize a block. - --nonblock-renderer Uses non-blocking renderer, which prevents the blockchain & swarm from waiting slow rendering. Turned off by default. - --nonblock-renderer-queue The size of the queue used by the non-blocking renderer. Ignored if --nonblock-renderer is turned off. - --strict-rendering Flag to turn on validating action renderer. - --log-action-renders Log action renders besides block renders. --rpc-server implies this. - --network-type Network type. (Allowed values: Main, Internal, Permanent, Test, Default) - --tx-life-time The lifetime of each transaction, which uses minute as its unit. - --message-timeout The grace period for new messages, which uses second as its unit. - --tip-timeout The grace period for tip update, which uses second as its unit. - --demand-buffer A number of block size that determines how far behind the demand the tip of the chain will publish `NodeException` to GraphQL subscriptions. - --static-peer ... A list of peers that the node will continue to maintain. - --skip-preload Run node without preloading. - --minimum-broadcast-target Minimum number of peers to broadcast message. - --bucket-size Number of the peers can be stored in each bucket. - --chain-tip-stale-behavior-type Determines behavior when the chain's tip is stale. "reboot" and "preload" is available and "reboot" option is selected by default. - --tx-quota-per-signer The number of maximum transactions can be included in stage per signer. - --maximum-poll-peers The maximum number of peers to poll blocks. int.MaxValue by default. - --consensus-port Port used for communicating consensus related messages. null by default. - --consensus-private-key The private key used for signing consensus messages. Cannot be null. - --consensus-seed ... A list of seed peers to join the block consensus. - -C, --config Absolute path of "appsettings.json" file to provide headless configurations. (Default: appsettings.json) - --sentry-dsn Sentry DSN - --sentry-trace-sample-rate Trace sample rate for sentry - -h, --help Show help message - --version Show version +dotnet run --project NineChronicles.Headless.Executable -C appsettings.mainnet.json --store-path={PATH_TO_STORE} ``` +For more information on the command line options, refer to the [CLI Documentation](https://planetarium.github.io/NineChronicles.Headless/cli). + ### Use `appsettings.{network}.json` to provide CLI options You can provide headless CLI options using file, `appsettings.json`. You'll find the default file at [here](NineChronicles.Headless.Executable/appsettings.json). @@ -286,7 +136,7 @@ $ docker push [/]:[] Check out [Nine Chronicles GraphQL API Tutorial](https://www.notion.so/Getting-Started-with-Nine-Chronicles-GraphQL-API-a14388a910844a93ab8dc0a2fe269f06) to get you started with using GraphQL API with NineChronicles Headless. -For more information on the GraphQL API, refer to the [NineChronicles Headless GraphQL Documentation](http://api.nine-chronicles.com/). +For more information on the GraphQL API, refer to the [NineChronicles Headless GraphQL Documentation](https://planetarium.github.io/NineChronicles.Headless/graphql). ---