diff --git a/sdk/node/Libplanet.Node.Executable/Libplanet.Node.Executable.csproj b/sdk/node/Libplanet.Node.Executable/Libplanet.Node.Executable.csproj
index d43112eaed4..620462b7549 100644
--- a/sdk/node/Libplanet.Node.Executable/Libplanet.Node.Executable.csproj
+++ b/sdk/node/Libplanet.Node.Executable/Libplanet.Node.Executable.csproj
@@ -12,6 +12,7 @@
+
@@ -21,6 +22,7 @@
+
diff --git a/sdk/node/Libplanet.Node.Executable/Program.cs b/sdk/node/Libplanet.Node.Executable/Program.cs
index 53ccd302d64..616209efe80 100644
--- a/sdk/node/Libplanet.Node.Executable/Program.cs
+++ b/sdk/node/Libplanet.Node.Executable/Program.cs
@@ -3,6 +3,8 @@
using Libplanet.Node.Options.Schema;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Serilog;
+using Serilog.Events;
var builder = WebHost.CreateDefaultBuilder(args);
var assemblies = new string[]
@@ -14,6 +16,16 @@
builder.ConfigureLogging(logging =>
{
logging.AddConsole();
+
+ // Logging setting
+ var loggerConfig = new LoggerConfiguration();
+ loggerConfig = loggerConfig.MinimumLevel.Information();
+ loggerConfig = loggerConfig
+ .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
+ .Enrich.FromLogContext()
+ .WriteTo.Console();
+
+ Log.Logger = loggerConfig.CreateLogger();
});
builder.ConfigureKestrel((context, options) =>
{
@@ -26,6 +38,19 @@
});
builder.ConfigureServices((context, services) =>
{
+// string pluginPath = "/Users/bin_bash_shell/Workspaces/planetarium/NineChronicles/" +
+// "lib9c/Lib9c.NCActionLoader/bin/Debug/net6.0/Lib9c.NCActionLoader.dll";
+// string actionLoaderType = "Lib9c.NCActionLoader.NineChroniclesActionLoader";
+// string blockPolicyType = "Lib9c.NCActionLoader.NineChroniclesPolicyActionRegistry";
+// IActionLoader actionLoader = PluginLoader.LoadActionLoader(pluginPath, actionLoaderType);
+// IPolicyActionsRegistry policyActionRegistry =
+// PluginLoader.LoadPolicyActionRegistry(pluginPath, blockPolicyType);
+
+// Libplanet.Crypto.CryptoConfig.CryptoBackend = new Secp256k1CryptoBackend();
+
+// builder.Services.AddSingleton(actionLoader);
+// builder.Services.AddSingleton(policyActionRegistry);
+
services.AddGrpc();
services.AddGrpcReflection();
services.AddLibplanetNode(context.Configuration);
diff --git a/sdk/node/Libplanet.Node.Executable/appsettings-schema.json b/sdk/node/Libplanet.Node.Executable/appsettings-schema.json
index 7220e925481..21b22236f82 100644
--- a/sdk/node/Libplanet.Node.Executable/appsettings-schema.json
+++ b/sdk/node/Libplanet.Node.Executable/appsettings-schema.json
@@ -1106,6 +1106,19 @@
}
}
},
+ "Action": {
+ "title": "ActionOptions",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "ActionLoaderPath": {
+ "type": "string"
+ },
+ "ActionLoaderType": {
+ "type": "string"
+ }
+ }
+ },
"Genesis": {
"title": "GenesisOptions",
"type": "object",
@@ -1114,12 +1127,12 @@
"properties": {
"GenesisKey": {
"type": "string",
- "description": "The key of the genesis block.",
+ "description": "The PrivateKey used to generate the genesis block. This property cannot be used with GenesisBlockPath.",
"pattern": "^[0-9a-fA-F]{64}$"
},
"Validators": {
"type": "array",
- "description": "Public keys of the validators.",
+ "description": "Public keys of the validators. This property cannot be used with GenesisBlockPath.",
"items": {
"type": "string"
}
@@ -1131,7 +1144,7 @@
},
"GenesisBlockPath": {
"type": "string",
- "description": "The path of the genesis block."
+ "description": "The path of the genesis block, which can be a file path or a URI.This property cannot be used with GenesisKey."
}
}
},
@@ -1221,6 +1234,12 @@
"type": "string",
"description": "The endpoint of the node to block sync.",
"pattern": "^$|^[0-9a-fA-F]{130}|[0-9a-fA-F]{66},\\s*(?:(?:[a-zA-Z0-9\\-\\.]+)|(?:\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})):\\d{1,5}$"
+ },
+ "TrustedAppProtocolVersionSigners": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
}
}
},
@@ -1260,6 +1279,10 @@
"description": "Type 'ExplorerOptions' does not have a description.",
"$ref": "#/definitions/Explorer"
},
+ "Action": {
+ "description": "Type 'ActionOptions' does not have a description.",
+ "$ref": "#/definitions/Action"
+ },
"Genesis": {
"description": "Options for the genesis block.",
"$ref": "#/definitions/Genesis"
diff --git a/sdk/node/Libplanet.Node.Executable/appsettings.Development.json b/sdk/node/Libplanet.Node.Executable/appsettings.Development.json
index 734f125c83e..c31976e21e6 100644
--- a/sdk/node/Libplanet.Node.Executable/appsettings.Development.json
+++ b/sdk/node/Libplanet.Node.Executable/appsettings.Development.json
@@ -16,9 +16,17 @@
},
"Swarm": {
"IsEnabled": true,
- "AppProtocolVersion": "200210/AB2da648b9154F2cCcAFBD85e0Bc3d51f97330Fc/MEUCIQCBr..8VdITFe9nMTobl4akFid.s8G2zy2pBidAyRXSeAIgER77qX+eywjgyth6QYi7rQw5nK3KXO6cQ6ngUh.CyfU=/ZHU5OnRpbWVzdGFtcHUxMDoyMDI0LTA3LTMwZQ=="
+ "AppProtocolVersion": "200210/AB2da648b9154F2cCcAFBD85e0Bc3d51f97330Fc/MEUCIQCBr..8VdITFe9nMTobl4akFid.s8G2zy2pBidAyRXSeAIgER77qX+eywjgyth6QYi7rQw5nK3KXO6cQ6ngUh.CyfU=/ZHU5OnRpbWVzdGFtcHUxMDoyMDI0LTA3LTMwZQ==",
+ "BlocksyncSeedPeer": "027bd36895d68681290e570692ad3736750ceaab37be402442ffb203967f98f7b6,9c-main-tcp-seed-1.planetarium.dev:31234"
},
- "Validator": {
- "IsEnabled": true
+ "Store": {
+ "Type": 0,
+ "RootPath": "/Users/jeesu/Projects/Storage/Chain",
+ "StoreName": "9c-main-snapshot-slim",
+ "StateStoreName": "9c-main-snapshot-slim/states"
+ },
+ "Action": {
+ "ActionLoaderPath": "/Users/jeesu/Downloads/osx-arm64/Lib9c.dll",
+ "ActionLoaderType": "Nekoyume.Action.Loader.NCActionLoader"
}
}
diff --git a/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs b/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs
index df39886e39e..e98423c2a99 100644
--- a/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs
+++ b/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs
@@ -24,6 +24,9 @@ public static ILibplanetNodeBuilder AddLibplanetNode(
.Bind(configuration.GetSection(StoreOptions.Position));
services.AddSingleton, StoreOptionsConfigurator>();
+ services.AddOptions()
+ .Bind(configuration.GetSection(ActionOptions.Position));
+
services.AddOptions()
.Bind(configuration.GetSection(SwarmOptions.Position));
services.AddSingleton, SwarmOptionsConfigurator>();
diff --git a/sdk/node/Libplanet.Node.Tests/Services/BlockChainServiceTest.cs b/sdk/node/Libplanet.Node.Tests/Services/BlockChainServiceTest.cs
index dd8461585ed..989f8712980 100644
--- a/sdk/node/Libplanet.Node.Tests/Services/BlockChainServiceTest.cs
+++ b/sdk/node/Libplanet.Node.Tests/Services/BlockChainServiceTest.cs
@@ -1,3 +1,5 @@
+using Libplanet.Action;
+using Libplanet.Action.Loader;
using Libplanet.Blockchain;
using Libplanet.Crypto;
using Libplanet.Node.Options;
@@ -20,6 +22,7 @@ public void Create_Test()
services.AddSingleton, GenesisOptionsConfigurator>();
services.AddOptions();
services.AddSingleton, StoreOptionsConfigurator>();
+ services.AddOptions();
services.AddOptions();
services.AddSingleton, SwarmOptionsConfigurator>();
@@ -28,11 +31,12 @@ public void Create_Test()
var logger = new NullLoggerFactory().CreateLogger();
var genesisOptions = serviceProvider.GetRequiredService>();
var storeOptions = serviceProvider.GetRequiredService>();
+ var actionOptions = serviceProvider.GetRequiredService>();
var blockChainService = new BlockChainService(
genesisOptions: genesisOptions,
storeOptions: storeOptions,
+ actionOptions: actionOptions,
policyService: policyService,
- actionLoaderProviders: [],
logger: logger);
var blockChain = blockChainService.BlockChain;
diff --git a/sdk/node/Libplanet.Node/Actions/PluginLoadContext.cs b/sdk/node/Libplanet.Node/Actions/PluginLoadContext.cs
new file mode 100644
index 00000000000..91c1b4c7d7f
--- /dev/null
+++ b/sdk/node/Libplanet.Node/Actions/PluginLoadContext.cs
@@ -0,0 +1,31 @@
+using System.Reflection;
+using System.Runtime.Loader;
+
+namespace Libplanet.Node.Actions;
+
+public class PluginLoadContext(string pluginPath) : AssemblyLoadContext
+{
+ private readonly AssemblyDependencyResolver _resolver = new(pluginPath);
+
+ protected override Assembly? Load(AssemblyName assemblyName)
+ {
+ var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
+ if (assemblyPath is not null)
+ {
+ return LoadFromAssemblyPath(assemblyPath);
+ }
+
+ return null;
+ }
+
+ protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
+ {
+ var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
+ if (libraryPath is not null)
+ {
+ return LoadUnmanagedDllFromPath(libraryPath);
+ }
+
+ return IntPtr.Zero;
+ }
+}
diff --git a/sdk/node/Libplanet.Node/Actions/PluginLoader.cs b/sdk/node/Libplanet.Node/Actions/PluginLoader.cs
new file mode 100644
index 00000000000..9d54bf4ba44
--- /dev/null
+++ b/sdk/node/Libplanet.Node/Actions/PluginLoader.cs
@@ -0,0 +1,89 @@
+using System.Reflection;
+using Libplanet.Action;
+using Libplanet.Action.Loader;
+
+namespace Libplanet.Node.Actions;
+
+public static class PluginLoader
+{
+ public static IActionLoader LoadActionLoader(string relativePath, string typeName)
+ {
+ Assembly assembly = LoadPlugin(relativePath);
+ IEnumerable loaders = Create(assembly);
+ foreach (IActionLoader loader in loaders)
+ {
+ if (loader.GetType().FullName == typeName)
+ {
+ return loader;
+ }
+ }
+
+ throw new ApplicationException(
+ $"Can't find {typeName} in {assembly} from {assembly.Location}. " +
+ $"Available types: {string
+ .Join(",", loaders.Select(x => x.GetType().FullName))}");
+ }
+
+ public static IPolicyActionsRegistry LoadPolicyActionRegistry(
+ string relativePath,
+ string typeName)
+ {
+ Assembly assembly = LoadPlugin(relativePath);
+ IEnumerable policies = Create(assembly);
+ foreach (IPolicyActionsRegistry policy in policies)
+ {
+ if (policy.GetType().FullName == typeName)
+ {
+ return policy;
+ }
+ }
+
+ throw new ApplicationException(
+ $"Can't find {typeName} in {assembly} from {assembly.Location}. " +
+ $"Available types: {string
+ .Join(",", policies.Select(x => x.GetType().FullName))}");
+ }
+
+ private static IEnumerable Create(Assembly assembly)
+ where T : class
+ {
+ int count = 0;
+
+ foreach (Type type in assembly.GetTypes())
+ {
+ if (typeof(T).IsAssignableFrom(type))
+ {
+ if (Activator.CreateInstance(type) is T result)
+ {
+ count++;
+ yield return result;
+ }
+ }
+ }
+
+ if (count == 0)
+ {
+ string availableTypes = string.Join(",", assembly.GetTypes().Select(t => t.FullName));
+ throw new ApplicationException(
+ $"Can't find any type which implements ICommand in {assembly} from {assembly.Location}.\n" +
+ $"Available types: {availableTypes}");
+ }
+ }
+
+ private static Assembly LoadPlugin(string relativePath)
+ {
+ // Navigate up to the solution root
+ // string root = Path.GetFullPath(Path.Combine(
+ // Path.GetDirectoryName(
+ // Path.GetDirectoryName(
+ // Path.GetDirectoryName(
+ // Path.GetDirectoryName(
+ // Path.GetDirectoryName(typeof(Program).Assembly.Location)))))));
+
+ // string pluginLocation = Path.GetFullPath(Path.Combine(root, relativePath.Replace('\\', Path.DirectorySeparatorChar)));
+ string pluginLocation = relativePath;
+ Console.WriteLine($"Loading commands from: {pluginLocation}");
+ PluginLoadContext loadContext = new PluginLoadContext(pluginLocation);
+ return loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(pluginLocation)));
+ }
+}
diff --git a/sdk/node/Libplanet.Node/Options/ActionOptions.cs b/sdk/node/Libplanet.Node/Options/ActionOptions.cs
new file mode 100644
index 00000000000..7f1506b1482
--- /dev/null
+++ b/sdk/node/Libplanet.Node/Options/ActionOptions.cs
@@ -0,0 +1,28 @@
+using Libplanet.Action;
+using Libplanet.Action.Loader;
+using Libplanet.Node.Actions;
+
+namespace Libplanet.Node.Options;
+
+[Options(Position)]
+public sealed class ActionOptions : OptionsBase
+{
+ public const string Position = "Action";
+
+ public string ActionLoaderPath { get; set; } = string.Empty;
+
+ public string ActionLoaderType { get; set; } = string.Empty;
+
+ internal IActionLoader GetActionLoader()
+ {
+ var actionLoaderPath = ActionLoaderPath;
+ var actionLoaderType = ActionLoaderType;
+ return PluginLoader.LoadActionLoader(actionLoaderPath, actionLoaderType);
+ }
+
+ internal IPolicyActionsRegistry GetPolicyActionsRegistry()
+ {
+ // PluginLoader.LoadPolicyActionRegistry(pluginPath, blockPolicyType);
+ return new PolicyActionsRegistry();
+ }
+}
diff --git a/sdk/node/Libplanet.Node/Options/SwarmOptions.cs b/sdk/node/Libplanet.Node/Options/SwarmOptions.cs
index 215b8223202..7728096a246 100644
--- a/sdk/node/Libplanet.Node/Options/SwarmOptions.cs
+++ b/sdk/node/Libplanet.Node/Options/SwarmOptions.cs
@@ -24,4 +24,8 @@ public sealed class SwarmOptions : OptionsBase
[BoundPeer]
[Description("The endpoint of the node to block sync.")]
public string BlocksyncSeedPeer { get; set; } = string.Empty;
+
+ // 030ffa9bd579ee1503ce008394f687c182279da913bfaec12baca34e79698a7cd1
+ [PublicKeyArray]
+ public string[] TrustedAppProtocolVersionSigners { get; set; } = [];
}
diff --git a/sdk/node/Libplanet.Node/Services/BlockChainService.cs b/sdk/node/Libplanet.Node/Services/BlockChainService.cs
index 2f528bce875..126e2563b1e 100644
--- a/sdk/node/Libplanet.Node/Services/BlockChainService.cs
+++ b/sdk/node/Libplanet.Node/Services/BlockChainService.cs
@@ -37,8 +37,8 @@ internal sealed class BlockChainService : IBlockChainService, IActionRenderer
public BlockChainService(
IOptions genesisOptions,
IOptions storeOptions,
+ IOptions actionOptions,
PolicyService policyService,
- IEnumerable actionLoaderProviders,
ILogger logger)
{
_synchronizationContext = SynchronizationContext.Current ?? new();
@@ -46,9 +46,9 @@ public BlockChainService(
_blockChain = CreateBlockChain(
genesisOptions: genesisOptions.Value,
storeOptions: storeOptions.Value,
+ actionOptions: actionOptions.Value,
stagePolicy: policyService.StagePolicy,
- renderers: [this],
- actionLoaders: [.. actionLoaderProviders.Select(item => item.GetActionLoader())]);
+ renderers: [this]);
}
public event EventHandler? BlockAppended;
@@ -84,7 +84,7 @@ void Action(object? state)
}
}
- _logger.LogInformation("#{Height}: Block appended.", newTip.Index);
+ _logger.LogInformation("#{Height}: Block appended", newTip.Index);
BlockAppended?.Invoke(this, new(newTip));
}
}
@@ -92,22 +92,31 @@ void Action(object? state)
private static BlockChain CreateBlockChain(
GenesisOptions genesisOptions,
StoreOptions storeOptions,
+ ActionOptions actionOptions,
IStagePolicy stagePolicy,
- IRenderer[] renderers,
- IActionLoader[] actionLoaders)
+ IRenderer[] renderers)
{
+ var actionLoader = actionOptions.GetActionLoader();
+ var policyActions = new PolicyActionsRegistry();
+
var (store, stateStore) = CreateStore(storeOptions);
- var actionLoader = new AggregateTypedActionLoader(actionLoaders);
var actionEvaluator = new ActionEvaluator(
- policyActionsRegistry: new(),
+ policyActionsRegistry: policyActions,
stateStore,
actionLoader);
var genesisBlock = CreateGenesisBlock(genesisOptions);
var policy = new BlockPolicy(
- blockInterval: TimeSpan.FromSeconds(10),
- getMaxTransactionsPerBlock: _ => int.MaxValue,
- getMaxTransactionsBytes: _ => long.MaxValue);
+ policyActionsRegistry: policyActions,
+ blockInterval: TimeSpan.FromSeconds(8),
+ validateNextBlockTx: (chain, transaction) => null,
+ validateNextBlock: (chain, block) => null,
+ getMaxTransactionsBytes: l => long.MaxValue,
+ getMinTransactionsPerBlock: l => 0,
+ getMaxTransactionsPerBlock: l => int.MaxValue,
+ getMaxTransactionsPerSignerPerBlock: l => int.MaxValue
+ );
+
var blockChainStates = new BlockChainStates(store, stateStore);
if (store.GetCanonicalChainId() is null)
{
diff --git a/sdk/node/Libplanet.Node/Services/SwarmService.cs b/sdk/node/Libplanet.Node/Services/SwarmService.cs
index 1c9f99c4937..7632a00c726 100644
--- a/sdk/node/Libplanet.Node/Services/SwarmService.cs
+++ b/sdk/node/Libplanet.Node/Services/SwarmService.cs
@@ -1,3 +1,4 @@
+using System.Collections.Immutable;
using System.Net;
using Libplanet.Common;
using Libplanet.Crypto;
@@ -75,11 +76,14 @@ public async Task StartAsync(CancellationToken cancellationToken)
var nodeOptions = _options;
var privateKey = PrivateKey.FromString(nodeOptions.PrivateKey);
var appProtocolVersion = AppProtocolVersion.FromToken(nodeOptions.AppProtocolVersion);
+ var trustedAppProtocolVersionSigners = nodeOptions.TrustedAppProtocolVersionSigners
+ .Select(PublicKey.FromHex).ToArray();
var swarmEndPoint = (DnsEndPoint)EndPointUtility.Parse(nodeOptions.EndPoint);
var swarmTransport = await CreateTransport(
privateKey: privateKey,
endPoint: swarmEndPoint,
- appProtocolVersion: appProtocolVersion);
+ appProtocolVersion: appProtocolVersion,
+ trustedAppProtocolVersionSigners);
var blocksyncSeedPeer = BoundPeerUtility.Parse(nodeOptions.BlocksyncSeedPeer);
var swarmOptions = new Net.Options.SwarmOptions
{
@@ -92,7 +96,11 @@ public async Task StartAsync(CancellationToken cancellationToken)
var consensusTransport = validatorOptions is not null
? await CreateConsensusTransportAsync(
- privateKey, appProtocolVersion, validatorOptions, cancellationToken)
+ privateKey,
+ appProtocolVersion,
+ trustedAppProtocolVersionSigners,
+ validatorOptions,
+ cancellationToken)
: null;
var consensusReactorOption = validatorOptions is not null
? CreateConsensusReactorOption(privateKey, validatorOptions)
@@ -179,14 +187,22 @@ public async ValueTask DisposeAsync()
}
private static async Task CreateTransport(
- PrivateKey privateKey, DnsEndPoint endPoint, AppProtocolVersion appProtocolVersion)
+ PrivateKey privateKey,
+ DnsEndPoint endPoint,
+ AppProtocolVersion appProtocolVersion,
+ PublicKey[] trustedAppProtocolVersionSigners)
{
var appProtocolVersionOptions = new Net.Options.AppProtocolVersionOptions
{
AppProtocolVersion = appProtocolVersion,
+ TrustedAppProtocolVersionSigners = [.. trustedAppProtocolVersionSigners],
};
var hostOptions = new Net.Options.HostOptions(endPoint.Host, [], endPoint.Port);
- return await NetMQTransport.Create(privateKey, appProtocolVersionOptions, hostOptions);
+ return await NetMQTransport.Create(
+ privateKey,
+ appProtocolVersionOptions,
+ hostOptions,
+ TimeSpan.FromSeconds(60));
}
private static ConsensusReactorOption CreateConsensusReactorOption(
@@ -207,6 +223,7 @@ private static ConsensusReactorOption CreateConsensusReactorOption(
private static async Task CreateConsensusTransportAsync(
PrivateKey privateKey,
AppProtocolVersion appProtocolVersion,
+ PublicKey[] trustedAppProtocolVersionSigners,
ValidatorOptions options,
CancellationToken cancellationToken)
{
@@ -215,6 +232,7 @@ private static async Task CreateConsensusTransportAsync(
return await CreateTransport(
privateKey: privateKey,
endPoint: consensusEndPoint,
- appProtocolVersion: appProtocolVersion);
+ appProtocolVersion: appProtocolVersion,
+ trustedAppProtocolVersionSigners);
}
}