diff --git a/Lib9c b/Lib9c index 1dc61aa5f..c8247aaa0 160000 --- a/Lib9c +++ b/Lib9c @@ -1 +1 @@ -Subproject commit 1dc61aa5f4844a0112ba9dad79bc9dbdc732a54e +Subproject commit c8247aaa03dd1d009c430e701a6c0aa020de3e52 diff --git a/Libplanet.Extensions.PluggedActionEvaluator/Libplanet.Extensions.PluggedActionEvaluator.csproj b/Libplanet.Extensions.PluggedActionEvaluator/Libplanet.Extensions.PluggedActionEvaluator.csproj new file mode 100644 index 000000000..78e3b7dcd --- /dev/null +++ b/Libplanet.Extensions.PluggedActionEvaluator/Libplanet.Extensions.PluggedActionEvaluator.csproj @@ -0,0 +1,15 @@ + + + + net6.0 + enable + enable + + + + + + + + + diff --git a/Libplanet.Extensions.PluggedActionEvaluator/PluggedActionEvaluator.cs b/Libplanet.Extensions.PluggedActionEvaluator/PluggedActionEvaluator.cs new file mode 100644 index 000000000..c2ee18264 --- /dev/null +++ b/Libplanet.Extensions.PluggedActionEvaluator/PluggedActionEvaluator.cs @@ -0,0 +1,51 @@ +using System.Reflection; +using System.Security.Cryptography; +using Lib9c.Plugin.Shared; +using Libplanet.Action; +using Libplanet.Action.Loader; +using Libplanet.Common; +using Libplanet.Extensions.ActionEvaluatorCommonComponents; +using Libplanet.Store.Trie; +using Libplanet.Types.Blocks; + +namespace Libplanet.Extensions.PluggedActionEvaluator +{ + public class PluggedActionEvaluator : IActionEvaluator + { + private readonly IPluginActionEvaluator _pluginActionEvaluator; + + public IActionLoader ActionLoader => throw new NotImplementedException(); + + public PluggedActionEvaluator(string pluginPath, string typeName, IKeyValueStore keyValueStore) + { + _pluginActionEvaluator = CreateActionEvaluator(pluginPath, typeName, keyValueStore); + } + + public static Assembly LoadPlugin(string absolutePath) + { + PluginLoadContext loadContext = new PluginLoadContext(absolutePath); + return loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(absolutePath))); + } + + 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) + => CreateActionEvaluator(LoadPlugin(pluginPath), typeName, new PluginKeyValueStore(keyValueStore)); + + public IReadOnlyList Evaluate(IPreEvaluationBlock block, HashDigest? baseStateRootHash) + => _pluginActionEvaluator.Evaluate( + PreEvaluationBlockMarshaller.Serialize(block), + baseStateRootHash is { } srh ? srh.ToByteArray() : null) + .Select(eval => ActionEvaluationMarshaller.Deserialize(eval)).ToList().AsReadOnly(); + } +} diff --git a/Libplanet.Extensions.PluggedActionEvaluator/PluginKeyValueStore.cs b/Libplanet.Extensions.PluggedActionEvaluator/PluginKeyValueStore.cs new file mode 100644 index 000000000..e2172fff5 --- /dev/null +++ b/Libplanet.Extensions.PluggedActionEvaluator/PluginKeyValueStore.cs @@ -0,0 +1,42 @@ +using System.Collections.Immutable; +using Lib9c.Plugin.Shared; +using Libplanet.Store.Trie; + +namespace Libplanet.Extensions.PluggedActionEvaluator +{ + public class PluginKeyValueStore : IPluginKeyValueStore + { + private readonly IKeyValueStore _keyValueStore; + + public PluginKeyValueStore(IKeyValueStore keyValueStore) + { + _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/Libplanet.Extensions.PluggedActionEvaluator/PluginLoadContext.cs b/Libplanet.Extensions.PluggedActionEvaluator/PluginLoadContext.cs new file mode 100644 index 000000000..497e9a791 --- /dev/null +++ b/Libplanet.Extensions.PluggedActionEvaluator/PluginLoadContext.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.Loader; + +namespace Libplanet.Extensions.PluggedActionEvaluator +{ + public class PluginLoadContext : AssemblyLoadContext + { + private readonly AssemblyDependencyResolver _resolver; + + public PluginLoadContext(string pluginPath) + { + _resolver = new AssemblyDependencyResolver(pluginPath); + } + + protected override Assembly? Load(AssemblyName assemblyName) + { + if (_resolver.ResolveAssemblyToPath(assemblyName) is { } assemblyPath) + { + return LoadFromAssemblyPath(assemblyPath); + } + + return null; + } + + protected override IntPtr LoadUnmanagedDll(string unmanagedDllName) + { + if (_resolver.ResolveUnmanagedDllToPath(unmanagedDllName) is { } libraryPath) + { + return LoadUnmanagedDllFromPath(libraryPath); + } + + return IntPtr.Zero; + } + } +} + diff --git a/Libplanet.Headless/Hosting/ActionEvaluatorType.cs b/Libplanet.Headless/Hosting/ActionEvaluatorType.cs index 9d9498b61..445198c0c 100644 --- a/Libplanet.Headless/Hosting/ActionEvaluatorType.cs +++ b/Libplanet.Headless/Hosting/ActionEvaluatorType.cs @@ -4,5 +4,5 @@ public enum ActionEvaluatorType { Default, // ActionEvaluator ForkableActionEvaluator, - RemoteActionEvaluator, + PluggedActionEvaluator, } diff --git a/Libplanet.Headless/Hosting/LibplanetNodeService.cs b/Libplanet.Headless/Hosting/LibplanetNodeService.cs index cc5a3d55a..11ecc161b 100644 --- a/Libplanet.Headless/Hosting/LibplanetNodeService.cs +++ b/Libplanet.Headless/Hosting/LibplanetNodeService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.IO; +using System.IO.Compression; using System.Linq; using System.Net.Http; using System.Threading; @@ -15,7 +16,7 @@ using Libplanet.Types.Blocks; using Libplanet.Crypto; using Libplanet.Extensions.ForkableActionEvaluator; -using Libplanet.Extensions.RemoteActionEvaluator; +using Libplanet.Extensions.PluggedActionEvaluator; using Libplanet.Net; using Libplanet.Net.Consensus; using Libplanet.Net.Options; @@ -80,8 +81,7 @@ public LibplanetNodeService( Action preloadStatusHandlerAction, IActionLoader actionLoader, bool ignoreBootstrapFailure = false, - bool ignorePreloadFailure = false, - bool useRemoteActionEvaluator = false + bool ignorePreloadFailure = false ) { if (blockPolicy is null) @@ -95,7 +95,7 @@ public LibplanetNodeService( var iceServers = Properties.IceServers; - (Store, StateStore) = LoadStore( + (Store, StateStore, IKeyValueStore keyValueStore) = LoadStore( Properties.StorePath, Properties.StoreType, Properties.StoreStatesCacheSize); @@ -116,23 +116,25 @@ public LibplanetNodeService( } var blockChainStates = new BlockChainStates(Store, StateStore); + IActionEvaluator BuildActionEvaluator(IActionEvaluatorConfiguration actionEvaluatorConfiguration) { return actionEvaluatorConfiguration switch { - RemoteActionEvaluatorConfiguration remoteActionEvaluatorConfiguration => new RemoteActionEvaluator( - new Uri(remoteActionEvaluatorConfiguration.StateServiceEndpoint)), - DefaultActionEvaluatorConfiguration _ => new ActionEvaluator( - _ => blockPolicy.BlockAction, - stateStore: StateStore, - actionTypeLoader: actionLoader - ), - ForkableActionEvaluatorConfiguration forkableActionEvaluatorConfiguration => new - ForkableActionEvaluator( - forkableActionEvaluatorConfiguration.Pairs.Select(pair => ( - (pair.Item1.Start, pair.Item1.End), BuildActionEvaluator(pair.Item2) - )) - ), + PluggedActionEvaluatorConfiguration pluginActionEvaluatorConfiguration => + new PluggedActionEvaluator( + ResolvePluginPath(pluginActionEvaluatorConfiguration.PluginPath), + pluginActionEvaluatorConfiguration.TypeName, + keyValueStore), + DefaultActionEvaluatorConfiguration _ => + new ActionEvaluator( + _ => blockPolicy.BlockAction, + stateStore: StateStore, + actionTypeLoader: actionLoader), + ForkableActionEvaluatorConfiguration forkableActionEvaluatorConfiguration => + new ForkableActionEvaluator( + forkableActionEvaluatorConfiguration.Pairs.Select( + pair => ((pair.Item1.Start, pair.Item1.End), BuildActionEvaluator(pair.Item2)))), _ => throw new InvalidOperationException("Unexpected type."), }; } @@ -302,7 +304,7 @@ public override async Task StopAsync(CancellationToken cancellationToken) } } - protected (IStore, IStateStore) LoadStore(string path, string type, int statesCacheSize) + protected (IStore, IStateStore, IKeyValueStore) LoadStore(string path, string type, int statesCacheSize) { IStore store = null; if (type == "rocksdb") @@ -346,7 +348,7 @@ public override async Task StopAsync(CancellationToken cancellationToken) IKeyValueStore stateKeyValueStore = new RocksDBKeyValueStore(Path.Combine(path, "states")); IStateStore stateStore = new TrieStateStore(stateKeyValueStore); - return (store, stateStore); + return (store, stateStore, stateKeyValueStore); } private async Task StartSwarm(bool preload, CancellationToken cancellationToken) @@ -568,7 +570,7 @@ protected async Task CheckPeerTable(CancellationToken cancellationToken = defaul if (grace == count) { var message = "No any peers are connected even seed peers were given. " + - $"(grace: {grace}"; + $"(grace: {grace}"; Log.Error(message); // _exceptionHandlerAction(RPCException.NetworkException, message); Properties.NodeExceptionOccurred(NodeExceptionType.NoAnyPeer, message); @@ -629,6 +631,32 @@ public override void Dispose() Log.Debug("Store disposed."); } + private string ResolvePluginPath(string path) => + Uri.IsWellFormedUriString(path, UriKind.Absolute) + ? DownloadPlugin(path).Result + : path; + + private async Task DownloadPlugin(string url) + { + var path = Path.Combine(Environment.CurrentDirectory, "plugins"); + Directory.CreateDirectory(path); + var hashed = url.GetHashCode().ToString(); + var logger = Log.ForContext("LibplanetNodeService", hashed); + using var httpClient = new HttpClient(); + var downloadPath = Path.Join(path, hashed + ".zip"); + var extractPath = Path.Join(path, hashed); + logger.Debug("Downloading..."); + await File.WriteAllBytesAsync( + downloadPath, + await httpClient.GetByteArrayAsync(url, SwarmCancellationToken), + SwarmCancellationToken); + logger.Debug("Finished downloading."); + logger.Debug("Extracting..."); + ZipFile.ExtractToDirectory(downloadPath, extractPath); + logger.Debug("Finished extracting."); + return Path.Combine(extractPath, "Lib9c.Plugin.dll"); + } + // FIXME: Request libplanet provide default implementation. private sealed class ActionTypeLoaderContext : IActionTypeLoaderContext { diff --git a/Libplanet.Headless/Hosting/PluggedActionEvaluatorConfiguration.cs b/Libplanet.Headless/Hosting/PluggedActionEvaluatorConfiguration.cs new file mode 100644 index 000000000..cca6d3a4d --- /dev/null +++ b/Libplanet.Headless/Hosting/PluggedActionEvaluatorConfiguration.cs @@ -0,0 +1,10 @@ +namespace Libplanet.Headless.Hosting; + +public class PluggedActionEvaluatorConfiguration : IActionEvaluatorConfiguration +{ + public ActionEvaluatorType Type => ActionEvaluatorType.PluggedActionEvaluator; + + public string PluginPath { get; init; } + + public string TypeName => "Lib9c.Plugin.PluginActionEvaluator"; +} diff --git a/Libplanet.Headless/Hosting/RemoteActionEvaluatorConfiguration.cs b/Libplanet.Headless/Hosting/RemoteActionEvaluatorConfiguration.cs deleted file mode 100644 index 270d2a824..000000000 --- a/Libplanet.Headless/Hosting/RemoteActionEvaluatorConfiguration.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Libplanet.Headless.Hosting; - -public class RemoteActionEvaluatorConfiguration : IActionEvaluatorConfiguration -{ - public ActionEvaluatorType Type => ActionEvaluatorType.RemoteActionEvaluator; - - public string StateServiceEndpoint { get; init; } -} diff --git a/Libplanet.Headless/Libplanet.Headless.csproj b/Libplanet.Headless/Libplanet.Headless.csproj index 94ce16144..b8e446219 100644 --- a/Libplanet.Headless/Libplanet.Headless.csproj +++ b/Libplanet.Headless/Libplanet.Headless.csproj @@ -23,7 +23,7 @@ - + diff --git a/NineChronicles.Headless.Executable.Tests/Commands/TxCommandTest.cs b/NineChronicles.Headless.Executable.Tests/Commands/TxCommandTest.cs index 732d040fc..d9697dd19 100644 --- a/NineChronicles.Headless.Executable.Tests/Commands/TxCommandTest.cs +++ b/NineChronicles.Headless.Executable.Tests/Commands/TxCommandTest.cs @@ -131,11 +131,7 @@ private void Assert_Tx(long txNonce, string filePath, bool gas) { var timeStamp = DateTimeOffset.FromUnixTimeSeconds(DateTimeOffset.UtcNow.ToUnixTimeSeconds()); var hashHex = ByteUtil.Hex(_blockHash.ByteArray); - long? maxGasPrice = null; - if (gas) - { - maxGasPrice = 1L; - } + long? maxGasPrice = gas ? (long?)1L : null; _command.Sign(ByteUtil.Hex(_privateKey.ByteArray), txNonce, hashHex, timeStamp.ToString(), new[] { filePath }, maxGasPrice: maxGasPrice); var output = _console.Out.ToString(); @@ -146,11 +142,12 @@ private void Assert_Tx(long txNonce, string filePath, bool gas) Assert.Equal(_privateKey.Address, tx.Signer); Assert.Equal(timeStamp, tx.Timestamp); ActionBase action = (ActionBase)new NCActionLoader().LoadAction(1L, tx.Actions.Single()); - long expectedGasLimit = 1L; - if (action is ITransferAsset || action is ITransferAssets) - { - expectedGasLimit = 4L; - } + long? expectedGasLimit = gas + ? action is ITransferAsset || action is ITransferAssets + ? (long?)4L + : (long?)1L + : null; + Assert.Equal(expectedGasLimit, tx.GasLimit); if (gas) { diff --git a/NineChronicles.Headless.Executable.sln b/NineChronicles.Headless.Executable.sln index b14931789..6e9604ede 100644 --- a/NineChronicles.Headless.Executable.sln +++ b/NineChronicles.Headless.Executable.sln @@ -60,25 +60,25 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libplanet.Extensions.Forkab EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".Libplanet", ".Libplanet", "{69F04D28-2B2E-454D-9B15-4D708EEEA8B5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Action", "Lib9c\.Libplanet\Libplanet.Action\Libplanet.Action.csproj", "{EB464A50-9976-4DEA-B170-F72C4FB73A9C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libplanet.Action", "Lib9c\.Libplanet\Libplanet.Action\Libplanet.Action.csproj", "{EB464A50-9976-4DEA-B170-F72C4FB73A9C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Common", "Lib9c\.Libplanet\Libplanet.Common\Libplanet.Common.csproj", "{95FB2620-540C-4498-9DAE-65198E89680C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libplanet.Common", "Lib9c\.Libplanet\Libplanet.Common\Libplanet.Common.csproj", "{95FB2620-540C-4498-9DAE-65198E89680C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Types", "Lib9c\.Libplanet\Libplanet.Types\Libplanet.Types.csproj", "{FC65B031-F6EE-4561-A365-47B6FDD1C114}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libplanet.Types", "Lib9c\.Libplanet\Libplanet.Types\Libplanet.Types.csproj", "{FC65B031-F6EE-4561-A365-47B6FDD1C114}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Store", "Lib9c\.Libplanet\Libplanet.Store\Libplanet.Store.csproj", "{2FF6DADC-5E7A-4F03-94D5-2CF50DED8C29}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libplanet.Store", "Lib9c\.Libplanet\Libplanet.Store\Libplanet.Store.csproj", "{2FF6DADC-5E7A-4F03-94D5-2CF50DED8C29}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Crypto", "Lib9c\.Libplanet\Libplanet.Crypto\Libplanet.Crypto.csproj", "{2C3AD392-38A1-4E07-B1F9-694EE4A1E0C0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libplanet.Crypto", "Lib9c\.Libplanet\Libplanet.Crypto\Libplanet.Crypto.csproj", "{2C3AD392-38A1-4E07-B1F9-694EE4A1E0C0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Extensions.ActionEvaluatorCommonComponents", "Lib9c\.Libplanet.Extensions.ActionEvaluatorCommonComponents\Libplanet.Extensions.ActionEvaluatorCommonComponents.csproj", "{A6922395-36E5-4B0A-BEBD-9BCE34D08722}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libplanet.Extensions.ActionEvaluatorCommonComponents", "Lib9c\.Libplanet.Extensions.ActionEvaluatorCommonComponents\Libplanet.Extensions.ActionEvaluatorCommonComponents.csproj", "{A6922395-36E5-4B0A-BEBD-9BCE34D08722}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lib9c.StateService.Shared", "Lib9c\.Lib9c.StateService.Shared\Lib9c.StateService.Shared.csproj", "{6A410F06-134A-46D9-8B39-381FA2ED861F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libplanet.Extensions.RemoteBlockChainStates", "Lib9c\.Libplanet.Extensions.RemoteBlockChainStates\Libplanet.Extensions.RemoteBlockChainStates.csproj", "{8F9E5505-C157-4DF3-A419-FF0108731397}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Extensions.RemoteActionEvaluator", "Lib9c\.Libplanet.Extensions.RemoteActionEvaluator\Libplanet.Extensions.RemoteActionEvaluator.csproj", "{C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NineChronicles.Headless.AccessControlCenter", "NineChronicles.Headless.AccessControlCenter\NineChronicles.Headless.AccessControlCenter.csproj", "{162C0F4B-A1D9-4132-BC34-31F1247BC26B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Libplanet.Extensions.RemoteBlockChainStates", "Lib9c\.Libplanet.Extensions.RemoteBlockChainStates\Libplanet.Extensions.RemoteBlockChainStates.csproj", "{8F9E5505-C157-4DF3-A419-FF0108731397}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Libplanet.Extensions.PluggedActionEvaluator", "Libplanet.Extensions.PluggedActionEvaluator\Libplanet.Extensions.PluggedActionEvaluator.csproj", "{DE91C36D-3999-47B6-A0BD-848C8EBA2A76}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NineChronicles.Headless.AccessControlCenter", "NineChronicles.Headless.AccessControlCenter\NineChronicles.Headless.AccessControlCenter.csproj", "{162C0F4B-A1D9-4132-BC34-31F1247BC26B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lib9c.Plugin.Shared", "Lib9c\.Lib9c.Plugin.Shared\Lib9c.Plugin.Shared.csproj", "{3D32DA34-E619-429F-8421-848FF4F14417}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -651,42 +651,6 @@ Global {A6922395-36E5-4B0A-BEBD-9BCE34D08722}.Release|x64.Build.0 = Release|Any CPU {A6922395-36E5-4B0A-BEBD-9BCE34D08722}.Release|x86.ActiveCfg = Release|Any CPU {A6922395-36E5-4B0A-BEBD-9BCE34D08722}.Release|x86.Build.0 = Release|Any CPU - {6A410F06-134A-46D9-8B39-381FA2ED861F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6A410F06-134A-46D9-8B39-381FA2ED861F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6A410F06-134A-46D9-8B39-381FA2ED861F}.Debug|x64.ActiveCfg = Debug|Any CPU - {6A410F06-134A-46D9-8B39-381FA2ED861F}.Debug|x64.Build.0 = Debug|Any CPU - {6A410F06-134A-46D9-8B39-381FA2ED861F}.Debug|x86.ActiveCfg = Debug|Any CPU - {6A410F06-134A-46D9-8B39-381FA2ED861F}.Debug|x86.Build.0 = Debug|Any CPU - {6A410F06-134A-46D9-8B39-381FA2ED861F}.DevEx|Any CPU.ActiveCfg = Debug|Any CPU - {6A410F06-134A-46D9-8B39-381FA2ED861F}.DevEx|Any CPU.Build.0 = Debug|Any CPU - {6A410F06-134A-46D9-8B39-381FA2ED861F}.DevEx|x64.ActiveCfg = Debug|Any CPU - {6A410F06-134A-46D9-8B39-381FA2ED861F}.DevEx|x64.Build.0 = Debug|Any CPU - {6A410F06-134A-46D9-8B39-381FA2ED861F}.DevEx|x86.ActiveCfg = Debug|Any CPU - {6A410F06-134A-46D9-8B39-381FA2ED861F}.DevEx|x86.Build.0 = Debug|Any CPU - {6A410F06-134A-46D9-8B39-381FA2ED861F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6A410F06-134A-46D9-8B39-381FA2ED861F}.Release|Any CPU.Build.0 = Release|Any CPU - {6A410F06-134A-46D9-8B39-381FA2ED861F}.Release|x64.ActiveCfg = Release|Any CPU - {6A410F06-134A-46D9-8B39-381FA2ED861F}.Release|x64.Build.0 = Release|Any CPU - {6A410F06-134A-46D9-8B39-381FA2ED861F}.Release|x86.ActiveCfg = Release|Any CPU - {6A410F06-134A-46D9-8B39-381FA2ED861F}.Release|x86.Build.0 = Release|Any CPU - {C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}.Debug|x64.ActiveCfg = Debug|Any CPU - {C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}.Debug|x64.Build.0 = Debug|Any CPU - {C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}.Debug|x86.ActiveCfg = Debug|Any CPU - {C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}.Debug|x86.Build.0 = Debug|Any CPU - {C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}.DevEx|Any CPU.ActiveCfg = Debug|Any CPU - {C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}.DevEx|Any CPU.Build.0 = Debug|Any CPU - {C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}.DevEx|x64.ActiveCfg = Debug|Any CPU - {C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}.DevEx|x64.Build.0 = Debug|Any CPU - {C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}.DevEx|x86.ActiveCfg = Debug|Any CPU - {C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}.DevEx|x86.Build.0 = Debug|Any CPU - {C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}.Release|Any CPU.Build.0 = Release|Any CPU - {C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}.Release|x64.ActiveCfg = Release|Any CPU - {C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}.Release|x64.Build.0 = Release|Any CPU - {C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}.Release|x86.ActiveCfg = Release|Any CPU - {C41BA817-5D5B-42A5-9CF8-E8F29D1B71EF}.Release|x86.Build.0 = Release|Any CPU {8F9E5505-C157-4DF3-A419-FF0108731397}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8F9E5505-C157-4DF3-A419-FF0108731397}.Debug|Any CPU.Build.0 = Debug|Any CPU {8F9E5505-C157-4DF3-A419-FF0108731397}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -723,6 +687,42 @@ Global {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Release|x64.Build.0 = Release|Any CPU {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Release|x86.ActiveCfg = Release|Any CPU {162C0F4B-A1D9-4132-BC34-31F1247BC26B}.Release|x86.Build.0 = Release|Any CPU + {DE91C36D-3999-47B6-A0BD-848C8EBA2A76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DE91C36D-3999-47B6-A0BD-848C8EBA2A76}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DE91C36D-3999-47B6-A0BD-848C8EBA2A76}.Debug|x64.ActiveCfg = Debug|Any CPU + {DE91C36D-3999-47B6-A0BD-848C8EBA2A76}.Debug|x64.Build.0 = Debug|Any CPU + {DE91C36D-3999-47B6-A0BD-848C8EBA2A76}.Debug|x86.ActiveCfg = Debug|Any CPU + {DE91C36D-3999-47B6-A0BD-848C8EBA2A76}.Debug|x86.Build.0 = Debug|Any CPU + {DE91C36D-3999-47B6-A0BD-848C8EBA2A76}.DevEx|Any CPU.ActiveCfg = Debug|Any CPU + {DE91C36D-3999-47B6-A0BD-848C8EBA2A76}.DevEx|Any CPU.Build.0 = Debug|Any CPU + {DE91C36D-3999-47B6-A0BD-848C8EBA2A76}.DevEx|x64.ActiveCfg = Debug|Any CPU + {DE91C36D-3999-47B6-A0BD-848C8EBA2A76}.DevEx|x64.Build.0 = Debug|Any CPU + {DE91C36D-3999-47B6-A0BD-848C8EBA2A76}.DevEx|x86.ActiveCfg = Debug|Any CPU + {DE91C36D-3999-47B6-A0BD-848C8EBA2A76}.DevEx|x86.Build.0 = Debug|Any CPU + {DE91C36D-3999-47B6-A0BD-848C8EBA2A76}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DE91C36D-3999-47B6-A0BD-848C8EBA2A76}.Release|Any CPU.Build.0 = Release|Any CPU + {DE91C36D-3999-47B6-A0BD-848C8EBA2A76}.Release|x64.ActiveCfg = Release|Any CPU + {DE91C36D-3999-47B6-A0BD-848C8EBA2A76}.Release|x64.Build.0 = Release|Any CPU + {DE91C36D-3999-47B6-A0BD-848C8EBA2A76}.Release|x86.ActiveCfg = Release|Any CPU + {DE91C36D-3999-47B6-A0BD-848C8EBA2A76}.Release|x86.Build.0 = Release|Any CPU + {3D32DA34-E619-429F-8421-848FF4F14417}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D32DA34-E619-429F-8421-848FF4F14417}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D32DA34-E619-429F-8421-848FF4F14417}.Debug|x64.ActiveCfg = Debug|Any CPU + {3D32DA34-E619-429F-8421-848FF4F14417}.Debug|x64.Build.0 = Debug|Any CPU + {3D32DA34-E619-429F-8421-848FF4F14417}.Debug|x86.ActiveCfg = Debug|Any CPU + {3D32DA34-E619-429F-8421-848FF4F14417}.Debug|x86.Build.0 = Debug|Any CPU + {3D32DA34-E619-429F-8421-848FF4F14417}.DevEx|Any CPU.ActiveCfg = Debug|Any CPU + {3D32DA34-E619-429F-8421-848FF4F14417}.DevEx|Any CPU.Build.0 = Debug|Any CPU + {3D32DA34-E619-429F-8421-848FF4F14417}.DevEx|x64.ActiveCfg = Debug|Any CPU + {3D32DA34-E619-429F-8421-848FF4F14417}.DevEx|x64.Build.0 = Debug|Any CPU + {3D32DA34-E619-429F-8421-848FF4F14417}.DevEx|x86.ActiveCfg = Debug|Any CPU + {3D32DA34-E619-429F-8421-848FF4F14417}.DevEx|x86.Build.0 = Debug|Any CPU + {3D32DA34-E619-429F-8421-848FF4F14417}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D32DA34-E619-429F-8421-848FF4F14417}.Release|Any CPU.Build.0 = Release|Any CPU + {3D32DA34-E619-429F-8421-848FF4F14417}.Release|x64.ActiveCfg = Release|Any CPU + {3D32DA34-E619-429F-8421-848FF4F14417}.Release|x64.Build.0 = Release|Any CPU + {3D32DA34-E619-429F-8421-848FF4F14417}.Release|x86.ActiveCfg = Release|Any CPU + {3D32DA34-E619-429F-8421-848FF4F14417}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/NineChronicles.Headless.Executable/Commands/ReplayCommand.Privates.cs b/NineChronicles.Headless.Executable/Commands/ReplayCommand.Privates.cs index 861b678c0..ee3c1ff4b 100644 --- a/NineChronicles.Headless.Executable/Commands/ReplayCommand.Privates.cs +++ b/NineChronicles.Headless.Executable/Commands/ReplayCommand.Privates.cs @@ -297,14 +297,8 @@ ActionContext CreateActionContext( randomSeed: randomSeed); } - byte[] hashedSignature; - using (var hasher = SHA1.Create()) - { - hashedSignature = hasher.ComputeHash(signature); - } - byte[] preEvaluationHashBytes = preEvaluationHash.ToByteArray(); - int seed = ActionEvaluator.GenerateRandomSeed(preEvaluationHashBytes, hashedSignature, signature, 0); + int seed = ActionEvaluator.GenerateRandomSeed(preEvaluationHashBytes, signature, 0); IAccount states = previousStates; foreach (IAction action in actions) diff --git a/NineChronicles.Headless.Executable/Commands/ReplayCommand.cs b/NineChronicles.Headless.Executable/Commands/ReplayCommand.cs index 9467301f8..dd06de842 100644 --- a/NineChronicles.Headless.Executable/Commands/ReplayCommand.cs +++ b/NineChronicles.Headless.Executable/Commands/ReplayCommand.cs @@ -145,19 +145,20 @@ public int Tx( outputSw?.WriteLine(msg); } + var inputState = actionEvaluation.InputContext.PreviousState; + var outputState = actionEvaluation.OutputState; + var accountDiff = AccountDiff.Create(inputState.Trie, outputState.Trie); + var states = actionEvaluation.OutputState; var addressNum = 1; - foreach (var (updatedAddress, updatedState) in states.Delta.States) + foreach (var (updatedAddress, stateDiff) in accountDiff.StateDiffs) { if (verbose) { - msg = $"- action #{actionNum} updated address #{addressNum}({updatedAddress}) beginning.."; - _console.Out.WriteLine(msg); - outputSw?.WriteLine(msg); - msg = $"{updatedState}"; + msg = $"- action #{actionNum} updated value at address #{addressNum} ({updatedAddress})"; _console.Out.WriteLine(msg); outputSw?.WriteLine(msg); - msg = $"- action #{actionNum} updated address #{addressNum}({updatedAddress}) end.."; + msg = $" from {stateDiff.Item1} to {stateDiff.Item2}"; _console.Out.WriteLine(msg); outputSw?.WriteLine(msg); } @@ -643,13 +644,13 @@ private void PrintEvaluation(ActionEvaluation evaluation, int index) _console.Out.WriteLine($"- action #{index + 1}: {type.Name}(\"{actionType}\")"); } - var states = evaluation.OutputState.Delta.States; - var indexedStates = states.Select((x, i) => (x.Key, x.Value, i: i)); - foreach (var (updatedAddress, updatedState, addressIndex) in indexedStates) + var inputState = evaluation.InputContext.PreviousState; + var outputState = evaluation.OutputState; + var accountDiff = AccountDiff.Create(inputState.Trie, outputState.Trie); + foreach (var (updatedAddress, stateDiff, addressIndex) in accountDiff.StateDiffs.Select((x, i) => (x.Key, x.Value, i))) { - _console.Out.WriteLine($"- action #{index + 1} updated address #{addressIndex + 1}({updatedAddress}) beginning..."); - _console.Out.WriteLine(updatedState); - _console.Out.WriteLine($"- action #{index + 1} updated address #{addressIndex + 1}({updatedAddress}) end..."); + _console.Out.WriteLine($"- action #{index + 1} updated value at address #{addressIndex + 1} ({updatedAddress})"); + _console.Out.WriteLine($" from {stateDiff.Item1} to {stateDiff.Item2}"); } } } diff --git a/NineChronicles.Headless.Executable/Commands/StateCommand.cs b/NineChronicles.Headless.Executable/Commands/StateCommand.cs index b88506bb6..28a930868 100644 --- a/NineChronicles.Headless.Executable/Commands/StateCommand.cs +++ b/NineChronicles.Headless.Executable/Commands/StateCommand.cs @@ -429,69 +429,6 @@ IEnumerable IKeyValueStore.ListKeys() => _dictionary.Keys; } - private static ImmutableDictionary GetTotalDelta( - IReadOnlyList actionEvaluations, - Func toStateKey, - Func<(Address, Currency), string> toFungibleAssetKey, - Func toTotalSupplyKey, - string validatorSetKey) - { - IImmutableSet
stateUpdatedAddresses = actionEvaluations - .SelectMany(a => a.OutputState.Delta.StateUpdatedAddresses) - .ToImmutableHashSet(); - IImmutableSet<(Address, Currency)> updatedFungibleAssets = actionEvaluations - .SelectMany(a => a.OutputState.Delta.UpdatedFungibleAssets) - .ToImmutableHashSet(); - IImmutableSet updatedTotalSupplies = actionEvaluations - .SelectMany(a => a.OutputState.Delta.UpdatedTotalSupplyCurrencies) - .ToImmutableHashSet(); - - if (actionEvaluations.Count == 0) - { - return ImmutableDictionary.Empty; - } - - IAccount lastStates = actionEvaluations[actionEvaluations.Count - 1].OutputState; - - ImmutableDictionary totalDelta = - stateUpdatedAddresses.ToImmutableDictionary( - toStateKey, - a => lastStates.GetState(a) ?? - throw new InvalidOperationException( - "If it was updated well, the output states will include it also.") - ).SetItems( - updatedFungibleAssets.Select(pair => - new KeyValuePair( - toFungibleAssetKey(pair), - new Bencodex.Types.Integer( - lastStates.GetBalance(pair.Item1, pair.Item2).RawValue - ) - ) - ) - ); - - foreach (var currency in updatedTotalSupplies) - { - if (lastStates.GetTotalSupply(currency).RawValue is { } rawValue) - { - totalDelta = totalDelta.SetItem( - toTotalSupplyKey(currency), - new Bencodex.Types.Integer(rawValue) - ); - } - } - - if (lastStates.GetValidatorSet() is { } validatorSet && validatorSet.Validators.Any()) - { - totalDelta = totalDelta.SetItem( - validatorSetKey, - validatorSet.Bencoded - ); - } - - return totalDelta; - } - private static string ToStateKey(Address address) => ByteUtil.Hex(address.ByteArray); private static string ToFungibleAssetKey(Address address, Currency currency) => diff --git a/NineChronicles.Headless.Executable/Commands/TxCommand.cs b/NineChronicles.Headless.Executable/Commands/TxCommand.cs index 88bef4faa..07482d9b6 100644 --- a/NineChronicles.Headless.Executable/Commands/TxCommand.cs +++ b/NineChronicles.Headless.Executable/Commands/TxCommand.cs @@ -87,15 +87,18 @@ public void Sign( return action; }).ToList(); +#pragma warning disable S3358 // Extract ternary condition. Transaction tx = Transaction.Create( nonce: nonce, privateKey: new PrivateKey(ByteUtil.ParseHex(privateKey)), genesisHash: BlockHash.FromString(genesisHash), timestamp: DateTimeOffset.Parse(timestamp), - gasLimit: parsedActions.Any(a => a is ITransferAssets or ITransferAsset) ? 4 : 1, + gasLimit: maxGasPrice.HasValue + ? parsedActions.Any(a => a is ITransferAssets or ITransferAsset) ? 4 : 1 + : null, maxGasPrice: maxGasPrice.HasValue ? maxGasPrice.Value * Currencies.Mead : null, - actions: parsedActions.ToPlainValues() - ); + actions: parsedActions.ToPlainValues()); +#pragma warning restore S3358 byte[] raw = tx.Serialize(); if (bytes) diff --git a/NineChronicles.Headless.Executable/Program.cs b/NineChronicles.Headless.Executable/Program.cs index d3ada1c07..48391e338 100644 --- a/NineChronicles.Headless.Executable/Program.cs +++ b/NineChronicles.Headless.Executable/Program.cs @@ -258,10 +258,6 @@ public async Task Run( return actionEvaluatorType switch { ActionEvaluatorType.Default => new DefaultActionEvaluatorConfiguration(), - ActionEvaluatorType.RemoteActionEvaluator => new RemoteActionEvaluatorConfiguration - { - StateServiceEndpoint = configuration.GetValue("StateServiceEndpoint"), - }, ActionEvaluatorType.ForkableActionEvaluator => new ForkableActionEvaluatorConfiguration { Pairs = (configuration.GetSection("Pairs") ?? @@ -275,6 +271,10 @@ public async Task Run( return (range, actionEvaluatorConfiguration); }).ToImmutableArray() }, + ActionEvaluatorType.PluggedActionEvaluator => new PluggedActionEvaluatorConfiguration + { + PluginPath = configuration.GetValue("PluginPath"), + }, _ => throw new InvalidOperationException("Unexpected type."), }; } diff --git a/NineChronicles.Headless.Executable/appsettings-schema.json b/NineChronicles.Headless.Executable/appsettings-schema.json index 19dd91e11..55a6e225a 100644 --- a/NineChronicles.Headless.Executable/appsettings-schema.json +++ b/NineChronicles.Headless.Executable/appsettings-schema.json @@ -217,7 +217,21 @@ "type": "string" } }, - "required": ["stateServiceEndpoint"], + "required": [ "stateServiceEndpoint" ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "PluggedActionEvaluator" + }, + "pluginPath": { + "$comment": "Local path or URI. If it is URI, download it under ./plugin", + "type": "string" + } + }, + "required": [ "pluginPath" ], "additionalProperties": false }, { @@ -238,7 +252,7 @@ "$ref": "#/definitions/action-evaluator" } }, - "required": ["range", "actionEvaluator"], + "required": [ "range", "actionEvaluator" ], "additionalProperties": false } } diff --git a/NineChronicles.Headless.Executable/appsettings.json b/NineChronicles.Headless.Executable/appsettings.json index aef98715b..4cce07805 100644 --- a/NineChronicles.Headless.Executable/appsettings.json +++ b/NineChronicles.Headless.Executable/appsettings.json @@ -128,5 +128,10 @@ "ManagementTimeMinutes": 60, "TxIntervalMinutes": 60, "ThresholdCount": 29 + }, + "Jwt": { + "EnableJwtAuthentication": false, + "Key": "secretKey", + "Issuer": "planetariumhq.com" } } diff --git a/NineChronicles.Headless/BlockChainService.cs b/NineChronicles.Headless/BlockChainService.cs index 82fb5cb18..a05b128f7 100644 --- a/NineChronicles.Headless/BlockChainService.cs +++ b/NineChronicles.Headless/BlockChainService.cs @@ -221,7 +221,7 @@ public UnaryResult> GetSheets( foreach (var b in addressBytesList) { var address = new Address(b); - if (_memoryCache.TryGetValue(address.ToString(), out byte[] cached)) + if (_memoryCache.TryGetSheet(address.ToString(), out byte[] cached)) { result.TryAdd(b, cached); } diff --git a/NineChronicles.Headless/GraphQLService.cs b/NineChronicles.Headless/GraphQLService.cs index 80be90055..de3b3b115 100644 --- a/NineChronicles.Headless/GraphQLService.cs +++ b/NineChronicles.Headless/GraphQLService.cs @@ -13,7 +13,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; using NineChronicles.Headless.GraphTypes; using NineChronicles.Headless.Middleware; using NineChronicles.Headless.Properties; @@ -25,6 +24,8 @@ public class GraphQLService { public const string LocalPolicyKey = "LocalPolicy"; + public const string JwtPolicyKey = "JwtPolicy"; + public const string NoCorsPolicyName = "AllowAllOrigins"; public const string SecretTokenKey = "secret"; @@ -129,6 +130,13 @@ public void ConfigureServices(IServiceCollection services) services.Configure(Configuration.GetSection("MultiAccountManaging")); } + var jwtOptions = Configuration.GetSection("Jwt"); + if (Convert.ToBoolean(jwtOptions["EnableJwtAuthentication"])) + { + services.Configure(jwtOptions); + services.AddTransient(); + } + if (!(Configuration[NoCorsKey] is null)) { services.AddCors( @@ -161,12 +169,19 @@ public void ConfigureServices(IServiceCollection services) .AddLibplanetExplorer() .AddUserContextBuilder() .AddGraphQLAuthorization( - options => options.AddPolicy( - LocalPolicyKey, - p => - p.RequireClaim( - "role", - "Admin"))); + options => + { + options.AddPolicy( + LocalPolicyKey, + p => + p.RequireClaim( + "role", + "Admin")); + options.AddPolicy( + JwtPolicyKey, + p => + p.RequireClaim("iss", jwtOptions["Issuer"])); + }); services.AddGraphTypes(); } @@ -190,6 +205,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseMiddleware(); app.UseMiddleware(); + if (Convert.ToBoolean(Configuration.GetSection("Jwt")["EnableJwtAuthentication"])) + { + app.UseMiddleware(); + } + if (Configuration[NoCorsKey] is null) { app.UseCors(); diff --git a/NineChronicles.Headless/GraphTypes/StandaloneMutation.cs b/NineChronicles.Headless/GraphTypes/StandaloneMutation.cs index b356498c8..dd1b523c7 100644 --- a/NineChronicles.Headless/GraphTypes/StandaloneMutation.cs +++ b/NineChronicles.Headless/GraphTypes/StandaloneMutation.cs @@ -28,6 +28,10 @@ IConfiguration configuration { this.AuthorizeWith(GraphQLService.LocalPolicyKey); } + else if (Convert.ToBoolean(configuration.GetSection("Jwt")["EnableJwtAuthentication"])) + { + this.AuthorizeWith(GraphQLService.JwtPolicyKey); + } Field( name: "keyStore", diff --git a/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs b/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs index 413d20e12..869f38e04 100644 --- a/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs +++ b/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs @@ -31,6 +31,10 @@ public class StandaloneQuery : ObjectGraphType public StandaloneQuery(StandaloneContext standaloneContext, IConfiguration configuration, ActionEvaluationPublisher publisher, StateMemoryCache stateMemoryCache) { bool useSecretToken = configuration[GraphQLService.SecretTokenKey] is { }; + if (Convert.ToBoolean(configuration.GetSection("Jwt")["EnableJwtAuthentication"])) + { + this.AuthorizeWith(GraphQLService.JwtPolicyKey); + } Field>(name: "stateQuery", arguments: new QueryArguments( new QueryArgument diff --git a/NineChronicles.Headless/GraphTypes/StandaloneSubscription.cs b/NineChronicles.Headless/GraphTypes/StandaloneSubscription.cs index bf3c025b7..d0c10c9b0 100644 --- a/NineChronicles.Headless/GraphTypes/StandaloneSubscription.cs +++ b/NineChronicles.Headless/GraphTypes/StandaloneSubscription.cs @@ -26,6 +26,7 @@ using Libplanet.Blockchain; using Libplanet.Store; using Libplanet.Types.Tx; +using Microsoft.Extensions.Configuration; using Serilog; namespace NineChronicles.Headless.GraphTypes @@ -129,9 +130,17 @@ public PreloadStateType() private StandaloneContext StandaloneContext { get; } - public StandaloneSubscription(StandaloneContext standaloneContext) + private IConfiguration Configuration { get; } + + public StandaloneSubscription(StandaloneContext standaloneContext, IConfiguration configuration) { StandaloneContext = standaloneContext; + Configuration = configuration; + if (Convert.ToBoolean(configuration.GetSection("Jwt")["EnableJwtAuthentication"])) + { + this.AuthorizeWith(GraphQLService.JwtPolicyKey); + } + AddField(new EventStreamFieldType { Name = "tipChanged", diff --git a/NineChronicles.Headless/GraphTypes/TransactionHeadlessQuery.cs b/NineChronicles.Headless/GraphTypes/TransactionHeadlessQuery.cs index 979e21b97..557a0a12b 100644 --- a/NineChronicles.Headless/GraphTypes/TransactionHeadlessQuery.cs +++ b/NineChronicles.Headless/GraphTypes/TransactionHeadlessQuery.cs @@ -199,11 +199,8 @@ public TransactionHeadlessQuery(StandaloneContext standaloneContext) Field>>( name: "transactionResults", arguments: new QueryArguments( - new QueryArgument>>> - { - Name = "txIds", - Description = "transaction ids." - } + new QueryArgument>> + { Name = "txIds", Description = "transaction ids." } ), resolve: context => { diff --git a/NineChronicles.Headless/MemoryCacheExtensions.cs b/NineChronicles.Headless/MemoryCacheExtensions.cs index 2c6366d83..3cda79d22 100644 --- a/NineChronicles.Headless/MemoryCacheExtensions.cs +++ b/NineChronicles.Headless/MemoryCacheExtensions.cs @@ -18,9 +18,14 @@ public static byte[] SetSheet(this MemoryCache cache, string cacheKey, IValue va return compressed; } + public static bool TryGetSheet(this MemoryCache cache, string cacheKey, out T cached) + { + return cache.TryGetValue(cacheKey, out cached); + } + public static string? GetSheet(this MemoryCache cache, string cacheKey) { - if (cache.TryGetValue(cacheKey, out byte[] cached)) + if (cache.TryGetSheet(cacheKey, out byte[] cached)) { return (Text)Codec.Decode(MessagePackSerializer.Deserialize(cached, Lz4Options)); } diff --git a/NineChronicles.Headless/Middleware/JwtAuthenticationMiddleware.cs b/NineChronicles.Headless/Middleware/JwtAuthenticationMiddleware.cs new file mode 100644 index 000000000..f30a5a933 --- /dev/null +++ b/NineChronicles.Headless/Middleware/JwtAuthenticationMiddleware.cs @@ -0,0 +1,85 @@ +using System; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Primitives; +using Microsoft.IdentityModel.Tokens; +using Newtonsoft.Json; +using Serilog; + +namespace NineChronicles.Headless.Middleware; + +public class JwtAuthenticationMiddleware : IMiddleware +{ + private readonly ILogger _logger; + private readonly JwtSecurityTokenHandler _tokenHandler = new JwtSecurityTokenHandler(); + private readonly TokenValidationParameters _validationParams; + + public JwtAuthenticationMiddleware(IConfiguration configuration) + { + _logger = Log.Logger.ForContext(); + var jwtConfig = configuration.GetSection("Jwt"); + var issuer = jwtConfig["Issuer"] ?? ""; + var key = jwtConfig["Key"] ?? ""; + _validationParams = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = issuer, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(key.PadRight(512 / 8, '\0'))) + }; + } + + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + context.Request.Headers.TryGetValue("Authorization", out var authorization); + if (authorization.Count > 0) + { + try + { + var (scheme, token) = ExtractSchemeAndToken(authorization); + if (scheme == "Bearer") + { + ValidateTokenAndAddClaims(context, token); + } + } + catch (Exception e) + { + _logger.Error($"Authorization error {e.Message}"); + context.Response.StatusCode = 401; + context.Response.ContentType = "application/json"; + await context.Response.WriteAsync( + JsonConvert.SerializeObject( + new { errpr = e.Message } + )); + return; + } + } + await next(context); + } + + private (string scheme, string token) ExtractSchemeAndToken(StringValues authorizationHeader) + { + var headerValues = authorizationHeader[0].Split(" "); + if (headerValues.Length < 2) + { + throw new ArgumentException("Invalid Authorization header format."); + } + + return (headerValues[0], headerValues[1]); + } + + private void ValidateTokenAndAddClaims(HttpContext context, string token) + { + _tokenHandler.ValidateToken(token, _validationParams, out SecurityToken validatedToken); + var jwt = (JwtSecurityToken)validatedToken; + var claims = jwt.Claims.Select(claim => new Claim(claim.Type, claim.Value)); + context.User.AddIdentity(new ClaimsIdentity(claims)); + } +} diff --git a/NineChronicles.Headless/NineChronicles.Headless.csproj b/NineChronicles.Headless/NineChronicles.Headless.csproj index 6bd8f6ce0..28e2dc0f3 100644 --- a/NineChronicles.Headless/NineChronicles.Headless.csproj +++ b/NineChronicles.Headless/NineChronicles.Headless.csproj @@ -39,6 +39,7 @@ + diff --git a/NineChronicles.Headless/Properties/JwtOptions.cs b/NineChronicles.Headless/Properties/JwtOptions.cs new file mode 100644 index 000000000..7fb6b8871 --- /dev/null +++ b/NineChronicles.Headless/Properties/JwtOptions.cs @@ -0,0 +1,10 @@ +namespace NineChronicles.Headless.Properties; + +public class JwtOptions +{ + public bool EnableJwtAuthentication { get; } + + public string Key { get; } = ""; + + public string Issuer { get; } = "planetariumhq.com"; +}