From 350f808856c80bc5d7e7f0dbfd0a3e615d74eb22 Mon Sep 17 00:00:00 2001 From: s2quake Date: Tue, 27 Aug 2024 09:47:21 +0900 Subject: [PATCH] feat: Add the feature 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 | 3 ++ .../Actions/PluginLoadContext.cs | 31 ++++++++++++ .../Libplanet.Node/Actions/PluginLoader.cs | 50 +++++++++++++++++++ sdk/node/Libplanet.Node/Libplanet.Node.csproj | 3 +- .../Libplanet.Node/Options/ActionOptions.cs | 41 +++++++++++++++ .../Libplanet.Node/Options/SwarmOptions.cs | 3 ++ .../Services/BlockChainService.cs | 32 +++++++----- .../Libplanet.Node/Services/SwarmService.cs | 28 +++++++++-- 11 files changed, 202 insertions(+), 19 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 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..f5ef168a3c9 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>(); 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 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/BlockChainService.cs b/sdk/node/Libplanet.Node/Services/BlockChainService.cs index 9a6bd7fb362..b7f18806de7 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; @@ -35,8 +34,8 @@ internal sealed class BlockChainService : IBlockChainService, IActionRenderer public BlockChainService( IOptions genesisOptions, IStoreService storeService, + IOptions actionOptions, PolicyService policyService, - IEnumerable actionLoaderProviders, ILogger logger) { _synchronizationContext = SynchronizationContext.Current ?? new(); @@ -45,9 +44,9 @@ public BlockChainService( genesisOptions: genesisOptions.Value, store: storeService.Store, stateStore: storeService.StateStore, + actionOptions: actionOptions.Value, stagePolicy: policyService.StagePolicy, - renderers: [this], - actionLoaders: [.. actionLoaderProviders.Select(item => item.GetActionLoader())]); + renderers: [this]); } public event EventHandler? BlockAppended; @@ -83,7 +82,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 +91,28 @@ private static BlockChain CreateBlockChain( GenesisOptions genesisOptions, IStore store, IStateStore stateStore, + ActionOptions actionOptions, IStagePolicy stagePolicy, - IRenderer[] renderers, - IActionLoader[] actionLoaders) + IRenderer[] renderers) { - var actionLoader = new AggregateTypedActionLoader(actionLoaders); + var actionLoader = actionOptions.GetActionLoader(); + var policyActionsRegistry = actionOptions.GetPolicyActionsRegistry(); 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/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); } }