From 286cc3595b9d29fb534ae2db82f1e27efde0b35c Mon Sep 17 00:00:00 2001 From: s2quake Date: Tue, 27 Aug 2024 09:47:21 +0900 Subject: [PATCH] feat: Add IActionService to load action loader from external assembly --- .../Libplanet.Node.Executable.csproj | 2 + sdk/node/Libplanet.Node.Executable/Program.cs | 2 + .../appsettings-schema.json | 26 ++++++++++ .../LibplanetServicesExtensions.cs | 5 ++ .../Actions/PluginLoadContext.cs | 31 ++++++++++++ .../Libplanet.Node/Actions/PluginLoader.cs | 50 +++++++++++++++++++ sdk/node/Libplanet.Node/Libplanet.Node.csproj | 3 +- .../Libplanet.Node/Options/ActionOptions.cs | 13 +++++ .../Libplanet.Node/Options/SwarmOptions.cs | 3 ++ .../Libplanet.Node/Services/ActionService.cs | 40 +++++++++++++++ .../Services/BlockChainService.cs | 31 +++++++----- .../Libplanet.Node/Services/IActionService.cs | 11 ++++ .../Libplanet.Node/Services/SwarmService.cs | 28 +++++++++-- 13 files changed, 227 insertions(+), 18 deletions(-) create mode 100644 sdk/node/Libplanet.Node/Actions/PluginLoadContext.cs create mode 100644 sdk/node/Libplanet.Node/Actions/PluginLoader.cs create mode 100644 sdk/node/Libplanet.Node/Options/ActionOptions.cs create mode 100644 sdk/node/Libplanet.Node/Services/ActionService.cs create mode 100644 sdk/node/Libplanet.Node/Services/IActionService.cs 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); } }