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
+
+
+
+
+
+
+
+
+
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).
---