From 428330408c520bc52a7a8f7071f381e43979ab8b 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 | 32 ++++++++++-- .../appsettings.Development.json | 9 ++-- .../LibplanetServicesExtensions.cs | 3 ++ .../Services/BlockChainServiceTest.cs | 6 ++- .../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 | 47 +++++++++-------- .../Libplanet.Node/Services/SwarmService.cs | 28 +++++++++-- 13 files changed, 224 insertions(+), 33 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 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); } }