diff --git a/sdk/node/Libplanet.Node.Executable/Libplanet.Node.Executable.csproj b/sdk/node/Libplanet.Node.Executable/Libplanet.Node.Executable.csproj
index b5d9a52806a..5da10a9d60b 100644
--- a/sdk/node/Libplanet.Node.Executable/Libplanet.Node.Executable.csproj
+++ b/sdk/node/Libplanet.Node.Executable/Libplanet.Node.Executable.csproj
@@ -16,6 +16,7 @@
+
@@ -26,6 +27,7 @@
+
diff --git a/sdk/node/Libplanet.Node.Executable/Program.cs b/sdk/node/Libplanet.Node.Executable/Program.cs
index a50e30a2821..48aac2560f7 100644
--- a/sdk/node/Libplanet.Node.Executable/Program.cs
+++ b/sdk/node/Libplanet.Node.Executable/Program.cs
@@ -2,6 +2,8 @@
using Libplanet.Node.API.Services;
using Libplanet.Node.Extensions;
using Microsoft.AspNetCore.Server.Kestrel.Core;
+using Serilog;
+using Serilog.Events;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.AddConsole();
diff --git a/sdk/node/Libplanet.Node.Executable/appsettings-schema.json b/sdk/node/Libplanet.Node.Executable/appsettings-schema.json
index 8c6836519af..a36d3d49d0a 100644
--- a/sdk/node/Libplanet.Node.Executable/appsettings-schema.json
+++ b/sdk/node/Libplanet.Node.Executable/appsettings-schema.json
@@ -1095,6 +1095,22 @@
}
}
},
+ "Action": {
+ "title": "ActionOptions",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "ModulePath": {
+ "type": "string"
+ },
+ "ActionLoaderType": {
+ "type": "string"
+ },
+ "PolicyActionRegistryType": {
+ "type": "string"
+ }
+ }
+ },
"Genesis": {
"title": "GenesisOptions",
"type": "object",
@@ -1207,6 +1223,12 @@
"type": "string",
"description": "The endpoint of the node to block sync.",
"pattern": "^$|^(?:[0-9a-fA-F]{130}|[0-9a-fA-F]{66}),(?:(?:[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"
+ }
}
}
},
@@ -1252,6 +1274,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.Extensions/LibplanetServicesExtensions.cs b/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs
index f5210f179f4..27aa98428e1 100644
--- a/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs
+++ b/sdk/node/Libplanet.Node.Extensions/LibplanetServicesExtensions.cs
@@ -26,6 +26,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>();
@@ -43,6 +46,8 @@ public static ILibplanetNodeBuilder AddLibplanetNode(
services.AddSingleton();
services.AddSingleton();
services.AddSingleton(s => (IStoreService)s.GetRequiredService());
+ services.AddSingleton();
+ services.AddSingleton(s => (IActionService)s.GetRequiredService());
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
diff --git a/sdk/node/Libplanet.Node/Actions/PluginLoadContext.cs b/sdk/node/Libplanet.Node/Actions/PluginLoadContext.cs
new file mode 100644
index 00000000000..11f5d5727ec
--- /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;
+
+internal sealed 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..0f1be7b4395
--- /dev/null
+++ b/sdk/node/Libplanet.Node/Actions/PluginLoader.cs
@@ -0,0 +1,50 @@
+using System.Reflection;
+using Libplanet.Action;
+using Libplanet.Action.Loader;
+
+namespace Libplanet.Node.Actions;
+
+internal static class PluginLoader
+{
+ public static IActionLoader LoadActionLoader(string modulePath, string typeName)
+ {
+ var assembly = LoadAssembly(modulePath);
+ return Create(assembly, typeName);
+ }
+
+ public static IPolicyActionsRegistry LoadPolicyActionRegistry(
+ string relativePath, string typeName)
+ {
+ var assembly = LoadAssembly(relativePath);
+ return Create(assembly, typeName);
+ }
+
+ private static T Create(Assembly assembly, string typeName)
+ where T : class
+ {
+ if (assembly.GetType(typeName) is not { } type)
+ {
+ throw new ApplicationException(
+ $"Can't find {typeName} in {assembly} from {assembly.Location}");
+ }
+
+ if (Activator.CreateInstance(type) is not T obj)
+ {
+ throw new ApplicationException(
+ $"Can't create an instance of {type} in {assembly} from {assembly.Location}");
+ }
+
+ return obj;
+ }
+
+ private static Assembly LoadAssembly(string modulePath)
+ {
+ var loadContext = new PluginLoadContext(modulePath);
+ if (Path.GetFileNameWithoutExtension(modulePath) is { } filename)
+ {
+ return loadContext.LoadFromAssemblyName(new AssemblyName(filename));
+ }
+
+ throw new ApplicationException($"Can't load plugin from {modulePath}");
+ }
+}
diff --git a/sdk/node/Libplanet.Node/Libplanet.Node.csproj b/sdk/node/Libplanet.Node/Libplanet.Node.csproj
index 19bd81fd736..a40c4b8d379 100644
--- a/sdk/node/Libplanet.Node/Libplanet.Node.csproj
+++ b/sdk/node/Libplanet.Node/Libplanet.Node.csproj
@@ -1,7 +1,7 @@
-
+
@@ -16,6 +16,7 @@
+
diff --git a/sdk/node/Libplanet.Node/Options/ActionOptions.cs b/sdk/node/Libplanet.Node/Options/ActionOptions.cs
new file mode 100644
index 00000000000..6b22fc3f24e
--- /dev/null
+++ b/sdk/node/Libplanet.Node/Options/ActionOptions.cs
@@ -0,0 +1,13 @@
+namespace Libplanet.Node.Options;
+
+[Options(Position)]
+public sealed class ActionOptions : OptionsBase
+{
+ public const string Position = "Action";
+
+ public string ModulePath { get; set; } = string.Empty;
+
+ public string ActionLoaderType { get; set; } = string.Empty;
+
+ public string PolicyActionRegistryType { get; set; } = string.Empty;
+}
diff --git a/sdk/node/Libplanet.Node/Options/SwarmOptions.cs b/sdk/node/Libplanet.Node/Options/SwarmOptions.cs
index 2d983173443..b51b41aeeed 100644
--- a/sdk/node/Libplanet.Node/Options/SwarmOptions.cs
+++ b/sdk/node/Libplanet.Node/Options/SwarmOptions.cs
@@ -20,4 +20,7 @@ public sealed class SwarmOptions : AppProtocolOptionsBase, IEnable
[BoundPeer]
[Description("The endpoint of the node to block sync.")]
public string BlocksyncSeedPeer { get; set; } = string.Empty;
+
+ [PublicKeyArray]
+ public string[] TrustedAppProtocolVersionSigners { get; set; } = [];
}
diff --git a/sdk/node/Libplanet.Node/Services/ActionService.cs b/sdk/node/Libplanet.Node/Services/ActionService.cs
new file mode 100644
index 00000000000..7238781b97d
--- /dev/null
+++ b/sdk/node/Libplanet.Node/Services/ActionService.cs
@@ -0,0 +1,40 @@
+using Libplanet.Action;
+using Libplanet.Action.Loader;
+using Libplanet.Node.Actions;
+using Libplanet.Node.Options;
+using Microsoft.Extensions.Options;
+
+namespace Libplanet.Node.Services;
+
+internal sealed class ActionService(IOptions options)
+ : IActionService
+{
+ public IActionLoader ActionLoader { get; } = GetActionLoader(options.Value);
+
+ public IPolicyActionsRegistry PolicyActionsRegistry { get; }
+ = GetPolicyActionsRegistry(options.Value);
+
+ private static IActionLoader GetActionLoader(ActionOptions options)
+ {
+ if (options.ActionLoaderType != string.Empty)
+ {
+ var modulePath = options.ModulePath;
+ var actionLoaderType = options.ActionLoaderType;
+ return PluginLoader.LoadActionLoader(modulePath, actionLoaderType);
+ }
+
+ return new AggregateTypedActionLoader();
+ }
+
+ private static IPolicyActionsRegistry GetPolicyActionsRegistry(ActionOptions options)
+ {
+ if (options.PolicyActionRegistryType != string.Empty)
+ {
+ var modulePath = options.ModulePath;
+ var policyActionRegistryType = options.PolicyActionRegistryType;
+ return PluginLoader.LoadPolicyActionRegistry(modulePath, policyActionRegistryType);
+ }
+
+ return new PolicyActionsRegistry();
+ }
+}
diff --git a/sdk/node/Libplanet.Node/Services/BlockChainService.cs b/sdk/node/Libplanet.Node/Services/BlockChainService.cs
index 9a6bd7fb362..48237101f28 100644
--- a/sdk/node/Libplanet.Node/Services/BlockChainService.cs
+++ b/sdk/node/Libplanet.Node/Services/BlockChainService.cs
@@ -35,8 +35,8 @@ internal sealed class BlockChainService : IBlockChainService, IActionRenderer
public BlockChainService(
IOptions genesisOptions,
IStoreService storeService,
+ IActionService actionService,
PolicyService policyService,
- IEnumerable actionLoaderProviders,
ILogger logger)
{
_synchronizationContext = SynchronizationContext.Current ?? new();
@@ -45,9 +45,10 @@ public BlockChainService(
genesisOptions: genesisOptions.Value,
store: storeService.Store,
stateStore: storeService.StateStore,
+ actionLoader: actionService.ActionLoader,
+ policyActionsRegistry: actionService.PolicyActionsRegistry,
stagePolicy: policyService.StagePolicy,
- renderers: [this],
- actionLoaders: [.. actionLoaderProviders.Select(item => item.GetActionLoader())]);
+ renderers: [this]);
}
public event EventHandler? BlockAppended;
@@ -83,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,21 +93,27 @@ private static BlockChain CreateBlockChain(
GenesisOptions genesisOptions,
IStore store,
IStateStore stateStore,
+ IActionLoader actionLoader,
+ IPolicyActionsRegistry policyActionsRegistry,
IStagePolicy stagePolicy,
- IRenderer[] renderers,
- IActionLoader[] actionLoaders)
+ IRenderer[] renderers)
{
- var actionLoader = new AggregateTypedActionLoader(actionLoaders);
var actionEvaluator = new ActionEvaluator(
- policyActionsRegistry: new(),
+ policyActionsRegistry: policyActionsRegistry,
stateStore,
actionLoader);
-
var genesisBlock = CreateGenesisBlock(genesisOptions);
var policy = new BlockPolicy(
- blockInterval: TimeSpan.FromSeconds(10),
- getMaxTransactionsPerBlock: _ => int.MaxValue,
- getMaxTransactionsBytes: _ => long.MaxValue);
+ policyActionsRegistry: policyActionsRegistry,
+ 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/IActionService.cs b/sdk/node/Libplanet.Node/Services/IActionService.cs
new file mode 100644
index 00000000000..dcc26b094c4
--- /dev/null
+++ b/sdk/node/Libplanet.Node/Services/IActionService.cs
@@ -0,0 +1,11 @@
+using Libplanet.Action;
+using Libplanet.Action.Loader;
+
+namespace Libplanet.Node.Services;
+
+public interface IActionService
+{
+ IActionLoader ActionLoader { get; }
+
+ IPolicyActionsRegistry PolicyActionsRegistry { get; }
+}
diff --git a/sdk/node/Libplanet.Node/Services/SwarmService.cs b/sdk/node/Libplanet.Node/Services/SwarmService.cs
index 2be379c7625..eed37c4357d 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;
@@ -72,11 +73,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 = BoundPeer.ParsePeer(nodeOptions.BlocksyncSeedPeer);
var swarmOptions = new Net.Options.SwarmOptions
{
@@ -89,7 +93,11 @@ public async Task StartAsync(CancellationToken cancellationToken)
var consensusTransport = _validatorOptions.IsEnabled
? await CreateConsensusTransportAsync(
- privateKey, appProtocolVersion, _validatorOptions, cancellationToken)
+ privateKey,
+ appProtocolVersion,
+ trustedAppProtocolVersionSigners,
+ _validatorOptions,
+ cancellationToken)
: null;
var consensusReactorOption = _validatorOptions.IsEnabled
? CreateConsensusReactorOption(privateKey, _validatorOptions)
@@ -165,14 +173,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(
@@ -193,6 +209,7 @@ private static ConsensusReactorOption CreateConsensusReactorOption(
private static async Task CreateConsensusTransportAsync(
PrivateKey privateKey,
AppProtocolVersion appProtocolVersion,
+ PublicKey[] trustedAppProtocolVersionSigners,
ValidatorOptions options,
CancellationToken cancellationToken)
{
@@ -201,6 +218,7 @@ private static async Task CreateConsensusTransportAsync(
return await CreateTransport(
privateKey: privateKey,
endPoint: consensusEndPoint,
- appProtocolVersion: appProtocolVersion);
+ appProtocolVersion: appProtocolVersion,
+ trustedAppProtocolVersionSigners);
}
}