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..349302ec9a1 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[]
diff --git a/sdk/node/Libplanet.Node.Executable/appsettings-schema.json b/sdk/node/Libplanet.Node.Executable/appsettings-schema.json
index 7220e925481..c2782b052ea 100644
--- a/sdk/node/Libplanet.Node.Executable/appsettings-schema.json
+++ b/sdk/node/Libplanet.Node.Executable/appsettings-schema.json
@@ -1106,6 +1106,22 @@
}
}
},
+ "Action": {
+ "title": "ActionOptions",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "ModulePath": {
+ "type": "string"
+ },
+ "ActionLoaderType": {
+ "type": "string"
+ },
+ "PolicyActionRegistryType": {
+ "type": "string"
+ }
+ }
+ },
"Genesis": {
"title": "GenesisOptions",
"type": "object",
@@ -1114,12 +1130,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 +1147,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 +1237,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 +1282,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 22f9ad15323..7d064c0764c 100644
--- a/sdk/node/Libplanet.Node.Executable/appsettings.Development.json
+++ b/sdk/node/Libplanet.Node.Executable/appsettings.Development.json
@@ -11,13 +11,16 @@
"Protocols": "Http1AndHttp2"
}
},
- "Genesis": {
- "GenesisBlockPath": "https://release.nine-chronicles.com/genesis-block-9c-main"
- },
"Swarm": {
"IsEnabled": true
},
"Validator": {
"IsEnabled": true
+ },
+ "Store": {
+ "Type": 1
+ },
+ "Explorer": {
+ "IsEnabled": true
}
}
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..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..229fed957b0
--- /dev/null
+++ b/sdk/node/Libplanet.Node/Options/ActionOptions.cs
@@ -0,0 +1,41 @@
+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 ModulePath { get; set; } = string.Empty;
+
+ public string ActionLoaderType { get; set; } = string.Empty;
+
+ public string PolicyActionRegistryType { get; set; } = string.Empty;
+
+ internal IActionLoader GetActionLoader()
+ {
+ if (ActionLoaderType != string.Empty)
+ {
+ var modulePath = ModulePath;
+ var actionLoaderType = ActionLoaderType;
+ return PluginLoader.LoadActionLoader(modulePath, actionLoaderType);
+ }
+
+ return new AggregateTypedActionLoader();
+ }
+
+ internal IPolicyActionsRegistry GetPolicyActionsRegistry()
+ {
+ if (PolicyActionRegistryType != string.Empty)
+ {
+ var modulePath = ModulePath;
+ var policyActionRegistryType = PolicyActionRegistryType;
+ return PluginLoader.LoadPolicyActionRegistry(modulePath, policyActionRegistryType);
+ }
+
+ return new PolicyActionsRegistry();
+ }
+}
diff --git a/sdk/node/Libplanet.Node/Options/SwarmOptions.cs b/sdk/node/Libplanet.Node/Options/SwarmOptions.cs
index 215b8223202..b3c3293976c 100644
--- a/sdk/node/Libplanet.Node/Options/SwarmOptions.cs
+++ b/sdk/node/Libplanet.Node/Options/SwarmOptions.cs
@@ -24,4 +24,7 @@ public sealed class SwarmOptions : OptionsBase
[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/BlockChainService.cs b/sdk/node/Libplanet.Node/Services/BlockChainService.cs
index 2f528bce875..8aecabe77ad 100644
--- a/sdk/node/Libplanet.Node/Services/BlockChainService.cs
+++ b/sdk/node/Libplanet.Node/Services/BlockChainService.cs
@@ -6,7 +6,6 @@
using Bencodex;
using Bencodex.Types;
using Libplanet.Action;
-using Libplanet.Action.Loader;
using Libplanet.Action.Sys;
using Libplanet.Blockchain;
using Libplanet.Blockchain.Policies;
@@ -37,8 +36,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 +45,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 +83,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 +91,29 @@ void Action(object? state)
private static BlockChain CreateBlockChain(
GenesisOptions genesisOptions,
StoreOptions storeOptions,
+ ActionOptions actionOptions,
IStagePolicy stagePolicy,
- IRenderer[] renderers,
- IActionLoader[] actionLoaders)
+ IRenderer[] renderers)
{
- var (store, stateStore) = CreateStore(storeOptions);
- var actionLoader = new AggregateTypedActionLoader(actionLoaders);
+ var actionLoader = actionOptions.GetActionLoader();
+ var policyActionsRegistry = actionOptions.GetPolicyActionsRegistry();
+ var (store, stateStore, _) = CreateStore(storeOptions);
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)
{
@@ -133,7 +139,7 @@ private static BlockChain CreateBlockChain(
renderers: renderers);
}
- private static (IStore, IStateStore) CreateStore(StoreOptions storeOptions)
+ private static (IStore, IStateStore, IKeyValueStore) CreateStore(StoreOptions storeOptions)
{
return storeOptions.Type switch
{
@@ -142,19 +148,20 @@ private static (IStore, IStateStore) CreateStore(StoreOptions storeOptions)
_ => throw new NotSupportedException($"Unsupported store type: {storeOptions.Type}"),
};
- (MemoryStore, TrieStateStore) CreateMemoryStore()
+ (MemoryStore, TrieStateStore, MemoryKeyValueStore) CreateMemoryStore()
{
var store = new MemoryStore();
- var stateStore = new TrieStateStore(new MemoryKeyValueStore());
- return (store, stateStore);
+ var keyValueStore = new MemoryKeyValueStore();
+ var stateStore = new TrieStateStore(keyValueStore);
+ return (store, stateStore, keyValueStore);
}
- (RocksDBStore.RocksDBStore, TrieStateStore) CreateDiskStore()
+ (RocksDBStore.RocksDBStore, TrieStateStore, RocksDBKeyValueStore) CreateDiskStore()
{
var store = new RocksDBStore.RocksDBStore(storeOptions.StoreName);
var keyValueStore = new RocksDBKeyValueStore(storeOptions.StateStoreName);
var stateStore = new TrieStateStore(keyValueStore);
- return (store, stateStore);
+ return (store, stateStore, keyValueStore);
}
}
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);
}
}