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..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.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/ActionContextMarshaller.cs b/sdk/node/Libplanet.Node/Actions/ActionContextMarshaller.cs
new file mode 100644
index 00000000000..ff14888d775
--- /dev/null
+++ b/sdk/node/Libplanet.Node/Actions/ActionContextMarshaller.cs
@@ -0,0 +1,79 @@
+using System.Security.Cryptography;
+using Bencodex;
+using Bencodex.Types;
+using Libplanet.Action;
+using Libplanet.Common;
+using Libplanet.Crypto;
+using Libplanet.Types.Tx;
+using Boolean = Bencodex.Types.Boolean;
+
+namespace Libplanet.Node.Actions;
+
+public static class ActionContextMarshaller
+{
+ private static readonly Codec Codec = new Codec();
+
+ public static byte[] Serialize(this ICommittedActionContext actionContext)
+ {
+ return Codec.Encode(Marshal(actionContext));
+ }
+
+ public static Dictionary Marshal(this ICommittedActionContext actionContext)
+ {
+ var dictionary = Bencodex.Types.Dictionary.Empty
+ .Add("signer", actionContext.Signer.ToHex())
+ .Add("miner", actionContext.Miner.ToHex())
+ .Add("block_index", actionContext.BlockIndex)
+ .Add("block_protocol_version", actionContext.BlockProtocolVersion)
+ .Add("previous_states", actionContext.PreviousState.ByteArray)
+ .Add("random_seed", actionContext.RandomSeed)
+ .Add("block_action", actionContext.IsPolicyAction);
+
+ if (actionContext.TxId is { } txId)
+ {
+ dictionary = dictionary.Add("tx_id", txId.ByteArray);
+ }
+
+ return dictionary;
+ }
+
+ public static ICommittedActionContext Unmarshal(Dictionary dictionary)
+ {
+ return new CommittedActionContext(
+ signer: new Address(((Text)dictionary["signer"]).Value),
+ txId: GetTxId(dictionary, "tx_id"),
+ miner: new Address(((Text)dictionary["miner"]).Value),
+ blockIndex: (Integer)dictionary["block_index"],
+ blockProtocolVersion: (Integer)dictionary["block_protocol_version"],
+ previousState: new HashDigest(dictionary["previous_states"]),
+ randomSeed: (Integer)dictionary["random_seed"],
+ isPolicyAction: (Boolean)dictionary["block_action"]
+ );
+
+ static TxId? GetTxId(Dictionary dictionary, string key)
+ {
+ if (dictionary.TryGetValue((Text)key, out IValue txIdValue)
+ && txIdValue is Binary txIdBinaryValue)
+ {
+ return new TxId(txIdBinaryValue.ByteArray);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ }
+
+ public static ICommittedActionContext Deserialize(byte[] serialized)
+ {
+ var decoded = Codec.Decode(serialized);
+ if (decoded is not Dictionary dictionary)
+ {
+ throw new ArgumentException(
+ message: $"Expected 'Dictionary' but {decoded.GetType().Name}",
+ paramName: nameof(serialized));
+ }
+
+ return Unmarshal(dictionary);
+ }
+}
diff --git a/sdk/node/Libplanet.Node/Actions/ActionEvaluationMarshaller.cs b/sdk/node/Libplanet.Node/Actions/ActionEvaluationMarshaller.cs
new file mode 100644
index 00000000000..c73563fa1de
--- /dev/null
+++ b/sdk/node/Libplanet.Node/Actions/ActionEvaluationMarshaller.cs
@@ -0,0 +1,48 @@
+using System.Security.Cryptography;
+using Bencodex;
+using Bencodex.Types;
+using Libplanet.Action;
+using Libplanet.Common;
+
+namespace Libplanet.Node.Actions;
+
+public static class ActionEvaluationMarshaller
+{
+ private static readonly Codec Codec = new();
+
+ public static byte[] Serialize(this ICommittedActionEvaluation actionEvaluation)
+ {
+ return Codec.Encode(Marshal(actionEvaluation));
+ }
+
+ public static Dictionary Marshal(this ICommittedActionEvaluation actionEvaluation) =>
+ Dictionary.Empty
+ .Add("action", actionEvaluation.Action)
+ .Add("output_states", actionEvaluation.OutputState.ByteArray)
+ .Add("input_context", actionEvaluation.InputContext.Marshal())
+ .Add("exception", GetException(actionEvaluation.Exception));
+
+ public static ICommittedActionEvaluation Unmarshal(IValue value)
+ {
+ if (value is not Dictionary dictionary)
+ {
+ throw new ArgumentException("The value must be a dictionary.", nameof(value));
+ }
+
+ return new CommittedActionEvaluation(
+ dictionary["action"],
+ ActionContextMarshaller.Unmarshal((Dictionary)dictionary["input_context"]),
+ new HashDigest(dictionary["output_states"]),
+ dictionary["exception"] is Text typeName ? new Exception(typeName) : null
+ );
+ }
+
+ public static ICommittedActionEvaluation Deserialize(byte[] serialized)
+ {
+ var decoded = Codec.Decode(serialized);
+ return Unmarshal(decoded);
+ }
+
+ private static IValue GetException(Exception? exception)
+ => exception?.GetType().FullName is { } typeName ? (Text)typeName : Null.Value;
+}
diff --git a/sdk/node/Libplanet.Node/Actions/PluggedActionEvaluator.cs b/sdk/node/Libplanet.Node/Actions/PluggedActionEvaluator.cs
new file mode 100644
index 00000000000..1d28fb947eb
--- /dev/null
+++ b/sdk/node/Libplanet.Node/Actions/PluggedActionEvaluator.cs
@@ -0,0 +1,70 @@
+using System.Diagnostics;
+using System.Reflection;
+using System.Security.Cryptography;
+using Lib9c.Plugin.Shared;
+using Libplanet.Action;
+using Libplanet.Action.Loader;
+using Libplanet.Common;
+using Libplanet.Node.Actions;
+using Libplanet.Store.Trie;
+using Libplanet.Types.Blocks;
+
+namespace Libplanet.Node.Actions;
+
+public class PluggedActionEvaluator : IActionEvaluator
+{
+ private readonly IPluginActionEvaluator _pluginActionEvaluator;
+
+ public PluggedActionEvaluator(
+ string pluginPath,
+ string typeName,
+ IKeyValueStore keyValueStore,
+ IActionLoader actionLoader)
+ {
+ _pluginActionEvaluator = CreateActionEvaluator(pluginPath, typeName, keyValueStore);
+ ActionLoader = actionLoader;
+ }
+
+ public IActionLoader ActionLoader { get; }
+
+ public static Assembly LoadPlugin(string absolutePath)
+ {
+ var loadContext = new PluginLoadContext(absolutePath);
+ var pluginBaseName = Path.GetFileNameWithoutExtension(absolutePath)
+ ?? throw new UnreachableException("Path.GetFileNameWithoutExtension returned null");
+ var pluginAssemblyName = new AssemblyName(pluginBaseName);
+ return loadContext.LoadFromAssemblyName(pluginAssemblyName);
+ }
+
+ public static IPluginActionEvaluator CreateActionEvaluator(
+ Assembly assembly, string typeName, IPluginKeyValueStore keyValueStore)
+ {
+ if (assembly.GetType(typeName) is Type type &&
+ Activator.CreateInstance(type, args: keyValueStore) as IPluginActionEvaluator
+ is IPluginActionEvaluator pluginActionEvaluator)
+ {
+ return pluginActionEvaluator;
+ }
+
+ throw new NullReferenceException("PluginActionEvaluator not found with given parameters");
+ }
+
+ public static IPluginActionEvaluator CreateActionEvaluator(
+ string pluginPath, string typeName, IKeyValueStore keyValueStore)
+ {
+ var assembly = LoadPlugin(pluginPath);
+ var pluginKeyValueStore = new PluginKeyValueStore(keyValueStore);
+ return CreateActionEvaluator(assembly, typeName, pluginKeyValueStore);
+ }
+
+ public IReadOnlyList Evaluate(
+ IPreEvaluationBlock block,
+ HashDigest? baseStateRootHash)
+ {
+ var evaluations = _pluginActionEvaluator.Evaluate(
+ PreEvaluationBlockMarshaller.Serialize(block),
+ baseStateRootHash is { } srh ? srh.ToByteArray() : null)
+ .Select(ActionEvaluationMarshaller.Deserialize).ToList().AsReadOnly();
+ return evaluations;
+ }
+}
diff --git a/sdk/node/Libplanet.Node/Actions/PluginKeyValueStore.cs b/sdk/node/Libplanet.Node/Actions/PluginKeyValueStore.cs
new file mode 100644
index 00000000000..ef00af9572b
--- /dev/null
+++ b/sdk/node/Libplanet.Node/Actions/PluginKeyValueStore.cs
@@ -0,0 +1,36 @@
+using System.Collections.Immutable;
+using Lib9c.Plugin.Shared;
+using Libplanet.Store.Trie;
+
+namespace Libplanet.Node.Actions;
+
+public class PluginKeyValueStore(IKeyValueStore keyValueStore) : IPluginKeyValueStore
+{
+ private readonly IKeyValueStore _keyValueStore = keyValueStore;
+
+ public byte[] Get(in ImmutableArray key) =>
+ _keyValueStore.Get(new KeyBytes(key));
+
+ public void Set(in ImmutableArray key, byte[] value) =>
+ _keyValueStore.Set(new KeyBytes(key), value);
+
+ public void Set(IDictionary, byte[]> values) =>
+ _keyValueStore.Set(
+ values.ToDictionary(kv => new KeyBytes(kv.Key), kv => kv.Value));
+
+ public void Delete(in ImmutableArray key) =>
+ _keyValueStore.Delete(new KeyBytes(key));
+
+ public void Delete(IEnumerable> keys) =>
+ _keyValueStore.Delete(
+ keys.Select(key => new KeyBytes(key)));
+
+ public bool Exists(in ImmutableArray key) =>
+ _keyValueStore.Exists(new KeyBytes(key));
+
+ public IEnumerable> ListKeys() =>
+ _keyValueStore.ListKeys().Select(key => key.ByteArray);
+
+ public void Dispose() =>
+ _keyValueStore.Dispose();
+}
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/Actions/PreEvaluationBlockMarshaller.cs b/sdk/node/Libplanet.Node/Actions/PreEvaluationBlockMarshaller.cs
new file mode 100644
index 00000000000..3a4bc99a3a0
--- /dev/null
+++ b/sdk/node/Libplanet.Node/Actions/PreEvaluationBlockMarshaller.cs
@@ -0,0 +1,42 @@
+using Bencodex;
+using Bencodex.Types;
+using Libplanet.Types.Blocks;
+
+namespace Libplanet.Node.Actions;
+
+public static class PreEvaluationBlockMarshaller
+{
+ private static readonly Codec Codec = new();
+
+ // Copied form Libplanet/Blocks/BlockMarshaler.cs
+ // Block fields:
+ private static readonly byte[] HeaderKey = "H"u8.ToArray(); // 'H'
+ private static readonly byte[] TransactionsKey = "T"u8.ToArray(); // 'T'
+
+ public static Dictionary Marshal(this IPreEvaluationBlock block)
+ {
+ return Dictionary.Empty
+ .Add(HeaderKey, BlockMarshaler.MarshalPreEvaluationBlockHeader(block))
+ .Add(
+ key: TransactionsKey,
+ value: new List(block.Transactions.Select(TransactionMarshaller.Serialize)));
+ }
+
+ public static IPreEvaluationBlock Unmarshal(Dictionary dictionary)
+ {
+ return new PreEvaluationBlock(
+ BlockMarshaler.UnmarshalPreEvaluationBlockHeader((Dictionary)dictionary[HeaderKey]),
+ BlockMarshaler.UnmarshalBlockTransactions(dictionary),
+ BlockMarshaler.UnmarshalBlockEvidence(dictionary));
+ }
+
+ public static byte[] Serialize(this IPreEvaluationBlock block)
+ {
+ return Codec.Encode(Marshal(block));
+ }
+
+ public static IPreEvaluationBlock Deserialize(byte[] serialized)
+ {
+ return Unmarshal((Dictionary)Codec.Decode(serialized));
+ }
+}
diff --git a/sdk/node/Libplanet.Node/Actions/TransactionMarshaller.cs b/sdk/node/Libplanet.Node/Actions/TransactionMarshaller.cs
new file mode 100644
index 00000000000..fe6ef46795a
--- /dev/null
+++ b/sdk/node/Libplanet.Node/Actions/TransactionMarshaller.cs
@@ -0,0 +1,23 @@
+using Bencodex;
+using Bencodex.Types;
+using Libplanet.Types.Tx;
+
+namespace Libplanet.Node.Actions;
+
+public static class TransactionMarshaller
+{
+ private static readonly Codec Codec = new();
+
+ // Copied from Libplanet/Tx/TxMarshaler.cs
+ private static readonly Binary SignatureKey = new("S"u8.ToArray()); // 'S'
+
+ public static Dictionary Marshal(this ITransaction transaction)
+ {
+ return transaction.MarshalUnsignedTx().Add(SignatureKey, transaction.Signature);
+ }
+
+ public static byte[] Serialize(this ITransaction transaction)
+ {
+ return Codec.Encode(Marshal(transaction));
+ }
+}
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..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..c629dccfad8 100644
--- a/sdk/node/Libplanet.Node/Services/BlockChainService.cs
+++ b/sdk/node/Libplanet.Node/Services/BlockChainService.cs
@@ -13,6 +13,7 @@
using Libplanet.Blockchain.Renderers;
using Libplanet.Common;
using Libplanet.Crypto;
+using Libplanet.Node.Actions;
using Libplanet.Node.Options;
using Libplanet.RocksDBStore;
using Libplanet.Store;
@@ -37,8 +38,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 +47,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 +85,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 +93,33 @@ 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 actionEvaluator = new ActionEvaluator(
- policyActionsRegistry: new(),
- stateStore,
- actionLoader);
+ // var actionLoader = actionOptions.GetActionLoader();
+ var policyActions = new PolicyActionsRegistry();
+
+ var (store, stateStore, keyValueStore) = CreateStore(storeOptions);
+
+ var actionEvaluator = new PluggedActionEvaluator("/Users/jeesu/Downloads/osx-arm64 (2)/Lib9c.Plugin.dll", "Lib9c.Plugin.PluginActionEvaluator", keyValueStore, new AggregateTypedActionLoader());
+ // var actionEvaluator = new ActionEvaluator(
+ // 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)
{
@@ -133,7 +145,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 +154,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);
}
}