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..616209efe80 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[] @@ -14,6 +16,16 @@ builder.ConfigureLogging(logging => { logging.AddConsole(); + + // Logging setting + var loggerConfig = new LoggerConfiguration(); + loggerConfig = loggerConfig.MinimumLevel.Information(); + loggerConfig = loggerConfig + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .Enrich.FromLogContext() + .WriteTo.Console(); + + Log.Logger = loggerConfig.CreateLogger(); }); builder.ConfigureKestrel((context, options) => { @@ -26,6 +38,19 @@ }); builder.ConfigureServices((context, services) => { +// string pluginPath = "/Users/bin_bash_shell/Workspaces/planetarium/NineChronicles/" + +// "lib9c/Lib9c.NCActionLoader/bin/Debug/net6.0/Lib9c.NCActionLoader.dll"; +// string actionLoaderType = "Lib9c.NCActionLoader.NineChroniclesActionLoader"; +// string blockPolicyType = "Lib9c.NCActionLoader.NineChroniclesPolicyActionRegistry"; +// IActionLoader actionLoader = PluginLoader.LoadActionLoader(pluginPath, actionLoaderType); +// IPolicyActionsRegistry policyActionRegistry = +// PluginLoader.LoadPolicyActionRegistry(pluginPath, blockPolicyType); + +// Libplanet.Crypto.CryptoConfig.CryptoBackend = new Secp256k1CryptoBackend(); + +// builder.Services.AddSingleton(actionLoader); +// builder.Services.AddSingleton(policyActionRegistry); + services.AddGrpc(); services.AddGrpcReflection(); services.AddLibplanetNode(context.Configuration); diff --git a/sdk/node/Libplanet.Node.Executable/appsettings-schema.json b/sdk/node/Libplanet.Node.Executable/appsettings-schema.json index 7220e925481..21b22236f82 100644 --- a/sdk/node/Libplanet.Node.Executable/appsettings-schema.json +++ b/sdk/node/Libplanet.Node.Executable/appsettings-schema.json @@ -1106,6 +1106,19 @@ } } }, + "Action": { + "title": "ActionOptions", + "type": "object", + "additionalProperties": false, + "properties": { + "ActionLoaderPath": { + "type": "string" + }, + "ActionLoaderType": { + "type": "string" + } + } + }, "Genesis": { "title": "GenesisOptions", "type": "object", @@ -1114,12 +1127,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 +1144,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 +1234,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 +1279,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 734f125c83e..c31976e21e6 100644 --- a/sdk/node/Libplanet.Node.Executable/appsettings.Development.json +++ b/sdk/node/Libplanet.Node.Executable/appsettings.Development.json @@ -16,9 +16,17 @@ }, "Swarm": { "IsEnabled": true, - "AppProtocolVersion": "200210/AB2da648b9154F2cCcAFBD85e0Bc3d51f97330Fc/MEUCIQCBr..8VdITFe9nMTobl4akFid.s8G2zy2pBidAyRXSeAIgER77qX+eywjgyth6QYi7rQw5nK3KXO6cQ6ngUh.CyfU=/ZHU5OnRpbWVzdGFtcHUxMDoyMDI0LTA3LTMwZQ==" + "AppProtocolVersion": "200210/AB2da648b9154F2cCcAFBD85e0Bc3d51f97330Fc/MEUCIQCBr..8VdITFe9nMTobl4akFid.s8G2zy2pBidAyRXSeAIgER77qX+eywjgyth6QYi7rQw5nK3KXO6cQ6ngUh.CyfU=/ZHU5OnRpbWVzdGFtcHUxMDoyMDI0LTA3LTMwZQ==", + "BlocksyncSeedPeer": "027bd36895d68681290e570692ad3736750ceaab37be402442ffb203967f98f7b6,9c-main-tcp-seed-1.planetarium.dev:31234" }, - "Validator": { - "IsEnabled": true + "Store": { + "Type": 0, + "RootPath": "/Users/jeesu/Projects/Storage/Chain", + "StoreName": "9c-main-snapshot-slim", + "StateStoreName": "9c-main-snapshot-slim/states" + }, + "Action": { + "ActionLoaderPath": "/Users/jeesu/Downloads/osx-arm64/Lib9c.dll", + "ActionLoaderType": "Nekoyume.Action.Loader.NCActionLoader" } } 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..91c1b4c7d7f --- /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; + +public 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..9d54bf4ba44 --- /dev/null +++ b/sdk/node/Libplanet.Node/Actions/PluginLoader.cs @@ -0,0 +1,89 @@ +using System.Reflection; +using Libplanet.Action; +using Libplanet.Action.Loader; + +namespace Libplanet.Node.Actions; + +public static class PluginLoader +{ + public static IActionLoader LoadActionLoader(string relativePath, string typeName) + { + Assembly assembly = LoadPlugin(relativePath); + IEnumerable loaders = Create(assembly); + foreach (IActionLoader loader in loaders) + { + if (loader.GetType().FullName == typeName) + { + return loader; + } + } + + throw new ApplicationException( + $"Can't find {typeName} in {assembly} from {assembly.Location}. " + + $"Available types: {string + .Join(",", loaders.Select(x => x.GetType().FullName))}"); + } + + public static IPolicyActionsRegistry LoadPolicyActionRegistry( + string relativePath, + string typeName) + { + Assembly assembly = LoadPlugin(relativePath); + IEnumerable policies = Create(assembly); + foreach (IPolicyActionsRegistry policy in policies) + { + if (policy.GetType().FullName == typeName) + { + return policy; + } + } + + throw new ApplicationException( + $"Can't find {typeName} in {assembly} from {assembly.Location}. " + + $"Available types: {string + .Join(",", policies.Select(x => x.GetType().FullName))}"); + } + + private static IEnumerable Create(Assembly assembly) + where T : class + { + int count = 0; + + foreach (Type type in assembly.GetTypes()) + { + if (typeof(T).IsAssignableFrom(type)) + { + if (Activator.CreateInstance(type) is T result) + { + count++; + yield return result; + } + } + } + + if (count == 0) + { + string availableTypes = string.Join(",", assembly.GetTypes().Select(t => t.FullName)); + throw new ApplicationException( + $"Can't find any type which implements ICommand in {assembly} from {assembly.Location}.\n" + + $"Available types: {availableTypes}"); + } + } + + private static Assembly LoadPlugin(string relativePath) + { + // Navigate up to the solution root + // string root = Path.GetFullPath(Path.Combine( + // Path.GetDirectoryName( + // Path.GetDirectoryName( + // Path.GetDirectoryName( + // Path.GetDirectoryName( + // Path.GetDirectoryName(typeof(Program).Assembly.Location))))))); + + // string pluginLocation = Path.GetFullPath(Path.Combine(root, relativePath.Replace('\\', Path.DirectorySeparatorChar))); + string pluginLocation = relativePath; + Console.WriteLine($"Loading commands from: {pluginLocation}"); + PluginLoadContext loadContext = new PluginLoadContext(pluginLocation); + return loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(pluginLocation))); + } +} diff --git a/sdk/node/Libplanet.Node/Options/ActionOptions.cs b/sdk/node/Libplanet.Node/Options/ActionOptions.cs new file mode 100644 index 00000000000..7f1506b1482 --- /dev/null +++ b/sdk/node/Libplanet.Node/Options/ActionOptions.cs @@ -0,0 +1,28 @@ +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 ActionLoaderPath { get; set; } = string.Empty; + + public string ActionLoaderType { get; set; } = string.Empty; + + internal IActionLoader GetActionLoader() + { + var actionLoaderPath = ActionLoaderPath; + var actionLoaderType = ActionLoaderType; + return PluginLoader.LoadActionLoader(actionLoaderPath, actionLoaderType); + } + + internal IPolicyActionsRegistry GetPolicyActionsRegistry() + { + // PluginLoader.LoadPolicyActionRegistry(pluginPath, blockPolicyType); + return new PolicyActionsRegistry(); + } +} diff --git a/sdk/node/Libplanet.Node/Options/SwarmOptions.cs b/sdk/node/Libplanet.Node/Options/SwarmOptions.cs index 215b8223202..7728096a246 100644 --- a/sdk/node/Libplanet.Node/Options/SwarmOptions.cs +++ b/sdk/node/Libplanet.Node/Options/SwarmOptions.cs @@ -24,4 +24,8 @@ public sealed class SwarmOptions : OptionsBase [BoundPeer] [Description("The endpoint of the node to block sync.")] public string BlocksyncSeedPeer { get; set; } = string.Empty; + + // 030ffa9bd579ee1503ce008394f687c182279da913bfaec12baca34e79698a7cd1 + [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..126e2563b1e 100644 --- a/sdk/node/Libplanet.Node/Services/BlockChainService.cs +++ b/sdk/node/Libplanet.Node/Services/BlockChainService.cs @@ -37,8 +37,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 +46,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 +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,22 +92,31 @@ void Action(object? state) private static BlockChain CreateBlockChain( GenesisOptions genesisOptions, StoreOptions storeOptions, + ActionOptions actionOptions, IStagePolicy stagePolicy, - IRenderer[] renderers, - IActionLoader[] actionLoaders) + IRenderer[] renderers) { + var actionLoader = actionOptions.GetActionLoader(); + var policyActions = new PolicyActionsRegistry(); + var (store, stateStore) = CreateStore(storeOptions); - var actionLoader = new AggregateTypedActionLoader(actionLoaders); var actionEvaluator = new ActionEvaluator( - policyActionsRegistry: new(), + policyActionsRegistry: policyActions, stateStore, actionLoader); var genesisBlock = CreateGenesisBlock(genesisOptions); var policy = new BlockPolicy( - blockInterval: TimeSpan.FromSeconds(10), - getMaxTransactionsPerBlock: _ => int.MaxValue, - getMaxTransactionsBytes: _ => long.MaxValue); + policyActionsRegistry: policyActions, + 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 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); } }