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/NineChronicles.Headless.AccessControlCenter/AccessControlService/IMutableAccessControlService.cs b/NineChronicles.Headless.AccessControlCenter/AccessControlService/IMutableAccessControlService.cs
new file mode 100644
index 000000000..aa8207174
--- /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 DenyAccess(Address address);
+ void AllowAccess(Address address);
+ List
ListBlockedAddresses(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..89c614a0a
--- /dev/null
+++ b/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableRedisAccessControlService.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Linq;
+using Libplanet.Crypto;
+using NineChronicles.Headless.Services;
+
+namespace NineChronicles.Headless.AccessControlCenter.AccessControlService
+{
+ public class MutableRedisAccessControlService : RedisAccessControlService, IMutableAccessControlService
+ {
+ public MutableRedisAccessControlService(string storageUri) : base(storageUri)
+ {
+ }
+
+ public void DenyAccess(Address address)
+ {
+ _db.StringSet(address.ToString(), "denied");
+ }
+
+ public void AllowAccess(Address address)
+ {
+ _db.KeyDelete(address.ToString());
+ }
+
+ public List ListBlockedAddresses(int offset, int limit)
+ {
+ var server = _db.Multiplexer.GetServer(_db.Multiplexer.GetEndPoints().First());
+ return server
+ .Keys()
+ .Select(k => new Address(k.ToString()))
+ .Skip(offset)
+ .Take(limit)
+ .ToList();
+ }
+ }
+}
diff --git a/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableSqliteAccessControlService.cs b/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableSqliteAccessControlService.cs
new file mode 100644
index 000000000..1d9455118
--- /dev/null
+++ b/NineChronicles.Headless.AccessControlCenter/AccessControlService/MutableSqliteAccessControlService.cs
@@ -0,0 +1,61 @@
+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 DenyAccessSql =
+ "INSERT OR IGNORE INTO blocklist (address) VALUES (@Address)";
+ private const string AllowAccessSql = "DELETE FROM blocklist WHERE address=@Address";
+
+ public MutableSqliteAccessControlService(string connectionString) : base(connectionString)
+ {
+ }
+
+ public void DenyAccess(Address address)
+ {
+ using var connection = new SqliteConnection(_connectionString);
+ connection.Open();
+
+ using var command = connection.CreateCommand();
+ command.CommandText = DenyAccessSql;
+ command.Parameters.AddWithValue("@Address", address.ToString());
+ command.ExecuteNonQuery();
+ }
+
+ public void AllowAccess(Address address)
+ {
+ using var connection = new SqliteConnection(_connectionString);
+ connection.Open();
+
+ using var command = connection.CreateCommand();
+ command.CommandText = AllowAccessSql;
+ command.Parameters.AddWithValue("@Address", address.ToString());
+ command.ExecuteNonQuery();
+ }
+
+ public List ListBlockedAddresses(int offset, int limit)
+ {
+ var blockedAddresses = new List();
+
+ using var connection = new SqliteConnection(_connectionString);
+ connection.Open();
+
+ using var command = connection.CreateCommand();
+ command.CommandText = $"SELECT address FROM blocklist LIMIT @Limit OFFSET @Offset";
+ command.Parameters.AddWithValue("@Limit", limit);
+ command.Parameters.AddWithValue("@Offset", offset);
+
+ using var reader = command.ExecuteReader();
+ while (reader.Read())
+ {
+ blockedAddresses.Add(new Address(reader.GetString(0)));
+ }
+
+ return blockedAddresses;
+ }
+ }
+}
diff --git a/NineChronicles.Headless.AccessControlCenter/AcsService.cs b/NineChronicles.Headless.AccessControlCenter/AcsService.cs
new file mode 100644
index 000000000..596d54fad
--- /dev/null
+++ b/NineChronicles.Headless.AccessControlCenter/AcsService.cs
@@ -0,0 +1,80 @@
+using System;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using NineChronicles.Headless.AccessControlCenter.AccessControlService;
+
+namespace NineChronicles.Headless.AccessControlCenter
+{
+ public class AcsService
+ {
+ public AcsService(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();
+
+ var accessControlService = MutableAccessControlServiceFactory.Create(
+ Enum.Parse(
+ Configuration.AccessControlServiceType,
+ true
+ ),
+ Configuration.AccessControlServiceConnectionString
+ );
+
+ services.AddSingleton(accessControlService);
+ }
+
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ if (env.IsDevelopment())
+ {
+ app.UseDeveloperExceptionPage();
+ }
+
+ app.UseRouting();
+ app.UseAuthorization();
+
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapControllers();
+ });
+ }
+ }
+ }
+}
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..685dca35e
--- /dev/null
+++ b/NineChronicles.Headless.AccessControlCenter/Controllers/AccessControlServiceController.cs
@@ -0,0 +1,48 @@
+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 IsAccessDenied(string address)
+ {
+ return _accessControlService.IsAccessDenied(new Address(address));
+ }
+
+ [HttpPost("entries/{address}/deny")]
+ public ActionResult DenyAccess(string address)
+ {
+ _accessControlService.DenyAccess(new Address(address));
+ return Ok();
+ }
+
+ [HttpPost("entries/{address}/allow")]
+ public ActionResult AllowAccess(string address)
+ {
+ _accessControlService.AllowAccess(new Address(address));
+ return Ok();
+ }
+
+ [HttpGet("entries")]
+ public ActionResult> ListBlockedAddresses(int offset, int limit)
+ {
+ return _accessControlService
+ .ListBlockedAddresses(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..74be55c3b
--- /dev/null
+++ b/NineChronicles.Headless.AccessControlCenter/NineChronicles.Headless.AccessControlCenter.csproj
@@ -0,0 +1,32 @@
+
+
+ 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..ee262e673
--- /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 AcsService(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.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/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/Program.cs b/NineChronicles.Headless.Executable/Program.cs
index 7db80836c..731fb0380 100644
--- a/NineChronicles.Headless.Executable/Program.cs
+++ b/NineChronicles.Headless.Executable/Program.cs
@@ -436,7 +436,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,
diff --git a/NineChronicles.Headless.Tests/GraphQLStartupTest.cs b/NineChronicles.Headless.Tests/GraphQLStartupTest.cs
index f582243e0..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
{
diff --git a/NineChronicles.Headless.Tests/GraphTypes/StandaloneSubscriptionTest.cs b/NineChronicles.Headless.Tests/GraphTypes/StandaloneSubscriptionTest.cs
index 419ec81f7..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));
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 b265edf87..181f14e6e 100644
--- a/NineChronicles.Headless/NineChroniclesNodeService.cs
+++ b/NineChronicles.Headless/NineChroniclesNodeService.cs
@@ -20,6 +20,7 @@
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;
@@ -78,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();
@@ -201,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");
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..b1dfb5e74
--- /dev/null
+++ b/NineChronicles.Headless/Services/RedisAccessControlService.cs
@@ -0,0 +1,22 @@
+using StackExchange.Redis;
+using Libplanet.Crypto;
+using Nekoyume.Blockchain;
+
+namespace NineChronicles.Headless.Services
+{
+ public class RedisAccessControlService : IAccessControlService
+ {
+ protected IDatabase _db;
+
+ public RedisAccessControlService(string storageUri)
+ {
+ var redis = ConnectionMultiplexer.Connect(storageUri);
+ _db = redis.GetDatabase();
+ }
+
+ public bool IsAccessDenied(Address address)
+ {
+ return _db.KeyExists(address.ToString());
+ }
+ }
+}
diff --git a/NineChronicles.Headless/Services/SQLiteAccessControlService.cs b/NineChronicles.Headless/Services/SQLiteAccessControlService.cs
new file mode 100644
index 000000000..0a9c1e456
--- /dev/null
+++ b/NineChronicles.Headless/Services/SQLiteAccessControlService.cs
@@ -0,0 +1,41 @@
+using Microsoft.Data.Sqlite;
+using Libplanet.Crypto;
+using Nekoyume.Blockchain;
+
+namespace NineChronicles.Headless.Services
+{
+ public class SQLiteAccessControlService : IAccessControlService
+ {
+ private const string CreateTableSql =
+ "CREATE TABLE IF NOT EXISTS blocklist (address VARCHAR(42))";
+ private const string CheckAccessSql =
+ "SELECT EXISTS(SELECT 1 FROM blocklist 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 bool IsAccessDenied(Address address)
+ {
+ using var connection = new SqliteConnection(_connectionString);
+ connection.Open();
+
+ using var command = connection.CreateCommand();
+ command.CommandText = CheckAccessSql;
+ command.Parameters.AddWithValue("@Address", address.ToString());
+
+ var result = command.ExecuteScalar();
+
+ return result is not null && (long)result == 1;
+ }
+ }
+}
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