diff --git a/.github/workflows/docker-keymaker.yml b/.github/workflows/docker-keymaker.yml new file mode 100644 index 00000000..db067c3c --- /dev/null +++ b/.github/workflows/docker-keymaker.yml @@ -0,0 +1,27 @@ +name: Docker - KeyMaker + + +on: + push: + branches: + - master + tags: + - 'v*.*.*' + paths: + - 'Tools/KeyMaker/**' + - 'Framework/**' + - 'ProjectPlugins/**' + - .github/workflows/docker-KeyMaker.yml + - .github/workflows/docker-reusable.yml + workflow_dispatch: + + +jobs: + build-and-push: + name: Build and Push + uses: ./.github/workflows/docker-reusable.yml + with: + docker_file: Tools/KeyMaker/docker/Dockerfile + docker_repo: codexstorage/codex-keymaker + secrets: inherit + diff --git a/Framework/ArgsUniform/ArgsUniform.cs b/Framework/ArgsUniform/ArgsUniform.cs index d987aa71..68bf8f27 100644 --- a/Framework/ArgsUniform/ArgsUniform.cs +++ b/Framework/ArgsUniform/ArgsUniform.cs @@ -4,9 +4,8 @@ namespace ArgsUniform { public class ArgsUniform { + private readonly Assigner assigner; private readonly Action printAppInfo; - private readonly object? defaultsProvider; - private readonly IEnv.IEnv env; private readonly string[] args; private const int cliStart = 8; private const int shortStart = 38; @@ -31,9 +30,9 @@ namespace ArgsUniform public ArgsUniform(Action printAppInfo, object defaultsProvider, IEnv.IEnv env, params string[] args) { this.printAppInfo = printAppInfo; - this.defaultsProvider = defaultsProvider; - this.env = env; this.args = args; + + assigner = new Assigner(env, args, defaultsProvider); } public T Parse(bool printResult = false) @@ -42,7 +41,7 @@ namespace ArgsUniform { printAppInfo(); PrintHelp(); - throw new Exception(); + Environment.Exit(0); } var result = Activator.CreateInstance(); @@ -53,18 +52,16 @@ namespace ArgsUniform var attr = uniformProperty.GetCustomAttribute(); if (attr != null) { - if (!UniformAssign(result, attr, uniformProperty) && attr.Required) + if (!assigner.UniformAssign(result, attr, uniformProperty) && attr.Required) { - { - missingRequired.Add(uniformProperty); - } + missingRequired.Add(uniformProperty); } } } if (missingRequired.Any()) { - PrintResults(result, uniformProperties); + PrintResults(printResult,result, uniformProperties); Print(""); foreach (var missing in missingRequired) { @@ -75,37 +72,39 @@ namespace ArgsUniform } PrintHelp(); - throw new ArgumentException("Unable to assemble all required arguments"); + Environment.Exit(1); } - if (printResult) - { - PrintResults(result, uniformProperties); - } + PrintResults(printResult, result, uniformProperties); return result; } - private void PrintResults(T result, PropertyInfo[] uniformProperties) - { - Print(""); - foreach (var p in uniformProperties) - { - Print($"\t{p.Name} = {p.GetValue(result)}"); - } - Print(""); - } - public void PrintHelp() { Print(""); - PrintAligned("CLI option:", "(short)", "Environment variable:", "Description"); - var attrs = typeof(T).GetProperties().Where(m => m.GetCustomAttributes(typeof(UniformAttribute), false).Length == 1).Select(p => p.GetCustomAttribute()).Where(a => a != null).ToArray(); - foreach (var attr in attrs) + PrintAligned("CLI option:", "(short)", "Environment variable:", "Description", "(default)"); + var props = typeof(T).GetProperties().Where(m => m.GetCustomAttributes(typeof(UniformAttribute), false).Length == 1).ToArray(); + foreach (var prop in props) { - var a = attr!; - var optional = !a.Required ? " *" : ""; - PrintAligned($"--{a.Arg}=...", $"({a.ArgShort})", a.EnvVar, a.Description + optional); + var a = prop.GetCustomAttribute(); + if (a != null) + { + var optional = !a.Required ? " (optional)" : ""; + var def = assigner.DescribeDefaultFor(prop); + PrintAligned($"--{a.Arg}=...", $"({a.ArgShort})", a.EnvVar, a.Description + optional, $"({def})"); + } + } + Print(""); + } + + private void PrintResults(bool printResult, T result, PropertyInfo[] uniformProperties) + { + if (!printResult) return; + Print(""); + foreach (var p in uniformProperties) + { + Print($"\t{p.Name} = {p.GetValue(result)}"); } Print(""); } @@ -115,7 +114,7 @@ namespace ArgsUniform Console.WriteLine(msg); } - private void PrintAligned(string cli, string s, string env, string desc) + private void PrintAligned(string cli, string s, string env, string desc, string def) { Console.CursorLeft = cliStart; Console.Write(cli); @@ -124,132 +123,8 @@ namespace ArgsUniform Console.CursorLeft = envStart; Console.Write(env); Console.CursorLeft = descStart; - Console.Write(desc + Environment.NewLine); - } - - private object GetDefaultValue(Type t) - { - if (t.IsValueType) return Activator.CreateInstance(t)!; - return null!; - } - - private bool UniformAssign(T result, UniformAttribute attr, PropertyInfo uniformProperty) - { - if (AssignFromArgsIfAble(result, attr, uniformProperty)) return true; - if (AssignFromEnvVarIfAble(result, attr, uniformProperty)) return true; - if (AssignFromDefaultsIfAble(result, uniformProperty)) return true; - return false; - } - - private bool AssignFromDefaultsIfAble(T result, PropertyInfo uniformProperty) - { - var currentValue = uniformProperty.GetValue(result); - var isEmptryString = (currentValue as string) == string.Empty; - if (currentValue != GetDefaultValue(uniformProperty.PropertyType) && !isEmptryString) return true; - - if (defaultsProvider == null) return false; - - var defaultProperty = defaultsProvider.GetType().GetProperties().SingleOrDefault(p => p.Name == uniformProperty.Name); - if (defaultProperty == null) return false; - - var value = defaultProperty.GetValue(defaultsProvider); - if (value != null) - { - return Assign(result, uniformProperty, value); - } - return false; - } - - private bool AssignFromEnvVarIfAble(T result, UniformAttribute attr, PropertyInfo uniformProperty) - { - var e = env.GetEnvVarOrDefault(attr.EnvVar, string.Empty); - if (!string.IsNullOrEmpty(e)) - { - return Assign(result, uniformProperty, e); - } - return false; - } - - private bool AssignFromArgsIfAble(T result, UniformAttribute attr, PropertyInfo uniformProperty) - { - var fromArg = GetFromArgs(attr.Arg); - if (fromArg != null) - { - return Assign(result, uniformProperty, fromArg); - } - var fromShort = GetFromArgs(attr.ArgShort); - if (fromShort != null) - { - return Assign(result, uniformProperty, fromShort); - } - return false; - } - - private bool Assign(T result, PropertyInfo uniformProperty, object value) - { - if (uniformProperty.PropertyType == value.GetType()) - { - uniformProperty.SetValue(result, value); - return true; - } - else - { - if (uniformProperty.PropertyType == typeof(string) || uniformProperty.PropertyType == typeof(int)) - { - uniformProperty.SetValue(result, Convert.ChangeType(value, uniformProperty.PropertyType)); - return true; - } - else - { - if (uniformProperty.PropertyType == typeof(int?)) return AssignOptionalInt(result, uniformProperty, value); - if (uniformProperty.PropertyType.IsEnum) return AssignEnum(result, uniformProperty, value); - if (uniformProperty.PropertyType == typeof(bool)) return AssignBool(result, uniformProperty, value); - - throw new NotSupportedException(); - } - } - } - - private static bool AssignEnum(T result, PropertyInfo uniformProperty, object value) - { - var s = value.ToString(); - if (Enum.TryParse(uniformProperty.PropertyType, s, out var e)) - { - uniformProperty.SetValue(result, e); - return true; - } - return false; - } - - private static bool AssignOptionalInt(T result, PropertyInfo uniformProperty, object value) - { - if (int.TryParse(value.ToString(), out int i)) - { - uniformProperty.SetValue(result, i); - return true; - } - return false; - } - - private static bool AssignBool(T result, PropertyInfo uniformProperty, object value) - { - var s = value.ToString(); - if (s == "1" || (s != null && s.ToLowerInvariant() == "true")) - { - uniformProperty.SetValue(result, true); - } - return true; - } - - private string? GetFromArgs(string key) - { - var argKey = $"--{key}="; - var arg = args.FirstOrDefault(a => a.StartsWith(argKey)); - if (arg != null) - { - return arg.Substring(argKey.Length); - } - return null; + Console.Write(desc + " "); + Console.Write(def + Environment.NewLine); } } } diff --git a/Framework/ArgsUniform/Assigner.cs b/Framework/ArgsUniform/Assigner.cs new file mode 100644 index 00000000..4db7a08c --- /dev/null +++ b/Framework/ArgsUniform/Assigner.cs @@ -0,0 +1,186 @@ +using System.Globalization; +using System.Numerics; +using System.Reflection; + +namespace ArgsUniform +{ + public class Assigner + { + private readonly IEnv.IEnv env; + private readonly string[] args; + private readonly object? defaultsProvider; + + public Assigner(IEnv.IEnv env, string[] args, object? defaultsProvider) + { + this.env = env; + this.args = args; + this.defaultsProvider = defaultsProvider; + } + + public bool UniformAssign(T result, UniformAttribute attr, PropertyInfo uniformProperty) + { + if (AssignFromArgsIfAble(result, attr, uniformProperty)) return true; + if (AssignFromEnvVarIfAble(result, attr, uniformProperty)) return true; + if (AssignFromDefaultsIfAble(result, uniformProperty)) return true; + return false; + } + + public string DescribeDefaultFor(PropertyInfo property) + { + var obj = Activator.CreateInstance(); + var defaultValue = GetDefaultValue(obj, property); + if (defaultValue == null) return ""; + if (defaultValue is string str) + { + return "\"" + str + "\""; + } + return defaultValue.ToString() ?? string.Empty; + } + + private object? GetDefaultValue(T result, PropertyInfo uniformProperty) + { + // Get value from object's static initializer if it's there. + var currentValue = uniformProperty.GetValue(result); + if (currentValue != null) return currentValue; + + // Get value from defaults-provider object if it's there. + if (defaultsProvider == null) return null; + var defaultProperty = defaultsProvider.GetType().GetProperties().SingleOrDefault(p => p.Name == uniformProperty.Name); + if (defaultProperty == null) return null; + return defaultProperty.GetValue(defaultsProvider); + } + + private bool AssignFromDefaultsIfAble(T result, PropertyInfo uniformProperty) + { + var defaultValue = GetDefaultValue(result, uniformProperty); + var isEmptryString = (defaultValue as string) == string.Empty; + if (defaultValue != null && defaultValue != GetDefaultValueForType(uniformProperty.PropertyType) && !isEmptryString) + { + return Assign(result, uniformProperty, defaultValue); + } + return false; + } + + private bool AssignFromEnvVarIfAble(T result, UniformAttribute attr, PropertyInfo uniformProperty) + { + var e = env.GetEnvVarOrDefault(attr.EnvVar, string.Empty); + if (!string.IsNullOrEmpty(e)) + { + return Assign(result, uniformProperty, e); + } + return false; + } + + private bool AssignFromArgsIfAble(T result, UniformAttribute attr, PropertyInfo uniformProperty) + { + var fromArg = GetFromArgs(attr.Arg); + if (fromArg != null) + { + return Assign(result, uniformProperty, fromArg); + } + var fromShort = GetFromArgs(attr.ArgShort); + if (fromShort != null) + { + return Assign(result, uniformProperty, fromShort); + } + return false; + } + + private bool Assign(T result, PropertyInfo uniformProperty, object value) + { + if (uniformProperty.PropertyType == value.GetType()) + { + uniformProperty.SetValue(result, value); + return true; + } + else + { + if (uniformProperty.PropertyType == typeof(string) || uniformProperty.PropertyType == typeof(int)) + { + uniformProperty.SetValue(result, Convert.ChangeType(value, uniformProperty.PropertyType)); + return true; + } + else + { + if (uniformProperty.PropertyType == typeof(int?)) return AssignOptionalInt(result, uniformProperty, value); + if (uniformProperty.PropertyType.IsEnum) return AssignEnum(result, uniformProperty, value); + if (uniformProperty.PropertyType == typeof(bool)) return AssignBool(result, uniformProperty, value); + if (uniformProperty.PropertyType == typeof(ulong)) return AssignUlong(result, uniformProperty, value); + if (uniformProperty.PropertyType == typeof(BigInteger)) return AssignBigInt(result, uniformProperty, value); + + throw new NotSupportedException( + $"Unsupported property type '${uniformProperty.PropertyType}' " + + $"for property '${uniformProperty.Name}'."); + } + } + } + + private static bool AssignEnum(T result, PropertyInfo uniformProperty, object value) + { + var s = value.ToString(); + if (Enum.TryParse(uniformProperty.PropertyType, s, out var e)) + { + uniformProperty.SetValue(result, e); + return true; + } + return false; + } + + private static bool AssignOptionalInt(T result, PropertyInfo uniformProperty, object value) + { + if (int.TryParse(value.ToString(), CultureInfo.InvariantCulture, out int i)) + { + uniformProperty.SetValue(result, i); + return true; + } + return false; + } + + private bool AssignUlong(T? result, PropertyInfo uniformProperty, object value) + { + if (ulong.TryParse(value.ToString(), CultureInfo.InvariantCulture, out ulong i)) + { + uniformProperty.SetValue(result, i); + return true; + } + return false; + } + + private bool AssignBigInt(T result, PropertyInfo uniformProperty, object value) + { + if (BigInteger.TryParse(value.ToString(), CultureInfo.InvariantCulture, out BigInteger i)) + { + uniformProperty.SetValue(result, i); + return true; + } + return false; + } + + private static bool AssignBool(T result, PropertyInfo uniformProperty, object value) + { + var s = value.ToString(); + if (s == "1" || (s != null && s.ToLowerInvariant() == "true")) + { + uniformProperty.SetValue(result, true); + } + return true; + } + + private string? GetFromArgs(string key) + { + var argKey = $"--{key}="; + var arg = args.FirstOrDefault(a => a.StartsWith(argKey)); + if (arg != null) + { + return arg.Substring(argKey.Length); + } + return null; + } + + private static object GetDefaultValueForType(Type t) + { + if (t.IsValueType) return Activator.CreateInstance(t)!; + return null!; + } + } +} diff --git a/Framework/Core/CoreInterface.cs b/Framework/Core/CoreInterface.cs index b43c1c20..81483a8b 100644 --- a/Framework/Core/CoreInterface.cs +++ b/Framework/Core/CoreInterface.cs @@ -30,11 +30,11 @@ namespace Core public IDownloadedLog DownloadLog(RunningContainer container, int? tailLines = null) { var workflow = entryPoint.Tools.CreateWorkflow(); - var file = entryPoint.Tools.GetLog().CreateSubfile(); - entryPoint.Tools.GetLog().Log($"Downloading container log for '{container.Name}' to file '{file.FullFilename}'..."); - var logHandler = new LogDownloadHandler(container.Name, file); + var msg = $"Downloading container log for '{container.Name}'"; + entryPoint.Tools.GetLog().Log(msg); + var logHandler = new WriteToFileLogHandler(entryPoint.Tools.GetLog(), msg); workflow.DownloadContainerLog(container, logHandler, tailLines); - return logHandler.DownloadLog(); + return new DownloadedLog(logHandler); } public string ExecuteContainerCommand(IHasContainer containerSource, string command, params string[] args) diff --git a/Framework/Core/DownloadedLog.cs b/Framework/Core/DownloadedLog.cs index 923bcbc8..3979f3ee 100644 --- a/Framework/Core/DownloadedLog.cs +++ b/Framework/Core/DownloadedLog.cs @@ -1,9 +1,11 @@ -using Logging; +using KubernetesWorkflow; +using Logging; namespace Core { public interface IDownloadedLog { + void IterateLines(Action action); string[] GetLinesContaining(string expectedString); string[] FindLinesThatContain(params string[] tags); void DeleteFile(); @@ -13,9 +15,22 @@ namespace Core { private readonly LogFile logFile; - internal DownloadedLog(LogFile logFile) + internal DownloadedLog(WriteToFileLogHandler logHandler) { - this.logFile = logFile; + logFile = logHandler.LogFile; + } + + public void IterateLines(Action action) + { + using var file = File.OpenRead(logFile.FullFilename); + using var streamReader = new StreamReader(file); + + var line = streamReader.ReadLine(); + while (line != null) + { + action(line); + line = streamReader.ReadLine(); + } } public string[] GetLinesContaining(string expectedString) diff --git a/Framework/Core/EntryPoint.cs b/Framework/Core/EntryPoint.cs index 7977eb3f..0db40f3d 100644 --- a/Framework/Core/EntryPoint.cs +++ b/Framework/Core/EntryPoint.cs @@ -38,10 +38,14 @@ namespace Core return new CoreInterface(this); } - public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles) + /// + /// Deletes kubernetes and tracked file resources. + /// when `waitTillDone` is true, this function will block until resources are deleted. + /// + public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles, bool waitTillDone) { - manager.DecommissionPlugins(deleteKubernetesResources, deleteTrackedFiles); - Tools.Decommission(deleteKubernetesResources, deleteTrackedFiles); + manager.DecommissionPlugins(deleteKubernetesResources, deleteTrackedFiles, waitTillDone); + Tools.Decommission(deleteKubernetesResources, deleteTrackedFiles, waitTillDone); } internal T GetPlugin() where T : IProjectPlugin diff --git a/Framework/Core/Http.cs b/Framework/Core/Http.cs index 0bd3117a..44cb3c66 100644 --- a/Framework/Core/Http.cs +++ b/Framework/Core/Http.cs @@ -7,6 +7,7 @@ namespace Core { T OnClient(Func action); T OnClient(Func action, string description); + T OnClient(Func action, Retry retry); IEndpoint CreateEndpoint(Address address, string baseUrl, string? logAlias = null); } @@ -35,13 +36,19 @@ namespace Core } public T OnClient(Func action, string description) + { + var retry = new Retry(description, timeSet.HttpRetryTimeout(), timeSet.HttpCallRetryDelay(), f => { }); + return OnClient(action, retry); + } + + public T OnClient(Func action, Retry retry) { var client = GetClient(); return LockRetry(() => { return action(client); - }, description); + }, retry); } public IEndpoint CreateEndpoint(Address address, string baseUrl, string? logAlias = null) @@ -54,11 +61,11 @@ namespace Core return DebugStack.GetCallerName(skipFrames: 2); } - private T LockRetry(Func operation, string description) + private T LockRetry(Func operation, Retry retry) { lock (httpLock) { - return Time.Retry(operation, timeSet.HttpMaxNumberOfRetries(), timeSet.HttpCallRetryDelay(), description); + return retry.Run(operation); } } diff --git a/Framework/Core/LogDownloadHandler.cs b/Framework/Core/LogDownloadHandler.cs deleted file mode 100644 index e1736ed1..00000000 --- a/Framework/Core/LogDownloadHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using KubernetesWorkflow; -using Logging; - -namespace Core -{ - internal class LogDownloadHandler : LogHandler, ILogHandler - { - private readonly LogFile log; - - internal LogDownloadHandler(string description, LogFile log) - { - this.log = log; - - log.Write($"{description} -->> {log.FullFilename}"); - log.WriteRaw(description); - } - - internal IDownloadedLog DownloadLog() - { - return new DownloadedLog(log); - } - - protected override void ProcessLine(string line) - { - log.WriteRaw(line); - } - } -} diff --git a/Framework/Core/PluginManager.cs b/Framework/Core/PluginManager.cs index e2b2a5cc..27b08fe3 100644 --- a/Framework/Core/PluginManager.cs +++ b/Framework/Core/PluginManager.cs @@ -34,12 +34,12 @@ return metadata; } - internal void DecommissionPlugins(bool deleteKubernetesResources, bool deleteTrackedFiles) + internal void DecommissionPlugins(bool deleteKubernetesResources, bool deleteTrackedFiles, bool waitTillDone) { foreach (var pair in pairs) { pair.Plugin.Decommission(); - pair.Tools.Decommission(deleteKubernetesResources, deleteTrackedFiles); + pair.Tools.Decommission(deleteKubernetesResources, deleteTrackedFiles, waitTillDone); } } diff --git a/Framework/Core/PluginTools.cs b/Framework/Core/PluginTools.cs index 5e1faeed..aa8e0a6e 100644 --- a/Framework/Core/PluginTools.cs +++ b/Framework/Core/PluginTools.cs @@ -6,7 +6,13 @@ namespace Core { public interface IPluginTools : IWorkflowTool, ILogTool, IHttpFactoryTool, IFileTool { - void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles); + ITimeSet TimeSet { get; } + + /// + /// Deletes kubernetes and tracked file resources. + /// when `waitTillDone` is true, this function will block until resources are deleted. + /// + void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles, bool waitTillDone); } public interface IWorkflowTool @@ -33,7 +39,6 @@ namespace Core internal class PluginTools : IPluginTools { - private readonly ITimeSet timeSet; private readonly WorkflowCreator workflowCreator; private readonly IFileManager fileManager; private readonly LogPrefixer log; @@ -42,10 +47,12 @@ namespace Core { this.log = new LogPrefixer(log); this.workflowCreator = workflowCreator; - this.timeSet = timeSet; + TimeSet = timeSet; fileManager = new FileManager(log, fileManagerRootFolder); } + public ITimeSet TimeSet { get; } + public void ApplyLogPrefix(string prefix) { log.Prefix = prefix; @@ -53,7 +60,7 @@ namespace Core public IHttp CreateHttp(Action onClientCreated) { - return CreateHttp(onClientCreated, timeSet); + return CreateHttp(onClientCreated, TimeSet); } public IHttp CreateHttp(Action onClientCreated, ITimeSet ts) @@ -63,7 +70,7 @@ namespace Core public IHttp CreateHttp() { - return new Http(log, timeSet); + return new Http(log, TimeSet); } public IStartupWorkflow CreateWorkflow(string? namespaceOverride = null) @@ -71,9 +78,9 @@ namespace Core return workflowCreator.CreateWorkflow(namespaceOverride); } - public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles) + public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles, bool waitTillDone) { - if (deleteKubernetesResources) CreateWorkflow().DeleteNamespace(); + if (deleteKubernetesResources) CreateWorkflow().DeleteNamespace(waitTillDone); if (deleteTrackedFiles) fileManager.DeleteAllFiles(); } diff --git a/Framework/Core/TimeSet.cs b/Framework/Core/TimeSet.cs index 3acd8524..0e29c31d 100644 --- a/Framework/Core/TimeSet.cs +++ b/Framework/Core/TimeSet.cs @@ -2,10 +2,31 @@ { public interface ITimeSet { + /// + /// Timeout for a single HTTP call. + /// TimeSpan HttpCallTimeout(); - int HttpMaxNumberOfRetries(); + + /// + /// Maximum total time to attempt to make a successful HTTP call to a service. + /// When HTTP calls time out during this timespan, retries will be made. + /// + TimeSpan HttpRetryTimeout(); + + /// + /// After a failed HTTP call, wait this long before trying again. + /// TimeSpan HttpCallRetryDelay(); - TimeSpan WaitForK8sServiceDelay(); + + /// + /// After a failed K8s operation, wait this long before trying again. + /// + TimeSpan K8sOperationRetryDelay(); + + /// + /// Maximum total time to attempt to perform a successful k8s operation. + /// If k8s operations fail during this timespan, retries will be made. + /// TimeSpan K8sOperationTimeout(); } @@ -16,9 +37,9 @@ return TimeSpan.FromMinutes(3); } - public int HttpMaxNumberOfRetries() + public TimeSpan HttpRetryTimeout() { - return 3; + return TimeSpan.FromMinutes(10); } public TimeSpan HttpCallRetryDelay() @@ -26,7 +47,7 @@ return TimeSpan.FromSeconds(1); } - public TimeSpan WaitForK8sServiceDelay() + public TimeSpan K8sOperationRetryDelay() { return TimeSpan.FromSeconds(10); } @@ -41,27 +62,27 @@ { public TimeSpan HttpCallTimeout() { - return TimeSpan.FromHours(2); + return TimeSpan.FromMinutes(30); } - public int HttpMaxNumberOfRetries() + public TimeSpan HttpRetryTimeout() { - return 1; + return TimeSpan.FromHours(2.2); } public TimeSpan HttpCallRetryDelay() { - return TimeSpan.FromSeconds(2); + return TimeSpan.FromSeconds(20); } - public TimeSpan WaitForK8sServiceDelay() + public TimeSpan K8sOperationRetryDelay() { - return TimeSpan.FromSeconds(10); + return TimeSpan.FromSeconds(30); } public TimeSpan K8sOperationTimeout() { - return TimeSpan.FromMinutes(15); + return TimeSpan.FromHours(1); } } } diff --git a/Framework/DiscordRewards/CheckConfig.cs b/Framework/DiscordRewards/CheckConfig.cs index 9c4fccb0..34425cea 100644 --- a/Framework/DiscordRewards/CheckConfig.cs +++ b/Framework/DiscordRewards/CheckConfig.cs @@ -13,9 +13,9 @@ namespace DiscordRewards public enum CheckType { Uninitialized, - FilledSlot, - FinishedSlot, - PostedContract, - StartedContract, + HostFilledSlot, + HostFinishedSlot, + ClientPostedContract, + ClientStartedContract, } } diff --git a/Framework/DiscordRewards/GiveRewardsCommand.cs b/Framework/DiscordRewards/GiveRewardsCommand.cs index 03bc835c..dffb4ede 100644 --- a/Framework/DiscordRewards/GiveRewardsCommand.cs +++ b/Framework/DiscordRewards/GiveRewardsCommand.cs @@ -3,6 +3,13 @@ public class GiveRewardsCommand { public RewardUsersCommand[] Rewards { get; set; } = Array.Empty(); + public MarketAverage[] Averages { get; set; } = Array.Empty(); + public string[] EventsOverview { get; set; } = Array.Empty(); + + public bool HasAny() + { + return Rewards.Any() || Averages.Any() || EventsOverview.Any(); + } } public class RewardUsersCommand @@ -10,4 +17,15 @@ public ulong RewardId { get; set; } public string[] UserAddresses { get; set; } = Array.Empty(); } + + public class MarketAverage + { + public int NumberOfFinished { get; set; } + public int TimeRangeSeconds { get; set; } + public float Price { get; set; } + public float Size { get; set; } + public float Duration { get; set; } + public float Collateral { get; set; } + public float ProofProbability { get; set; } + } } diff --git a/Framework/DiscordRewards/RewardRepo.cs b/Framework/DiscordRewards/RewardRepo.cs index 51ac3fcb..1c97caf9 100644 --- a/Framework/DiscordRewards/RewardRepo.cs +++ b/Framework/DiscordRewards/RewardRepo.cs @@ -1,53 +1,53 @@ -using Utils; - -namespace DiscordRewards +namespace DiscordRewards { public class RewardRepo { private static string Tag => RewardConfig.UsernameTag; - public RewardConfig[] Rewards { get; } = new RewardConfig[] - { - // Filled any slot - new RewardConfig(1187039439558541498, $"{Tag} successfully filled their first slot!", new CheckConfig - { - Type = CheckType.FilledSlot - }), + public RewardConfig[] Rewards { get; } = new RewardConfig[0]; - // Finished any slot - new RewardConfig(1202286165630390339, $"{Tag} successfully finished their first slot!", new CheckConfig - { - Type = CheckType.FinishedSlot - }), + // Example configuration, from test server: + //{ + // // Filled any slot + // new RewardConfig(1187039439558541498, $"{Tag} successfully filled their first slot!", new CheckConfig + // { + // Type = CheckType.HostFilledSlot + // }), - // Finished a sizable slot - new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot! (10mb/5mins for test)", new CheckConfig - { - Type = CheckType.FinishedSlot, - MinSlotSize = 10.MB(), - MinDuration = TimeSpan.FromMinutes(5.0), - }), + // // Finished any slot + // new RewardConfig(1202286165630390339, $"{Tag} successfully finished their first slot!", new CheckConfig + // { + // Type = CheckType.HostFinishedSlot + // }), - // Posted any contract - new RewardConfig(1202286258370383913, $"{Tag} posted their first contract!", new CheckConfig - { - Type = CheckType.PostedContract - }), + // // Finished a sizable slot + // new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot! (10mb/5mins for test)", new CheckConfig + // { + // Type = CheckType.HostFinishedSlot, + // MinSlotSize = 10.MB(), + // MinDuration = TimeSpan.FromMinutes(5.0), + // }), - // Started any contract - new RewardConfig(1202286330873126992, $"A contract created by {Tag} reached Started state for the first time!", new CheckConfig - { - Type = CheckType.StartedContract - }), + // // Posted any contract + // new RewardConfig(1202286258370383913, $"{Tag} posted their first contract!", new CheckConfig + // { + // Type = CheckType.ClientPostedContract + // }), - // Started a sizable contract - new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time! (10mb/5mins for test)", new CheckConfig - { - Type = CheckType.StartedContract, - MinNumberOfHosts = 4, - MinSlotSize = 10.MB(), - MinDuration = TimeSpan.FromMinutes(5.0), - }) - }; + // // Started any contract + // new RewardConfig(1202286330873126992, $"A contract created by {Tag} reached Started state for the first time!", new CheckConfig + // { + // Type = CheckType.ClientStartedContract + // }), + + // // Started a sizable contract + // new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time! (10mb/5mins for test)", new CheckConfig + // { + // Type = CheckType.ClientStartedContract, + // MinNumberOfHosts = 4, + // MinSlotSize = 10.MB(), + // MinDuration = TimeSpan.FromMinutes(5.0), + // }) + //}; } } diff --git a/Framework/FileUtils/TrackedFile.cs b/Framework/FileUtils/TrackedFile.cs index 694408b9..9ff45d7a 100644 --- a/Framework/FileUtils/TrackedFile.cs +++ b/Framework/FileUtils/TrackedFile.cs @@ -65,7 +65,7 @@ namespace FileUtils if (readExpected == 0 && readActual == 0) { - log.Log($"OK: '{Describe()}' is equal to '{actual.Describe()}'."); + log.Log($"OK: {Describe()} is equal to {actual.Describe()}."); return; } diff --git a/Framework/KubernetesWorkflow/ByteSizeExtensions.cs b/Framework/KubernetesWorkflow/ByteSizeExtensions.cs deleted file mode 100644 index c3cca244..00000000 --- a/Framework/KubernetesWorkflow/ByteSizeExtensions.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Utils; - -namespace KubernetesWorkflow -{ - public static class ByteSizeExtensions - { - public static string ToSuffixNotation(this ByteSize b) - { - long x = 1024; - var map = new Dictionary - { - { Pow(x, 4), "Ti" }, - { Pow(x, 3), "Gi" }, - { Pow(x, 2), "Mi" }, - { (x), "Ki" }, - }; - - var bytes = b.SizeInBytes; - foreach (var pair in map) - { - if (bytes > pair.Key) - { - double bytesD = bytes; - double divD = pair.Key; - double numD = Math.Ceiling(bytesD / divD); - var v = Convert.ToInt64(numD); - return $"{v}{pair.Value}"; - } - } - - return $"{bytes}"; - } - - private static long Pow(long x, int v) - { - long result = 1; - for (var i = 0; i < v; i++) result *= x; - return result; - } - } -} diff --git a/Framework/KubernetesWorkflow/CrashWatcher.cs b/Framework/KubernetesWorkflow/CrashWatcher.cs index 5cb2e034..7f38cfb5 100644 --- a/Framework/KubernetesWorkflow/CrashWatcher.cs +++ b/Framework/KubernetesWorkflow/CrashWatcher.cs @@ -11,7 +11,6 @@ namespace KubernetesWorkflow private readonly string podName; private readonly string recipeName; private readonly string k8sNamespace; - private ILogHandler? logHandler; private CancellationTokenSource cts; private Task? worker; private Exception? workerException; @@ -27,11 +26,10 @@ namespace KubernetesWorkflow cts = new CancellationTokenSource(); } - public void Start(ILogHandler logHandler) + public void Start() { if (worker != null) throw new InvalidOperationException(); - this.logHandler = logHandler; cts = new CancellationTokenSource(); worker = Task.Run(Worker); } @@ -50,7 +48,9 @@ namespace KubernetesWorkflow public bool HasContainerCrashed() { using var client = new Kubernetes(config); - return HasContainerBeenRestarted(client); + var result = HasContainerBeenRestarted(client); + if (result) DownloadCrashedContainerLogs(client); + return result; } private void Worker() @@ -83,14 +83,16 @@ namespace KubernetesWorkflow private bool HasContainerBeenRestarted(Kubernetes client) { var podInfo = client.ReadNamespacedPod(podName, k8sNamespace); - return podInfo.Status.ContainerStatuses.Any(c => c.RestartCount > 0); + var result = podInfo.Status.ContainerStatuses.Any(c => c.RestartCount > 0); + if (result) log.Log("Pod crash detected for " + containerName); + return result; } private void DownloadCrashedContainerLogs(Kubernetes client) { - log.Log("Pod crash detected for " + containerName); using var stream = client.ReadNamespacedPodLog(podName, k8sNamespace, recipeName, previous: true); - logHandler!.Log(stream); + var handler = new WriteToFileLogHandler(log, "Crash detected for " + containerName); + handler.Log(stream); } } } diff --git a/Framework/KubernetesWorkflow/K8sCluster.cs b/Framework/KubernetesWorkflow/K8sCluster.cs index 366855fa..be87e1fe 100644 --- a/Framework/KubernetesWorkflow/K8sCluster.cs +++ b/Framework/KubernetesWorkflow/K8sCluster.cs @@ -16,6 +16,7 @@ namespace KubernetesWorkflow { var config = GetConfig(); UpdateHostAddress(config); + config.SkipTlsVerify = true; // Required for operation on Wings cluster. return config; } diff --git a/Framework/KubernetesWorkflow/K8sController.cs b/Framework/KubernetesWorkflow/K8sController.cs index b14267d3..e5fac7c4 100644 --- a/Framework/KubernetesWorkflow/K8sController.cs +++ b/Framework/KubernetesWorkflow/K8sController.cs @@ -43,6 +43,11 @@ namespace KubernetesWorkflow return new StartResult(cluster, containerRecipes, deployment, internalService, externalService); } + public void WaitUntilOnline(RunningContainer container) + { + WaitUntilDeploymentOnline(container); + } + public PodInfo GetPodInfo(RunningDeployment deployment) { var pod = GetPodForDeployment(deployment); @@ -59,14 +64,14 @@ namespace KubernetesWorkflow if (waitTillStopped) WaitUntilPodsForDeploymentAreOffline(startResult.Deployment); } - public void DownloadPodLog(RunningContainer container, ILogHandler logHandler, int? tailLines) + public void DownloadPodLog(RunningContainer container, ILogHandler logHandler, int? tailLines, bool? previous) { log.Debug(); var podName = GetPodName(container); var recipeName = container.Recipe.Name; - using var stream = client.Run(c => c.ReadNamespacedPodLog(podName, K8sNamespace, recipeName, tailLines: tailLines)); + using var stream = client.Run(c => c.ReadNamespacedPodLog(podName, K8sNamespace, recipeName, tailLines: tailLines, previous: previous)); logHandler.Log(stream); } @@ -110,7 +115,7 @@ namespace KubernetesWorkflow }); } - public void DeleteAllNamespacesStartingWith(string prefix) + public void DeleteAllNamespacesStartingWith(string prefix, bool wait) { log.Debug(); @@ -119,25 +124,28 @@ namespace KubernetesWorkflow foreach (var ns in namespaces) { - DeleteNamespace(ns); + DeleteNamespace(ns, wait); } } - public void DeleteNamespace() + public void DeleteNamespace(bool wait) { log.Debug(); if (IsNamespaceOnline(K8sNamespace)) { client.Run(c => c.DeleteNamespace(K8sNamespace, null, null, gracePeriodSeconds: 0)); + + if (wait) WaitUntilNamespaceDeleted(K8sNamespace); } } - public void DeleteNamespace(string ns) + public void DeleteNamespace(string ns, bool wait) { log.Debug(); if (IsNamespaceOnline(ns)) { client.Run(c => c.DeleteNamespace(ns, null, null, gracePeriodSeconds: 0)); + if (wait) WaitUntilNamespaceDeleted(ns); } } @@ -372,7 +380,6 @@ namespace KubernetesWorkflow }; client.Run(c => c.CreateNamespacedDeployment(deploymentSpec, K8sNamespace)); - WaitUntilDeploymentOnline(deploymentSpec.Metadata.Name); var name = deploymentSpec.Metadata.Name; return new RunningDeployment(name, podLabel); @@ -528,7 +535,7 @@ namespace KubernetesWorkflow } if (set.Memory.SizeInBytes != 0) { - result.Add("memory", new ResourceQuantity(set.Memory.ToSuffixNotation())); + result.Add("memory", new ResourceQuantity(set.Memory.SizeInBytes.ToString())); } return result; } @@ -701,14 +708,14 @@ namespace KubernetesWorkflow private string GetPodName(RunningContainer container) { - return GetPodForDeployment(container.RunningContainers.StartResult.Deployment).Metadata.Name; + return GetPodForDeployment(container.RunningPod.StartResult.Deployment).Metadata.Name; } private V1Pod GetPodForDeployment(RunningDeployment deployment) { return Time.Retry(() => GetPodForDeplomentInternal(deployment), // We will wait up to 1 minute, k8s might be moving pods around. - maxRetries: 6, + maxTimeout: TimeSpan.FromMinutes(1), retryTime: TimeSpan.FromSeconds(10), description: "Find pod by label for deployment."); } @@ -864,16 +871,45 @@ namespace KubernetesWorkflow private void WaitUntilNamespaceCreated() { - WaitUntil(() => IsNamespaceOnline(K8sNamespace)); + WaitUntil(() => IsNamespaceOnline(K8sNamespace), nameof(WaitUntilNamespaceCreated)); } - private void WaitUntilDeploymentOnline(string deploymentName) + private void WaitUntilNamespaceDeleted(string @namespace) + { + WaitUntil(() => !IsNamespaceOnline(@namespace), nameof(WaitUntilNamespaceDeleted)); + } + + private void WaitUntilDeploymentOnline(RunningContainer container) { WaitUntil(() => { - var deployment = client.Run(c => c.ReadNamespacedDeployment(deploymentName, K8sNamespace)); + CheckForCrash(container); + + var deployment = client.Run(c => c.ReadNamespacedDeployment(container.Recipe.Name, K8sNamespace)); return deployment?.Status.AvailableReplicas != null && deployment.Status.AvailableReplicas > 0; - }); + }, nameof(WaitUntilDeploymentOnline)); + } + + private void CheckForCrash(RunningContainer container) + { + var deploymentName = container.Recipe.Name; + var podName = GetPodName(container); + + var podInfo = client.Run(c => c.ReadNamespacedPod(podName, K8sNamespace)); + if (podInfo == null) return; + if (podInfo.Status == null) return; + if (podInfo.Status.ContainerStatuses == null) return; + + var result = podInfo.Status.ContainerStatuses.Any(c => c.RestartCount > 0); + if (result) + { + var msg = $"Pod crash detected for deployment {deploymentName} (pod:{podName})"; + log.Error(msg); + + DownloadPodLog(container, new WriteToFileLogHandler(log, msg), tailLines: null, previous: true); + + throw new Exception(msg); + } } private void WaitUntilDeploymentOffline(string deploymentName) @@ -883,7 +919,7 @@ namespace KubernetesWorkflow var deployments = client.Run(c => c.ListNamespacedDeployment(K8sNamespace)); var deployment = deployments.Items.SingleOrDefault(d => d.Metadata.Name == deploymentName); return deployment == null || deployment.Status.AvailableReplicas == 0; - }); + }, nameof(WaitUntilDeploymentOffline)); } private void WaitUntilPodsForDeploymentAreOffline(RunningDeployment deployment) @@ -892,19 +928,19 @@ namespace KubernetesWorkflow { var pods = FindPodsByLabel(deployment.PodLabel); return !pods.Any(); - }); + }, nameof(WaitUntilPodsForDeploymentAreOffline)); } - private void WaitUntil(Func predicate) + private void WaitUntil(Func predicate, string msg) { var sw = Stopwatch.Begin(log, true); try { - Time.WaitUntil(predicate, cluster.K8sOperationTimeout(), cluster.K8sOperationRetryDelay()); + Time.WaitUntil(predicate, cluster.K8sOperationTimeout(), cluster.K8sOperationRetryDelay(), msg); } finally { - sw.End("", 1); + sw.End(msg, 1); } } diff --git a/Framework/KubernetesWorkflow/K8sHooks.cs b/Framework/KubernetesWorkflow/K8sHooks.cs index 74bb93b4..4dad7401 100644 --- a/Framework/KubernetesWorkflow/K8sHooks.cs +++ b/Framework/KubernetesWorkflow/K8sHooks.cs @@ -5,18 +5,18 @@ namespace KubernetesWorkflow { public interface IK8sHooks { - void OnContainersStarted(RunningContainers runningContainers); - void OnContainersStopped(RunningContainers runningContainers); + void OnContainersStarted(RunningPod runningPod); + void OnContainersStopped(RunningPod runningPod); void OnContainerRecipeCreated(ContainerRecipe recipe); } public class DoNothingK8sHooks : IK8sHooks { - public void OnContainersStarted(RunningContainers runningContainers) + public void OnContainersStarted(RunningPod runningPod) { } - public void OnContainersStopped(RunningContainers runningContainers) + public void OnContainersStopped(RunningPod runningPod) { } diff --git a/Framework/KubernetesWorkflow/LogHandler.cs b/Framework/KubernetesWorkflow/LogHandler.cs index 77e57468..f185ba17 100644 --- a/Framework/KubernetesWorkflow/LogHandler.cs +++ b/Framework/KubernetesWorkflow/LogHandler.cs @@ -1,4 +1,6 @@ -namespace KubernetesWorkflow +using Logging; + +namespace KubernetesWorkflow { public interface ILogHandler { @@ -20,4 +22,25 @@ protected abstract void ProcessLine(string line); } + + public class WriteToFileLogHandler : LogHandler, ILogHandler + { + public WriteToFileLogHandler(ILog sourceLog, string description) + { + LogFile = sourceLog.CreateSubfile(); + + var msg = $"{description} -->> {LogFile.FullFilename}"; + sourceLog.Log(msg); + + LogFile.Write(msg); + LogFile.WriteRaw(description); + } + + public LogFile LogFile { get; } + + protected override void ProcessLine(string line) + { + LogFile.WriteRaw(line); + } + } } diff --git a/Framework/KubernetesWorkflow/Recipe/ContainerRecipeFactory.cs b/Framework/KubernetesWorkflow/Recipe/ContainerRecipeFactory.cs index 6b6ae2df..2c42143a 100644 --- a/Framework/KubernetesWorkflow/Recipe/ContainerRecipeFactory.cs +++ b/Framework/KubernetesWorkflow/Recipe/ContainerRecipeFactory.cs @@ -105,7 +105,7 @@ namespace KubernetesWorkflow.Recipe protected void AddVolume(string name, string mountPath, string? subPath = null, string? secret = null, string? hostPath = null) { - var size = 10.MB().ToSuffixNotation(); + var size = 10.MB().SizeInBytes.ToString(); volumeMounts.Add(new VolumeMount(name, mountPath, subPath, size, secret, hostPath)); } @@ -114,7 +114,7 @@ namespace KubernetesWorkflow.Recipe volumeMounts.Add(new VolumeMount( $"autovolume-{Guid.NewGuid().ToString().ToLowerInvariant()}", mountPath, - resourceQuantity: volumeSize.ToSuffixNotation())); + resourceQuantity: volumeSize.SizeInBytes.ToString())); } protected void Additional(object userData) diff --git a/Framework/KubernetesWorkflow/RunningPod.cs b/Framework/KubernetesWorkflow/RunningPod.cs index e86b2aa6..43a05cc1 100644 --- a/Framework/KubernetesWorkflow/RunningPod.cs +++ b/Framework/KubernetesWorkflow/RunningPod.cs @@ -1,8 +1,5 @@ -using k8s; -using k8s.Models; -using KubernetesWorkflow.Recipe; +using KubernetesWorkflow.Recipe; using KubernetesWorkflow.Types; -using Newtonsoft.Json; namespace KubernetesWorkflow { diff --git a/Framework/KubernetesWorkflow/StartupWorkflow.cs b/Framework/KubernetesWorkflow/StartupWorkflow.cs index 4d1a2f67..6a4d50ca 100644 --- a/Framework/KubernetesWorkflow/StartupWorkflow.cs +++ b/Framework/KubernetesWorkflow/StartupWorkflow.cs @@ -9,16 +9,16 @@ namespace KubernetesWorkflow public interface IStartupWorkflow { IKnownLocations GetAvailableLocations(); - RunningContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig); - RunningContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig); + FutureContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig); + FutureContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig); PodInfo GetPodInfo(RunningContainer container); - PodInfo GetPodInfo(RunningContainers containers); + PodInfo GetPodInfo(RunningPod pod); CrashWatcher CreateCrashWatcher(RunningContainer container); - void Stop(RunningContainers containers, bool waitTillStopped); - void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null); + void Stop(RunningPod pod, bool waitTillStopped); + void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null, bool? previous = null); string ExecuteCommand(RunningContainer container, string command, params string[] args); - void DeleteNamespace(); - void DeleteNamespacesStartingWith(string namespacePrefix); + void DeleteNamespace(bool wait); + void DeleteNamespacesStartingWith(string namespacePrefix, bool wait); } public class StartupWorkflow : IStartupWorkflow @@ -45,12 +45,12 @@ namespace KubernetesWorkflow return locationProvider.GetAvailableLocations(); } - public RunningContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig) + public FutureContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig) { return Start(numberOfContainers, KnownLocations.UnspecifiedLocation, recipeFactory, startupConfig); } - public RunningContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig) + public FutureContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig) { return K8s(controller => { @@ -60,25 +60,36 @@ namespace KubernetesWorkflow var startResult = controller.BringOnline(recipes, location); var containers = CreateContainers(startResult, recipes, startupConfig); - var rc = new RunningContainers(startupConfig, startResult, containers); + var rc = new RunningPod(startupConfig, startResult, containers); cluster.Configuration.Hooks.OnContainersStarted(rc); if (startResult.ExternalService != null) { componentFactory.Update(controller); } - return rc; + return new FutureContainers(rc, this); + }); + } + + public void WaitUntilOnline(RunningPod rc) + { + K8s(controller => + { + foreach (var c in rc.Containers) + { + controller.WaitUntilOnline(c); + } }); } public PodInfo GetPodInfo(RunningContainer container) { - return K8s(c => c.GetPodInfo(container.RunningContainers.StartResult.Deployment)); + return K8s(c => c.GetPodInfo(container.RunningPod.StartResult.Deployment)); } - public PodInfo GetPodInfo(RunningContainers containers) + public PodInfo GetPodInfo(RunningPod pod) { - return K8s(c => c.GetPodInfo(containers.StartResult.Deployment)); + return K8s(c => c.GetPodInfo(pod.StartResult.Deployment)); } public CrashWatcher CreateCrashWatcher(RunningContainer container) @@ -86,20 +97,20 @@ namespace KubernetesWorkflow return K8s(c => c.CreateCrashWatcher(container)); } - public void Stop(RunningContainers runningContainers, bool waitTillStopped) + public void Stop(RunningPod runningPod, bool waitTillStopped) { K8s(controller => { - controller.Stop(runningContainers.StartResult, waitTillStopped); - cluster.Configuration.Hooks.OnContainersStopped(runningContainers); + controller.Stop(runningPod.StartResult, waitTillStopped); + cluster.Configuration.Hooks.OnContainersStopped(runningPod); }); } - public void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null) + public void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null, bool? previous = null) { K8s(controller => { - controller.DownloadPodLog(container, logHandler, tailLines); + controller.DownloadPodLog(container, logHandler, tailLines, previous); }); } @@ -111,19 +122,19 @@ namespace KubernetesWorkflow }); } - public void DeleteNamespace() + public void DeleteNamespace(bool wait) { K8s(controller => { - controller.DeleteNamespace(); + controller.DeleteNamespace(wait); }); } - public void DeleteNamespacesStartingWith(string namespacePrefix) + public void DeleteNamespacesStartingWith(string namespacePrefix, bool wait) { K8s(controller => { - controller.DeleteAllNamespacesStartingWith(namespacePrefix); + controller.DeleteAllNamespacesStartingWith(namespacePrefix, wait); }); } diff --git a/Framework/KubernetesWorkflow/Types/FutureContainers.cs b/Framework/KubernetesWorkflow/Types/FutureContainers.cs new file mode 100644 index 00000000..296be534 --- /dev/null +++ b/Framework/KubernetesWorkflow/Types/FutureContainers.cs @@ -0,0 +1,20 @@ +namespace KubernetesWorkflow.Types +{ + public class FutureContainers + { + private readonly RunningPod runningPod; + private readonly StartupWorkflow workflow; + + public FutureContainers(RunningPod runningPod, StartupWorkflow workflow) + { + this.runningPod = runningPod; + this.workflow = workflow; + } + + public RunningPod WaitForOnline() + { + workflow.WaitUntilOnline(runningPod); + return runningPod; + } + } +} diff --git a/Framework/KubernetesWorkflow/Types/RunningContainer.cs b/Framework/KubernetesWorkflow/Types/RunningContainer.cs index f391e457..b0fbc02e 100644 --- a/Framework/KubernetesWorkflow/Types/RunningContainer.cs +++ b/Framework/KubernetesWorkflow/Types/RunningContainer.cs @@ -19,7 +19,7 @@ namespace KubernetesWorkflow.Types public ContainerAddress[] Addresses { get; } [JsonIgnore] - public RunningContainers RunningContainers { get; internal set; } = null!; + public RunningPod RunningPod { get; internal set; } = null!; public Address GetAddress(ILog log, string portTag) { diff --git a/Framework/KubernetesWorkflow/Types/RunningContainers.cs b/Framework/KubernetesWorkflow/Types/RunningPod.cs similarity index 55% rename from Framework/KubernetesWorkflow/Types/RunningContainers.cs rename to Framework/KubernetesWorkflow/Types/RunningPod.cs index 9a6e5f3a..4d03cf36 100644 --- a/Framework/KubernetesWorkflow/Types/RunningContainers.cs +++ b/Framework/KubernetesWorkflow/Types/RunningPod.cs @@ -2,15 +2,15 @@ namespace KubernetesWorkflow.Types { - public class RunningContainers + public class RunningPod { - public RunningContainers(StartupConfig startupConfig, StartResult startResult, RunningContainer[] containers) + public RunningPod(StartupConfig startupConfig, StartResult startResult, RunningContainer[] containers) { StartupConfig = startupConfig; StartResult = startResult; Containers = containers; - foreach (var c in containers) c.RunningContainers = this; + foreach (var c in containers) c.RunningPod = this; } public StartupConfig StartupConfig { get; } @@ -20,7 +20,7 @@ namespace KubernetesWorkflow.Types [JsonIgnore] public string Name { - get { return $"{Containers.Length}x '{Containers.First().Name}'"; } + get { return $"'{string.Join("&", Containers.Select(c => c.Name).ToArray())}'"; } } public string Describe() @@ -31,12 +31,7 @@ namespace KubernetesWorkflow.Types public static class RunningContainersExtensions { - public static RunningContainer[] Containers(this RunningContainers[] runningContainers) - { - return runningContainers.SelectMany(c => c.Containers).ToArray(); - } - - public static string Describe(this RunningContainers[] runningContainers) + public static string Describe(this RunningPod[] runningContainers) { return string.Join(",", runningContainers.Select(c => c.Describe())); } diff --git a/Framework/NethereumWorkflow/BlockUtils/BlockTimeFinder.cs b/Framework/NethereumWorkflow/BlockUtils/BlockTimeFinder.cs index e4c73d50..d9a6ac29 100644 --- a/Framework/NethereumWorkflow/BlockUtils/BlockTimeFinder.cs +++ b/Framework/NethereumWorkflow/BlockUtils/BlockTimeFinder.cs @@ -18,6 +18,14 @@ namespace NethereumWorkflow.BlockUtils bounds = new BlockchainBounds(cache, web3); } + public BlockTimeEntry Get(ulong blockNumber) + { + bounds.Initialize(); + var b = cache.Get(blockNumber); + if (b != null) return b; + return GetBlock(blockNumber); + } + public ulong? GetHighestBlockNumberBefore(DateTime moment) { bounds.Initialize(); @@ -38,7 +46,7 @@ namespace NethereumWorkflow.BlockUtils private ulong Log(Func operation) { - var sw = Stopwatch.Begin(log, nameof(BlockTimeFinder)); + var sw = Stopwatch.Begin(log, nameof(BlockTimeFinder), true); var result = operation(); sw.End($"(Bounds: [{bounds.Genesis.BlockNumber}-{bounds.Current.BlockNumber}] Cache: {cache.Size})"); diff --git a/Framework/NethereumWorkflow/NethereumInteraction.cs b/Framework/NethereumWorkflow/NethereumInteraction.cs index 197cf2d0..01f26bf5 100644 --- a/Framework/NethereumWorkflow/NethereumInteraction.cs +++ b/Framework/NethereumWorkflow/NethereumInteraction.cs @@ -117,9 +117,17 @@ namespace NethereumWorkflow } return new BlockInterval( + timeRange: timeRange, from: fromBlock.Value, to: toBlock.Value ); } + + public BlockTimeEntry GetBlockForNumber(ulong number) + { + var wrapper = new Web3Wrapper(web3, log); + var blockTimeFinder = new BlockTimeFinder(blockCache, wrapper, log); + return blockTimeFinder.Get(number); + } } } diff --git a/Framework/Utils/BlockInterval.cs b/Framework/Utils/BlockInterval.cs index 79229bfa..64430488 100644 --- a/Framework/Utils/BlockInterval.cs +++ b/Framework/Utils/BlockInterval.cs @@ -2,7 +2,7 @@ { public class BlockInterval { - public BlockInterval(ulong from, ulong to) + public BlockInterval(TimeRange timeRange, ulong from, ulong to) { if (from < to) { @@ -14,10 +14,13 @@ From = to; To = from; } + TimeRange = timeRange; } public ulong From { get; } public ulong To { get; } + public TimeRange TimeRange { get; } + public ulong NumberOfBlocks => To - From; public override string ToString() { diff --git a/Framework/Utils/Formatter.cs b/Framework/Utils/Formatter.cs index 1ea15503..7422c84e 100644 --- a/Framework/Utils/Formatter.cs +++ b/Framework/Utils/Formatter.cs @@ -1,4 +1,6 @@ -namespace Utils +using System.Globalization; + +namespace Utils { public static class Formatter { @@ -10,7 +12,7 @@ var sizeOrder = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024))); var digit = Math.Round(bytes / Math.Pow(1024, sizeOrder), 1); - return digit.ToString() + sizeSuffixes[sizeOrder]; + return digit.ToString(CultureInfo.InvariantCulture) + sizeSuffixes[sizeOrder]; } } } diff --git a/Framework/Utils/NumberSource.cs b/Framework/Utils/NumberSource.cs index 2c7266fe..69d51899 100644 --- a/Framework/Utils/NumberSource.cs +++ b/Framework/Utils/NumberSource.cs @@ -2,6 +2,7 @@ { public class NumberSource { + private readonly object @lock = new object(); private int number; public NumberSource(int start) @@ -11,8 +12,12 @@ public int GetNextNumber() { - var n = number; - number++; + var n = -1; + lock (@lock) + { + n = number; + number++; + } return n; } } diff --git a/Framework/Utils/Retry.cs b/Framework/Utils/Retry.cs new file mode 100644 index 00000000..ec05ea66 --- /dev/null +++ b/Framework/Utils/Retry.cs @@ -0,0 +1,131 @@ +namespace Utils +{ + public class Retry + { + private readonly string description; + private readonly TimeSpan maxTimeout; + private readonly TimeSpan sleepAfterFail; + private readonly Action onFail; + + public Retry(string description, TimeSpan maxTimeout, TimeSpan sleepAfterFail, Action onFail) + { + this.description = description; + this.maxTimeout = maxTimeout; + this.sleepAfterFail = sleepAfterFail; + this.onFail = onFail; + } + + public void Run(Action task) + { + var run = new RetryRun(description, task, maxTimeout, sleepAfterFail, onFail); + run.Run(); + } + + public T Run(Func task) + { + T? result = default; + + var run = new RetryRun(description, () => + { + result = task(); + }, maxTimeout, sleepAfterFail, onFail); + run.Run(); + + return result!; + } + + private class RetryRun + { + private readonly string description; + private readonly Action task; + private readonly TimeSpan maxTimeout; + private readonly TimeSpan sleepAfterFail; + private readonly Action onFail; + private readonly DateTime start = DateTime.UtcNow; + private readonly List failures = new List(); + private int tryNumber; + private DateTime tryStart; + + public RetryRun(string description, Action task, TimeSpan maxTimeout, TimeSpan sleepAfterFail, Action onFail) + { + this.description = description; + this.task = task; + this.maxTimeout = maxTimeout; + this.sleepAfterFail = sleepAfterFail; + this.onFail = onFail; + + tryNumber = 0; + tryStart = DateTime.UtcNow; + } + + public void Run() + { + while (true) + { + CheckMaximums(); + + tryNumber++; + tryStart = DateTime.UtcNow; + try + { + task(); + return; + } + catch (Exception ex) + { + var failure = CaptureFailure(ex); + onFail(failure); + Time.Sleep(sleepAfterFail); + } + } + } + + private Failure CaptureFailure(Exception ex) + { + var f = new Failure(ex, DateTime.UtcNow - tryStart, tryNumber); + failures.Add(f); + return f; + } + + private void CheckMaximums() + { + if (Duration() > maxTimeout) Fail(); + } + + private void Fail() + { + throw new TimeoutException($"Retry '{description}' timed out after {tryNumber} tries over {Time.FormatDuration(Duration())}: {GetFailureReport}", + new AggregateException(failures.Select(f => f.Exception))); + } + + private string GetFailureReport() + { + return Environment.NewLine + string.Join(Environment.NewLine, failures.Select(f => f.Describe())); + } + + private TimeSpan Duration() + { + return DateTime.UtcNow - start; + } + } + } + + public class Failure + { + public Failure(Exception exception, TimeSpan duration, int tryNumber) + { + Exception = exception; + Duration = duration; + TryNumber = tryNumber; + } + + public Exception Exception { get; } + public TimeSpan Duration { get; } + public int TryNumber { get; } + + public string Describe() + { + return $"Try {TryNumber} failed after {Time.FormatDuration(Duration)} with exception '{Exception}'"; + } + } +} diff --git a/Framework/Utils/Time.cs b/Framework/Utils/Time.cs index 82a836e6..42051af7 100644 --- a/Framework/Utils/Time.cs +++ b/Framework/Utils/Time.cs @@ -18,6 +18,12 @@ task.Wait(); } + public static string FormatDuration(TimeSpan? d) + { + if (d == null) return "[NULL]"; + return FormatDuration(d.Value); + } + public static string FormatDuration(TimeSpan d) { var result = ""; @@ -57,100 +63,70 @@ return result; } - public static void WaitUntil(Func predicate) + public static void WaitUntil(Func predicate, string msg) { - WaitUntil(predicate, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(1)); + WaitUntil(predicate, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(1), msg); } - public static void WaitUntil(Func predicate, TimeSpan timeout, TimeSpan retryDelay) + public static void WaitUntil(Func predicate, TimeSpan timeout, TimeSpan retryDelay, string msg) { var start = DateTime.UtcNow; + var tries = 1; var state = predicate(); while (!state) { - if (DateTime.UtcNow - start > timeout) + var duration = DateTime.UtcNow - start; + if (duration > timeout) { - throw new TimeoutException("Operation timed out."); + throw new TimeoutException($"Operation timed out after {tries} tries over (total) {FormatDuration(duration)}. '{msg}'"); } Sleep(retryDelay); state = predicate(); + tries++; } } public static void Retry(Action action, string description) { - Retry(action, 1, description); + Retry(action, TimeSpan.FromSeconds(30), description); } public static T Retry(Func action, string description) { - return Retry(action, 1, description); + return Retry(action, TimeSpan.FromSeconds(30), description); } - public static void Retry(Action action, int maxRetries, string description) + public static void Retry(Action action, TimeSpan maxTimeout, string description) { - Retry(action, maxRetries, TimeSpan.FromSeconds(5), description); + Retry(action, maxTimeout, TimeSpan.FromSeconds(5), description); } - public static T Retry(Func action, int maxRetries, string description) + public static T Retry(Func action, TimeSpan maxTimeout, string description) { - return Retry(action, maxRetries, TimeSpan.FromSeconds(5), description); + return Retry(action, maxTimeout, TimeSpan.FromSeconds(5), description); } - public static void Retry(Action action, int maxRetries, TimeSpan retryTime, string description) + public static void Retry(Action action, TimeSpan maxTimeout, TimeSpan retryTime, string description) { - var start = DateTime.UtcNow; - var retries = 0; - var exceptions = new List(); - while (true) - { - if (retries > maxRetries) - { - var duration = DateTime.UtcNow - start; - throw new TimeoutException($"Retry '{description}' timed out after {maxRetries} tries over {Time.FormatDuration(duration)}.", new AggregateException(exceptions)); - } - - try - { - action(); - return; - } - catch (Exception ex) - { - exceptions.Add(ex); - retries++; - } - - Sleep(retryTime); - } + Retry(action, maxTimeout, retryTime, description, f => { }); } - public static T Retry(Func action, int maxRetries, TimeSpan retryTime, string description) + public static T Retry(Func action, TimeSpan maxTimeout, TimeSpan retryTime, string description) { - var start = DateTime.UtcNow; - var retries = 0; - var exceptions = new List(); - while (true) - { - if (retries > maxRetries) - { - var duration = DateTime.UtcNow - start; - throw new TimeoutException($"Retry '{description}' timed out after {maxRetries} tries over {Time.FormatDuration(duration)}.", new AggregateException(exceptions)); - } + return Retry(action, maxTimeout, retryTime, description, f => { }); + } - try - { - return action(); - } - catch (Exception ex) - { - exceptions.Add(ex); - retries++; - } + public static void Retry(Action action, TimeSpan maxTimeout, TimeSpan retryTime, string description, Action onFail) + { + var r = new Retry(description, maxTimeout, retryTime, onFail); + r.Run(action); + } - Sleep(retryTime); - } + public static T Retry(Func action, TimeSpan maxTimeout, TimeSpan retryTime, string description, Action onFail) + { + var r = new Retry(description, maxTimeout, retryTime, onFail); + return r.Run(action); } } } diff --git a/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainEvents.cs b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainEvents.cs new file mode 100644 index 00000000..25d08513 --- /dev/null +++ b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainEvents.cs @@ -0,0 +1,68 @@ +using CodexContractsPlugin.Marketplace; +using Utils; + +namespace CodexContractsPlugin.ChainMonitor +{ + public class ChainEvents + { + private ChainEvents( + BlockInterval blockInterval, + Request[] requests, + RequestFulfilledEventDTO[] fulfilled, + RequestCancelledEventDTO[] cancelled, + SlotFilledEventDTO[] slotFilled, + SlotFreedEventDTO[] slotFreed + ) + { + BlockInterval = blockInterval; + Requests = requests; + Fulfilled = fulfilled; + Cancelled = cancelled; + SlotFilled = slotFilled; + SlotFreed = slotFreed; + } + + public BlockInterval BlockInterval { get; } + public Request[] Requests { get; } + public RequestFulfilledEventDTO[] Fulfilled { get; } + public RequestCancelledEventDTO[] Cancelled { get; } + public SlotFilledEventDTO[] SlotFilled { get; } + public SlotFreedEventDTO[] SlotFreed { get; } + + public IHasBlock[] All + { + get + { + var all = new List(); + all.AddRange(Requests); + all.AddRange(Fulfilled); + all.AddRange(Cancelled); + all.AddRange(SlotFilled); + all.AddRange(SlotFreed); + return all.ToArray(); + } + } + + public static ChainEvents FromBlockInterval(ICodexContracts contracts, BlockInterval blockInterval) + { + return FromContractEvents(contracts.GetEvents(blockInterval)); + } + + public static ChainEvents FromTimeRange(ICodexContracts contracts, TimeRange timeRange) + { + return FromContractEvents(contracts.GetEvents(timeRange)); + } + + public static ChainEvents FromContractEvents(ICodexContractsEvents events) + { + return new ChainEvents( + events.BlockInterval, + events.GetStorageRequests(), + events.GetRequestFulfilledEvents(), + events.GetRequestCancelledEvents(), + events.GetSlotFilledEvents(), + events.GetSlotFreedEvents() + ); + } + } +} diff --git a/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainState.cs b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainState.cs new file mode 100644 index 00000000..23ac7dc5 --- /dev/null +++ b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainState.cs @@ -0,0 +1,170 @@ +using CodexContractsPlugin.Marketplace; +using GethPlugin; +using Logging; +using NethereumWorkflow.BlockUtils; +using System.Numerics; +using Utils; + +namespace CodexContractsPlugin.ChainMonitor +{ + public interface IChainStateChangeHandler + { + void OnNewRequest(RequestEvent requestEvent); + void OnRequestFinished(RequestEvent requestEvent); + void OnRequestFulfilled(RequestEvent requestEvent); + void OnRequestCancelled(RequestEvent requestEvent); + void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex); + void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex); + } + + public class RequestEvent + { + public RequestEvent(BlockTimeEntry block, IChainStateRequest request) + { + Block = block; + Request = request; + } + + public BlockTimeEntry Block { get; } + public IChainStateRequest Request { get; } + } + + public class ChainState + { + private readonly List requests = new List(); + private readonly ILog log; + private readonly ICodexContracts contracts; + private readonly IChainStateChangeHandler handler; + + public ChainState(ILog log, ICodexContracts contracts, IChainStateChangeHandler changeHandler, DateTime startUtc) + { + this.log = new LogPrefixer(log, "(ChainState) "); + this.contracts = contracts; + handler = changeHandler; + StartUtc = startUtc; + TotalSpan = new TimeRange(startUtc, startUtc); + } + + public TimeRange TotalSpan { get; private set; } + public IChainStateRequest[] Requests => requests.ToArray(); + + public DateTime StartUtc { get; } + + public void Update() + { + Update(DateTime.UtcNow); + } + + public void Update(DateTime toUtc) + { + var span = new TimeRange(TotalSpan.To, toUtc); + var events = ChainEvents.FromTimeRange(contracts, span); + Apply(events); + + TotalSpan = new TimeRange(TotalSpan.From, span.To); + } + + private void Apply(ChainEvents events) + { + if (events.BlockInterval.TimeRange.From < TotalSpan.From) + throw new Exception("Attempt to update ChainState with set of events from before its current record."); + + log.Log($"ChainState updating: {events.BlockInterval}"); + + // Run through each block and apply the events to the state in order. + var span = events.BlockInterval.TimeRange.Duration; + var numBlocks = events.BlockInterval.NumberOfBlocks; + var spanPerBlock = span / numBlocks; + + var eventUtc = events.BlockInterval.TimeRange.From; + for (var b = events.BlockInterval.From; b <= events.BlockInterval.To; b++) + { + var blockEvents = events.All.Where(e => e.Block.BlockNumber == b).ToArray(); + ApplyEvents(b, blockEvents, eventUtc); + + eventUtc += spanPerBlock; + } + } + + private void ApplyEvents(ulong blockNumber, IHasBlock[] blockEvents, DateTime eventsUtc) + { + foreach (var e in blockEvents) + { + dynamic d = e; + ApplyEvent(d); + } + + ApplyTimeImplicitEvents(blockNumber, eventsUtc); + } + + private void ApplyEvent(Request request) + { + if (requests.Any(r => Equal(r.Request.RequestId, request.RequestId))) + throw new Exception("Received NewRequest event for id that already exists."); + + var newRequest = new ChainStateRequest(log, request, RequestState.New); + requests.Add(newRequest); + + handler.OnNewRequest(new RequestEvent(request.Block, newRequest)); + } + + private void ApplyEvent(RequestFulfilledEventDTO @event) + { + var r = FindRequest(@event.RequestId); + if (r == null) return; + r.UpdateState(@event.Block.BlockNumber, RequestState.Started); + handler.OnRequestFulfilled(new RequestEvent(@event.Block, r)); + } + + private void ApplyEvent(RequestCancelledEventDTO @event) + { + var r = FindRequest(@event.RequestId); + if (r == null) return; + r.UpdateState(@event.Block.BlockNumber, RequestState.Cancelled); + handler.OnRequestCancelled(new RequestEvent(@event.Block, r)); + } + + private void ApplyEvent(SlotFilledEventDTO @event) + { + var r = FindRequest(@event.RequestId); + if (r == null) return; + r.Hosts.Add(@event.Host, (int)@event.SlotIndex); + r.Log($"[{@event.Block.BlockNumber}] SlotFilled (host:'{@event.Host}', slotIndex:{@event.SlotIndex})"); + handler.OnSlotFilled(new RequestEvent(@event.Block, r), @event.Host, @event.SlotIndex); + } + + private void ApplyEvent(SlotFreedEventDTO @event) + { + var r = FindRequest(@event.RequestId); + if (r == null) return; + r.Hosts.RemoveHost((int)@event.SlotIndex); + r.Log($"[{@event.Block.BlockNumber}] SlotFreed (slotIndex:{@event.SlotIndex})"); + handler.OnSlotFreed(new RequestEvent(@event.Block, r), @event.SlotIndex); + } + + private void ApplyTimeImplicitEvents(ulong blockNumber, DateTime eventsUtc) + { + foreach (var r in requests) + { + if (r.State == RequestState.Started + && r.FinishedUtc < eventsUtc) + { + r.UpdateState(blockNumber, RequestState.Finished); + handler.OnRequestFinished(new RequestEvent(new BlockTimeEntry(blockNumber, eventsUtc), r)); + } + } + } + + private ChainStateRequest? FindRequest(byte[] requestId) + { + var r = requests.SingleOrDefault(r => Equal(r.Request.RequestId, requestId)); + if (r == null) log.Log("Unable to find request by ID!"); + return r; + } + + private bool Equal(byte[] a, byte[] b) + { + return a.SequenceEqual(b); + } + } +} diff --git a/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainStateRequest.cs b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainStateRequest.cs new file mode 100644 index 00000000..d908d193 --- /dev/null +++ b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainStateRequest.cs @@ -0,0 +1,80 @@ +using CodexContractsPlugin.Marketplace; +using GethPlugin; +using Logging; + +namespace CodexContractsPlugin.ChainMonitor +{ + public interface IChainStateRequest + { + Request Request { get; } + RequestState State { get; } + DateTime ExpiryUtc { get; } + DateTime FinishedUtc { get; } + EthAddress Client { get; } + RequestHosts Hosts { get; } + } + + public class ChainStateRequest : IChainStateRequest + { + private readonly ILog log; + + public ChainStateRequest(ILog log, Request request, RequestState state) + { + this.log = log; + Request = request; + State = state; + + ExpiryUtc = request.Block.Utc + TimeSpan.FromSeconds((double)request.Expiry); + FinishedUtc = request.Block.Utc + TimeSpan.FromSeconds((double)request.Ask.Duration); + + Log($"[{request.Block.BlockNumber}] Created as {State}."); + + Client = new EthAddress(request.Client); + Hosts = new RequestHosts(); + } + + public Request Request { get; } + public RequestState State { get; private set; } + public DateTime ExpiryUtc { get; } + public DateTime FinishedUtc { get; } + public EthAddress Client { get; } + public RequestHosts Hosts { get; } + + public void UpdateState(ulong blockNumber, RequestState newState) + { + Log($"[{blockNumber}] Transit: {State} -> {newState}"); + State = newState; + } + + public void Log(string msg) + { + log.Log($"Request '{Request.Id}': {msg}"); + } + } + + public class RequestHosts + { + private readonly Dictionary hosts = new Dictionary(); + + public void Add(EthAddress host, int index) + { + hosts.Add(index, host); + } + + public void RemoveHost(int index) + { + hosts.Remove(index); + } + + public EthAddress? GetHost(int index) + { + if (!hosts.ContainsKey(index)) return null; + return hosts[index]; + } + + public EthAddress[] GetHosts() + { + return hosts.Values.ToArray(); + } + } +} diff --git a/ProjectPlugins/CodexContractsPlugin/ChainMonitor/DoNothingChainEventHandler.cs b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/DoNothingChainEventHandler.cs new file mode 100644 index 00000000..c564ec8a --- /dev/null +++ b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/DoNothingChainEventHandler.cs @@ -0,0 +1,32 @@ +using GethPlugin; +using System.Numerics; + +namespace CodexContractsPlugin.ChainMonitor +{ + public class DoNothingChainEventHandler : IChainStateChangeHandler + { + public void OnNewRequest(RequestEvent requestEvent) + { + } + + public void OnRequestCancelled(RequestEvent requestEvent) + { + } + + public void OnRequestFinished(RequestEvent requestEvent) + { + } + + public void OnRequestFulfilled(RequestEvent requestEvent) + { + } + + public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex) + { + } + + public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex) + { + } + } +} diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs index 4279347a..58fbb29b 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs @@ -2,9 +2,10 @@ using GethPlugin; using Logging; using Nethereum.ABI; -using Nethereum.Hex.HexTypes; using Nethereum.Util; using NethereumWorkflow; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Utils; namespace CodexContractsPlugin @@ -19,15 +20,13 @@ namespace CodexContractsPlugin TestToken GetTestTokenBalance(IHasEthAddress owner); TestToken GetTestTokenBalance(EthAddress ethAddress); - Request[] GetStorageRequests(BlockInterval blockRange); + ICodexContractsEvents GetEvents(TimeRange timeRange); + ICodexContractsEvents GetEvents(BlockInterval blockInterval); EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex); RequestState GetRequestState(Request request); - RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange); - RequestCancelledEventDTO[] GetRequestCancelledEvents(BlockInterval blockRange); - SlotFilledEventDTO[] GetSlotFilledEvents(BlockInterval blockRange); - SlotFreedEventDTO[] GetSlotFreedEvents(BlockInterval blockRange); } + [JsonConverter(typeof(StringEnumConverter))] public enum RequestState { New, @@ -63,7 +62,7 @@ namespace CodexContractsPlugin public string MintTestTokens(EthAddress ethAddress, TestToken testTokens) { - return StartInteraction().MintTestTokens(ethAddress, testTokens.Amount, Deployment.TokenAddress); + return StartInteraction().MintTestTokens(ethAddress, testTokens.TstWei, Deployment.TokenAddress); } public TestToken GetTestTokenBalance(IHasEthAddress owner) @@ -74,68 +73,17 @@ namespace CodexContractsPlugin public TestToken GetTestTokenBalance(EthAddress ethAddress) { var balance = StartInteraction().GetBalance(Deployment.TokenAddress, ethAddress.Address); - return balance.TestTokens(); + return balance.TstWei(); } - public Request[] GetStorageRequests(BlockInterval blockRange) + public ICodexContractsEvents GetEvents(TimeRange timeRange) { - var events = gethNode.GetEvents(Deployment.MarketplaceAddress, blockRange); - var i = StartInteraction(); - return events - .Select(e => - { - var requestEvent = i.GetRequest(Deployment.MarketplaceAddress, e.Event.RequestId); - var result = requestEvent.ReturnValue1; - result.BlockNumber = e.Log.BlockNumber.ToUlong(); - result.RequestId = e.Event.RequestId; - return result; - }) - .ToArray(); + return GetEvents(gethNode.ConvertTimeRangeToBlockRange(timeRange)); } - public RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange) + public ICodexContractsEvents GetEvents(BlockInterval blockInterval) { - var events = gethNode.GetEvents(Deployment.MarketplaceAddress, blockRange); - return events.Select(e => - { - var result = e.Event; - result.BlockNumber = e.Log.BlockNumber.ToUlong(); - return result; - }).ToArray(); - } - - public RequestCancelledEventDTO[] GetRequestCancelledEvents(BlockInterval blockRange) - { - var events = gethNode.GetEvents(Deployment.MarketplaceAddress, blockRange); - return events.Select(e => - { - var result = e.Event; - result.BlockNumber = e.Log.BlockNumber.ToUlong(); - return result; - }).ToArray(); - } - - public SlotFilledEventDTO[] GetSlotFilledEvents(BlockInterval blockRange) - { - var events = gethNode.GetEvents(Deployment.MarketplaceAddress, blockRange); - return events.Select(e => - { - var result = e.Event; - result.BlockNumber = e.Log.BlockNumber.ToUlong(); - result.Host = GetEthAddressFromTransaction(e.Log.TransactionHash); - return result; - }).ToArray(); - } - - public SlotFreedEventDTO[] GetSlotFreedEvents(BlockInterval blockRange) - { - var events = gethNode.GetEvents(Deployment.MarketplaceAddress, blockRange); - return events.Select(e => - { - var result = e.Event; - result.BlockNumber = e.Log.BlockNumber.ToUlong(); - return result; - }).ToArray(); + return new CodexContractsEvents(log, gethNode, Deployment, blockInterval); } public EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex) @@ -166,12 +114,6 @@ namespace CodexContractsPlugin return gethNode.Call(Deployment.MarketplaceAddress, func); } - private EthAddress GetEthAddressFromTransaction(string transactionHash) - { - var transaction = gethNode.GetTransaction(transactionHash); - return new EthAddress(transaction.From); - } - private ContractInteractions StartInteraction() { return new ContractInteractions(log, gethNode); diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs new file mode 100644 index 00000000..cf7b2d74 --- /dev/null +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs @@ -0,0 +1,108 @@ +using CodexContractsPlugin.Marketplace; +using GethPlugin; +using Logging; +using Nethereum.Hex.HexTypes; +using NethereumWorkflow.BlockUtils; +using Utils; + +namespace CodexContractsPlugin +{ + public interface ICodexContractsEvents + { + BlockInterval BlockInterval { get; } + Request[] GetStorageRequests(); + RequestFulfilledEventDTO[] GetRequestFulfilledEvents(); + RequestCancelledEventDTO[] GetRequestCancelledEvents(); + SlotFilledEventDTO[] GetSlotFilledEvents(); + SlotFreedEventDTO[] GetSlotFreedEvents(); + } + + public class CodexContractsEvents : ICodexContractsEvents + { + private readonly ILog log; + private readonly IGethNode gethNode; + private readonly CodexContractsDeployment deployment; + + public CodexContractsEvents(ILog log, IGethNode gethNode, CodexContractsDeployment deployment, BlockInterval blockInterval) + { + this.log = log; + this.gethNode = gethNode; + this.deployment = deployment; + BlockInterval = blockInterval; + } + + public BlockInterval BlockInterval { get; } + + public Request[] GetStorageRequests() + { + var events = gethNode.GetEvents(deployment.MarketplaceAddress, BlockInterval); + var i = new ContractInteractions(log, gethNode); + return events + .Select(e => + { + var requestEvent = i.GetRequest(deployment.MarketplaceAddress, e.Event.RequestId); + var result = requestEvent.ReturnValue1; + result.Block = GetBlock(e.Log.BlockNumber.ToUlong()); + result.RequestId = e.Event.RequestId; + return result; + }) + .ToArray(); + } + + public RequestFulfilledEventDTO[] GetRequestFulfilledEvents() + { + var events = gethNode.GetEvents(deployment.MarketplaceAddress, BlockInterval); + return events.Select(e => + { + var result = e.Event; + result.Block = GetBlock(e.Log.BlockNumber.ToUlong()); + return result; + }).ToArray(); + } + + public RequestCancelledEventDTO[] GetRequestCancelledEvents() + { + var events = gethNode.GetEvents(deployment.MarketplaceAddress, BlockInterval); + return events.Select(e => + { + var result = e.Event; + result.Block = GetBlock(e.Log.BlockNumber.ToUlong()); + return result; + }).ToArray(); + } + + public SlotFilledEventDTO[] GetSlotFilledEvents() + { + var events = gethNode.GetEvents(deployment.MarketplaceAddress, BlockInterval); + return events.Select(e => + { + var result = e.Event; + result.Block = GetBlock(e.Log.BlockNumber.ToUlong()); + result.Host = GetEthAddressFromTransaction(e.Log.TransactionHash); + return result; + }).ToArray(); + } + + public SlotFreedEventDTO[] GetSlotFreedEvents() + { + var events = gethNode.GetEvents(deployment.MarketplaceAddress, BlockInterval); + return events.Select(e => + { + var result = e.Event; + result.Block = GetBlock(e.Log.BlockNumber.ToUlong()); + return result; + }).ToArray(); + } + + private BlockTimeEntry GetBlock(ulong number) + { + return gethNode.GetBlockForNumber(number); + } + + private EthAddress GetEthAddressFromTransaction(string transactionHash) + { + var transaction = gethNode.GetTransaction(transactionHash); + return new EthAddress(transaction.From); + } + } +} diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsStarter.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsStarter.cs index a6f6641e..d2b5c3f0 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsStarter.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsStarter.cs @@ -24,7 +24,7 @@ namespace CodexContractsPlugin var startupConfig = CreateStartupConfig(gethNode); startupConfig.NameOverride = "codex-contracts"; - var containers = workflow.Start(1, new CodexContractsContainerRecipe(), startupConfig); + var containers = workflow.Start(1, new CodexContractsContainerRecipe(), startupConfig).WaitForOnline(); if (containers.Containers.Length != 1) throw new InvalidOperationException("Expected 1 Codex contracts container to be created. Test infra failure."); var container = containers.Containers[0]; @@ -59,7 +59,7 @@ namespace CodexContractsPlugin var logHandler = new ContractsReadyLogHandler(tools.GetLog()); workflow.DownloadContainerLog(container, logHandler, 100); return logHandler.Found; - }); + }, nameof(DeployContract)); Log("Contracts deployed. Extracting addresses..."); var extractor = new ContractsContainerInfoExtractor(tools.GetLog(), workflow, container); @@ -71,7 +71,7 @@ namespace CodexContractsPlugin Log("Extract completed. Checking sync..."); - Time.WaitUntil(() => interaction.IsSynced(marketplaceAddress, abi)); + Time.WaitUntil(() => interaction.IsSynced(marketplaceAddress, abi), nameof(DeployContract)); Log("Synced. Codex SmartContracts deployed."); @@ -83,9 +83,9 @@ namespace CodexContractsPlugin tools.GetLog().Log(msg); } - private void WaitUntil(Func predicate) + private void WaitUntil(Func predicate, string msg) { - Time.WaitUntil(predicate, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(2)); + Time.WaitUntil(predicate, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(2), msg); } private StartupConfig CreateStartupConfig(IGethNode gethNode) diff --git a/ProjectPlugins/CodexContractsPlugin/ContractInteractions.cs b/ProjectPlugins/CodexContractsPlugin/ContractInteractions.cs index 93045db2..29840f97 100644 --- a/ProjectPlugins/CodexContractsPlugin/ContractInteractions.cs +++ b/ProjectPlugins/CodexContractsPlugin/ContractInteractions.cs @@ -44,7 +44,7 @@ namespace CodexContractsPlugin } } - public string MintTestTokens(EthAddress address, decimal amount, string tokenAddress) + public string MintTestTokens(EthAddress address, BigInteger amount, string tokenAddress) { log.Debug($"{amount} -> {address} (token: {tokenAddress})"); return MintTokens(address.Address, amount, tokenAddress); @@ -85,7 +85,7 @@ namespace CodexContractsPlugin } } - private string MintTokens(string account, decimal amount, string tokenAddress) + private string MintTokens(string account, BigInteger amount, string tokenAddress) { log.Debug($"({tokenAddress}) {amount} --> {account}"); if (string.IsNullOrEmpty(account)) throw new ArgumentException("Invalid arguments for MintTestTokens"); @@ -93,7 +93,7 @@ namespace CodexContractsPlugin var function = new MintTokensFunction { Holder = account, - Amount = amount.ToBig() + Amount = amount }; return gethNode.SendTransaction(tokenAddress, function); diff --git a/ProjectPlugins/CodexContractsPlugin/ContractsContainerInfoExtractor.cs b/ProjectPlugins/CodexContractsPlugin/ContractsContainerInfoExtractor.cs index 94cf4e6c..752192ca 100644 --- a/ProjectPlugins/CodexContractsPlugin/ContractsContainerInfoExtractor.cs +++ b/ProjectPlugins/CodexContractsPlugin/ContractsContainerInfoExtractor.cs @@ -1,4 +1,5 @@ -using KubernetesWorkflow; +using CodexContractsPlugin.Marketplace; +using KubernetesWorkflow; using KubernetesWorkflow.Types; using Logging; using Newtonsoft.Json; @@ -53,7 +54,18 @@ namespace CodexContractsPlugin var artifact = JObject.Parse(json); var abi = artifact["abi"]; - return abi!.ToString(Formatting.None); + var byteCode = artifact["bytecode"]; + var abiResult = abi!.ToString(Formatting.None); + var byteCodeResult = byteCode!.ToString(Formatting.None); + + if (byteCodeResult + .ToLowerInvariant() + .Replace("\"", "") != MarketplaceDeploymentBase.BYTECODE.ToLowerInvariant()) + { + throw new Exception("BYTECODE in CodexContractsPlugin does not match BYTECODE deployed by container. Update Marketplace.cs generated code?"); + } + + return abiResult; } private static string Retry(Func fetch) diff --git a/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs b/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs index 96490b75..43311430 100644 --- a/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs +++ b/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs @@ -1,35 +1,56 @@ #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. using GethPlugin; +using NethereumWorkflow.BlockUtils; +using Newtonsoft.Json; namespace CodexContractsPlugin.Marketplace { - public partial class Request : RequestBase + public interface IHasBlock { - public ulong BlockNumber { get; set; } + BlockTimeEntry Block { get; set; } + } + + public partial class Request : RequestBase, IHasBlock + { + [JsonIgnore] + public BlockTimeEntry Block { get; set; } public byte[] RequestId { get; set; } public EthAddress ClientAddress { get { return new EthAddress(Client); } } + + [JsonIgnore] + public string Id + { + get + { + return BitConverter.ToString(RequestId).Replace("-", "").ToLowerInvariant(); + } + } } - public partial class RequestFulfilledEventDTO + public partial class RequestFulfilledEventDTO : IHasBlock { - public ulong BlockNumber { get; set; } + [JsonIgnore] + public BlockTimeEntry Block { get; set; } } - public partial class RequestCancelledEventDTO + public partial class RequestCancelledEventDTO : IHasBlock { - public ulong BlockNumber { get; set; } + [JsonIgnore] + public BlockTimeEntry Block { get; set; } } - public partial class SlotFilledEventDTO + public partial class SlotFilledEventDTO : IHasBlock { - public ulong BlockNumber { get; set; } + [JsonIgnore] + public BlockTimeEntry Block { get; set; } public EthAddress Host { get; set; } } - public partial class SlotFreedEventDTO + public partial class SlotFreedEventDTO : IHasBlock { - public ulong BlockNumber { get; set; } + [JsonIgnore] + public BlockTimeEntry Block { get; set; } } } #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. diff --git a/ProjectPlugins/CodexContractsPlugin/Marketplace/Marketplace.cs b/ProjectPlugins/CodexContractsPlugin/Marketplace/Marketplace.cs index 507334ab..feb9518d 100644 --- a/ProjectPlugins/CodexContractsPlugin/Marketplace/Marketplace.cs +++ b/ProjectPlugins/CodexContractsPlugin/Marketplace/Marketplace.cs @@ -7,6 +7,25 @@ using System.Numerics; #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. namespace CodexContractsPlugin.Marketplace { + public partial class MarketplaceDeployment : MarketplaceDeploymentBase + { + public MarketplaceDeployment() : base(BYTECODE) { } + public MarketplaceDeployment(string byteCode) : base(byteCode) { } + } + + public class MarketplaceDeploymentBase : ContractDeploymentMessage + { + public static string BYTECODE = "0x60c06040523480156200001157600080fd5b5060405162003c9b38038062003c9b833981016040819052620000349162000487565b60208301518051608052816101004311620000965760405162461bcd60e51b815260206004820152601960248201527f496e73756666696369656e7420626c6f636b206865696768740000000000000060448201526064015b60405180910390fd5b81516000908155602083015160015560408301516002805460ff191660ff9092169190911790556060830151839190600390620000d4908262000627565b5050600480546001600160a01b0319166001600160a01b0393841617905550831660a05250825151606460ff9091161115620001535760405162461bcd60e51b815260206004820152601560248201527f4d757374206265206c657373207468616e20313030000000000000000000000060448201526064016200008d565b606483600001516060015160ff161115620001b15760405162461bcd60e51b815260206004820152601560248201527f4d757374206265206c657373207468616e20313030000000000000000000000060448201526064016200008d565b82516060810151602090910151606491620001cc91620006f3565b60ff1611156200021f5760405162461bcd60e51b815260206004820152601d60248201527f4d6178696d756d20736c617368696e672065786365656473203130302500000060448201526064016200008d565b82518051600c805460208085015160408087015160609788015160ff9081166401000000000260ff60201b1961ffff90931662010000029290921664ffffff0000199482166101000261ffff1990971698821698909817959095179290921695909517178355808801518051600d90815591810151600e5593840151600f80549190931660ff19919091161790915592820151869391929190601090620002c7908262000627565b50505090505050505062000725565b634e487b7160e01b600052604160045260246000fd5b604051608081016001600160401b0381118282101715620003115762000311620002d6565b60405290565b604080519081016001600160401b0381118282101715620003115762000311620002d6565b604051601f8201601f191681016001600160401b0381118282101715620003675762000367620002d6565b604052919050565b805160ff811681146200038157600080fd5b919050565b6000608082840312156200039957600080fd5b620003a3620002ec565b90508151815260208083015181830152620003c1604084016200036f565b604083015260608301516001600160401b0380821115620003e157600080fd5b818501915085601f830112620003f657600080fd5b8151818111156200040b576200040b620002d6565b6200041f601f8201601f191685016200033c565b915080825286848285010111156200043657600080fd5b60005b818110156200045657838101850151838201860152840162000439565b5060008482840101525080606085015250505092915050565b80516001600160a01b03811681146200038157600080fd5b6000806000606084860312156200049d57600080fd5b83516001600160401b0380821115620004b557600080fd5b9085019081870360a0811215620004cb57600080fd5b620004d562000317565b6080821215620004e457600080fd5b620004ee620002ec565b9150620004fb846200036f565b82526200050b602085016200036f565b6020830152604084015161ffff811681146200052657600080fd5b604083015262000539606085016200036f565b6060830152908152608083015190828211156200055557600080fd5b620005638983860162000386565b6020820152809650505050506200057d602085016200046f565b91506200058d604085016200046f565b90509250925092565b600181811c90821680620005ab57607f821691505b602082108103620005cc57634e487b7160e01b600052602260045260246000fd5b50919050565b601f82111562000622576000816000526020600020601f850160051c81016020861015620005fd5750805b601f850160051c820191505b818110156200061e5782815560010162000609565b5050505b505050565b81516001600160401b03811115620006435762000643620002d6565b6200065b8162000654845462000596565b84620005d2565b602080601f8311600181146200069357600084156200067a5750858301515b600019600386901b1c1916600185901b1785556200061e565b600085815260208120601f198616915b82811015620006c457888601518255948401946001909101908401620006a3565b5085821015620006e35787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b60ff81811683821602908116908181146200071e57634e487b7160e01b600052601160045260246000fd5b5092915050565b60805160a05161352d6200076e600039600081816103ca01528181610bc001528181611d59015281816120de015261224a0152600081816126670152612806015261352d6000f3fe608060405234801561001057600080fd5b50600436106101825760003560e01c80639777b72c116100d8578063be5cdc481161008c578063f752196b11610066578063f752196b14610388578063fb1e61ca146103a8578063fc0c546a146103c857600080fd5b8063be5cdc4814610342578063c0cc4add14610362578063e8aa0a071461037557600080fd5b8063a3a0807e116100bd578063a3a0807e146102ec578063a6af384b1461030f578063b396dc791461032257600080fd5b80639777b72c146102d1578063a29c29a4146102d957600080fd5b80634641dce61161013a5780636b00c8cf116101145780636b00c8cf146102655780636e2b54ee146102a957806379502c55146102bc57600080fd5b80634641dce61461021857806359cc89ed1461023d5780635da738351461025057600080fd5b806308695fcd1161016b57806308695fcd146101cd578063329b5a0b146101e2578063458d2bf11461020557600080fd5b806302fa8e651461018757806305b90773146101ad575b600080fd5b61019a610195366004612a3c565b6103ee565b6040519081526020015b60405180910390f35b6101c06101bb366004612a3c565b610465565b6040516101a49190612a6b565b6101e06101db366004612a85565b610557565b005b61019a6101f0366004612a3c565b60009081526012602052604090206005015490565b61019a610213366004612a3c565b6106ad565b61022b610226366004612a3c565b6106c6565b60405160ff90911681526020016101a4565b6101e061024b366004612aba565b6106d9565b6102586109dd565b6040516101a49190612af1565b610291610273366004612a3c565b6000908152601360205260409020600501546001600160a01b031690565b6040516001600160a01b0390911681526020016101a4565b6101e06102b7366004612a3c565b610a04565b6102c4610c41565b6040516101a49190612b7b565b610258610db2565b6101e06102e7366004612a3c565b610dd1565b6102ff6102fa366004612a3c565b610fa2565b60405190151581526020016101a4565b6101e061031d366004612bfd565b610fd7565b610335610330366004612a3c565b611240565b6040516101a49190612d06565b610355610350366004612a3c565b611429565b6040516101a49190612d38565b6102ff610370366004612a3c565b6114f7565b6101e0610383366004612d4c565b61150a565b61019a610396366004612a3c565b60009081526007602052604090205490565b6103bb6103b6366004612a3c565b611647565b6040516101a49190612d7a565b7f0000000000000000000000000000000000000000000000000000000000000000610291565b6000818152601260205260408120600401548161040a84610465565b9050600081600481111561042057610420612a55565b148061043d5750600181600481111561043b5761043b612a55565b145b15610449575092915050565b61045d82610458600142612da3565b6117fe565b949350505050565b60008181526011602052604081205482906001600160a01b03166104c25760405162461bcd60e51b815260206004820152600f60248201526e155b9adb9bdddb881c995c5d595cdd608a1b60448201526064015b60405180910390fd5b600083815260126020526040812090815460ff1660048111156104e7576104e7612a55565b148015610504575060008481526012602052604090206005015442115b15610513576002925050610551565b6001815460ff16600481111561052b5761052b612a55565b14801561053b5750806004015442115b1561054a576003925050610551565b5460ff1691505b50919050565b600161056283611429565b600581111561057357610573612a55565b146105c05760405162461bcd60e51b815260206004820152601960248201527f536c6f74206e6f7420616363657074696e672070726f6f66730000000000000060448201526064016104b9565b6105ca8282611816565b6000828152601360209081526040808320600181015484526011909252909120600c5461ffff620100009091041661060e8560009081526007602052604090205490565b6106189190612dcc565b6000036106a757600c54600682015460009160649161064291640100000000900460ff1690612de0565b61064c9190612df7565b9050808360040160008282546106629190612da3565b9091555050600c54600086815260076020526040902054610100820460ff169162010000900461ffff16906106979190612df7565b106106a5576106a585611a43565b505b50505050565b60006106c0826106bb611be9565b611bf4565b92915050565b60006106c0826106d4611be9565b611c08565b60008381526011602052604090205483906001600160a01b03166107315760405162461bcd60e51b815260206004820152600f60248201526e155b9adb9bdddb881c995c5d595cdd608a1b60448201526064016104b9565b6000848152601160205260409020600181015467ffffffffffffffff16841061079c5760405162461bcd60e51b815260206004820152600c60248201527f496e76616c696420736c6f74000000000000000000000000000000000000000060448201526064016104b9565b60408051602080820188905281830187905282518083038401815260609092019092528051910120600090600081815260136020526040812060018101899055600381018890559192506107ef83611429565b600581111561080057610800612a55565b1461084d5760405162461bcd60e51b815260206004820152601060248201527f536c6f74206973206e6f7420667265650000000000000000000000000000000060448201526064016104b9565b600483015460008381526005602090815260408083204290556006909152902055610878828661150a565b60058101805473ffffffffffffffffffffffffffffffffffffffff191633179055805460ff1916600190811782554260028301556000888152601260205260408120808301805491939290916108cf908490612e0b565b909155506108df90508842611c74565b8160020160008282546108f29190612da3565b909155505060068401546109063382611d11565b806014600001600082825461091b9190612e0b565b909155505060048301819055600583015461093f906001600160a01b031685611e19565b887ff530852268993f91008f1a1e0b09b5c813acd4188481f1fa83c33c7182e814b48960405161097191815260200190565b60405180910390a26001808601549083015467ffffffffffffffff90911690036109d257815460ff1916600117825542600383015560405189907f85e1543bf2f84fe80c6badbce3648c8539ad1df4d2b3d822938ca0538be727e690600090a25b505050505050505050565b336000908152600b602052604090206060906109ff906109fc90611e3b565b90565b905090565b60008181526011602090815260408083206012909252909120600501544211610a6f5760405162461bcd60e51b815260206004820152601960248201527f52657175657374206e6f74207965742074696d6564206f75740000000000000060448201526064016104b9565b80546001600160a01b03163314610ac85760405162461bcd60e51b815260206004820152601660248201527f496e76616c696420636c69656e7420616464726573730000000000000000000060448201526064016104b9565b600082815260126020526040812090815460ff166004811115610aed57610aed612a55565b14610b3a5760405162461bcd60e51b815260206004820152600d60248201527f496e76616c69642073746174650000000000000000000000000000000000000060448201526064016104b9565b805460ff191660021781558154610b5a906001600160a01b031684611e48565b60405183907ff903f4774c7bd27355f9d7fcbc382b079b164a697a44ac5d95267a4c3cb3bb2290600090a2600281015460158054829190600090610b9f908490612e0b565b909155505060405163a9059cbb60e01b8152336004820152602481018290527f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03169063a9059cbb906044016020604051808303816000875af1158015610c11573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190610c359190612e1e565b6106a7576106a7612e40565b610c986040805160c0810182526000918101828152606082018390526080820183905260a0820192909252908190815260408051608081018252600080825260208281018290529282015260608082015291015290565b6040805160c081018252600c805460ff8082168486019081526101008304821660608087019190915261ffff62010000850416608080880191909152640100000000909404831660a087015290855285519283018652600d80548452600e54602085810191909152600f54909316968401969096526010805495969495928701949291840191610d2790612e56565b80601f0160208091040260200160405190810160405280929190818152602001828054610d5390612e56565b8015610da05780601f10610d7557610100808354040283529160200191610da0565b820191906000526020600020905b815481529060010190602001808311610d8357829003601f168201915b50505050508152505081525050905090565b336000908152600a602052604090206060906109ff906109fc90611e3b565b806000808281526013602052604090205460ff166005811115610df657610df6612a55565b03610e325760405162461bcd60e51b815260206004820152600c60248201526b536c6f74206973206672656560a01b60448201526064016104b9565b600082815260136020526040902060058101546001600160a01b03163314610e9c5760405162461bcd60e51b815260206004820152601960248201527f536c6f742066696c6c6564206279206f7468657220686f73740000000000000060448201526064016104b9565b6000610ea784611429565b90506004816005811115610ebd57610ebd612a55565b03610f0a5760405162461bcd60e51b815260206004820152600c60248201527f416c72656164792070616964000000000000000000000000000000000000000060448201526064016104b9565b6002816005811115610f1e57610f1e612a55565b03610f3657610f31826001015485611e6a565b6106a7565b6005816005811115610f4a57610f4a612a55565b03610f5d57610f31826001015485612157565b6003816005811115610f7157610f71612a55565b03610f8057610f3133856122c3565b6001816005811115610f9457610f94612a55565b036106a7576106a784611a43565b6000806000610fb884610fb3611be9565b6122e5565b909250905081801561045d575060025460ff9081169116109392505050565b33610fe56020830183612ea2565b6001600160a01b03161461103b5760405162461bcd60e51b815260206004820152601660248201527f496e76616c696420636c69656e7420616464726573730000000000000000000060448201526064016104b9565b600061104e61104983613034565b6123c8565b6000818152601160205260409020549091506001600160a01b0316156110b65760405162461bcd60e51b815260206004820152601660248201527f5265717565737420616c7265616479206578697374730000000000000000000060448201526064016104b9565b60008261012001351180156110d357506060820135610120830135105b61111f5760405162461bcd60e51b815260206004820152601360248201527f457870697279206e6f7420696e2072616e67650000000000000000000000000060448201526064016104b9565b600081815260116020526040902082906111398282613291565b5061114a9050606083013542612e0b565b60008281526012602052604090206004015561116b61012083013542612e0b565b6000828152601260209081526040909120600501919091556111999061119390840184612ea2565b826123f8565b60006111ac6111a784613034565b61241a565b60008381526012602052604081206002018290556014805492935083929091906111d7908490612e0b565b909155506111e790503382611d11565b7f5fdb86c365a247a4d97dcbcc5c3abde9d6e3e2de26273f3fda8eef5073b9a96c8284602001601260008681526020019081526020016000206005015460405161123393929190613389565b60405180910390a1505050565b611248612974565b816000808281526013602052604090205460ff16600581111561126d5761126d612a55565b036112a95760405162461bcd60e51b815260206004820152600c60248201526b536c6f74206973206672656560a01b60448201526064016104b9565b60008381526013602052604090206112bf612974565b600180830154600090815260116020908152604091829020825160a0808201855282546001600160a01b03168252845160e0810186529583015467ffffffffffffffff9081168752600284015487860152600384015487870152600484015460608801526005840154608088015260068401549187019190915260078301541660c086015291820193909352815180830183526008840180549294938501928290829061136b90612e56565b80601f016020809104026020016040519081016040528092919081815260200182805461139790612e56565b80156113e45780601f106113b9576101008083540402835291602001916113e4565b820191906000526020600020905b8154815290600101906020018083116113c757829003601f168201915b505050918352505060019190910154602091820152908252600a83015482820152600b9092015460409091015290825260039092015491810191909152915050919050565b60008181526013602052604081206001810154820361144b5750600092915050565b600061145a8260010154610465565b90506004825460ff16600581111561147457611474612a55565b03611483575060049392505050565b600281600481111561149757611497612a55565b036114a6575060059392505050565b60038160048111156114ba576114ba612a55565b036114c9575060029392505050565b60048160048111156114dd576114dd612a55565b036114ec575060039392505050565b505460ff1692915050565b60006106c082611505611be9565b61243f565b6000828152601360209081526040808320600101548084526011909252909120546001600160a01b03166115725760405162461bcd60e51b815260206004820152600f60248201526e155b9adb9bdddb881c995c5d595cdd608a1b60448201526064016104b9565b6000838152601360209081526040808320600181015484526011835281842082516003808252608082019094529194909390929082016060803683370190505090506115c56115c0876106ad565b612472565b816000815181106115d8576115d8613405565b602090810291909101015260098201546115f190612483565b8160018151811061160457611604613405565b60200260200101818152505082600301548160028151811061162857611628613405565b60200260200101818152505061163f86868361248f565b505050505050565b61164f612994565b60008281526011602052604090205482906001600160a01b03166116a75760405162461bcd60e51b815260206004820152600f60248201526e155b9adb9bdddb881c995c5d595cdd608a1b60448201526064016104b9565b600083815260116020908152604091829020825160a0808201855282546001600160a01b03168252845160e081018652600184015467ffffffffffffffff90811682526002850154828701526003850154828801526004850154606083015260058501546080830152600685015492820192909252600784015490911660c0820152928101929092528251808401845260088201805493949293928501928290829061175290612e56565b80601f016020809104026020016040519081016040528092919081815260200182805461177e90612e56565b80156117cb5780601f106117a0576101008083540402835291602001916117cb565b820191906000526020600020905b8154815290600101906020018083116117ae57829003601f168201915b505050505081526020016001820154815250508152602001600a8201548152602001600b82015481525050915050919050565b600081831061180d578161180f565b825b9392505050565b60006118218261264d565b90504281106118725760405162461bcd60e51b815260206004820152601860248201527f506572696f6420686173206e6f7420656e64656420796574000000000000000060448201526064016104b9565b60015461187f9082612e0b565b42106118cd5760405162461bcd60e51b815260206004820152601460248201527f56616c69646174696f6e2074696d6564206f757400000000000000000000000060448201526064016104b9565b600083815260086020908152604080832085845290915290205460ff16156119375760405162461bcd60e51b815260206004820181905260248201527f50726f6f6620776173207375626d69747465642c206e6f74206d697373696e6760448201526064016104b9565b611941838361243f565b61198d5760405162461bcd60e51b815260206004820152601660248201527f50726f6f6620776173206e6f742072657175697265640000000000000000000060448201526064016104b9565b600083815260096020908152604080832085845290915290205460ff16156119f75760405162461bcd60e51b815260206004820152601f60248201527f50726f6f6620616c7265616479206d61726b6564206173206d697373696e670060448201526064016104b9565b60008381526009602090815260408083208584528252808320805460ff1916600190811790915586845260079092528220805491929091611a39908490612e0b565b9091555050505050565b60008181526013602090815260408083206001810154808552601290935292206005830154611a7b906001600160a01b0316856122c3565b6003808401546000868152601360205260408120805460ff1916815560018082018390556002820183905593810182905560048101829055600501805473ffffffffffffffffffffffffffffffffffffffff191690558383018054929392909190611ae7908490612da3565b909155505060405181815283907f1d31c9f8dea6e179f6a050db117595feea8937029ea51f5168a4780be7e8f5529060200160405180910390a2600085815260076020526040812055600083815260116020526040812060018085015490820154919291611b5f919067ffffffffffffffff16612da3565b600783015490915067ffffffffffffffff1681118015611b9457506001845460ff166004811115611b9257611b92612a55565b145b15611be057835460ff19166004178455611baf600142612da3565b600485015560405185907f4769361a442504ecaf038f35e119bcccdd5e42096b24c09e3c17fd17c6684c0290600090a25b50505050505050565b60006109ff42612660565b600061180f611c038484611c08565b61268c565b600080611c1761010043612dcc565b90506000610100611c29856043612de0565b611c339190612dcc565b90506000611c4361010087612dcc565b9050600061010082611c558587612e0b565b611c5f9190612e0b565b611c699190612dcc565b979650505050505050565b600082815260116020908152604080832060129092528220600501548310611cde5760405162461bcd60e51b815260206004820152601760248201527f5374617274206e6f74206265666f72652065787069727900000000000000000060448201526064016104b9565b600581015483611cfd8660009081526012602052604090206005015490565b611d079190612da3565b61045d9190612de0565b6040517f23b872dd0000000000000000000000000000000000000000000000000000000081526001600160a01b038381166004830152306024830181905260448301849052917f0000000000000000000000000000000000000000000000000000000000000000909116906323b872dd906064016020604051808303816000875af1158015611da4573d6000803e3d6000fd5b505050506040513d601f19601f82011682018060405250810190611dc89190612e1e565b611e145760405162461bcd60e51b815260206004820152600f60248201527f5472616e73666572206661696c6564000000000000000000000000000000000060448201526064016104b9565b505050565b6001600160a01b0382166000908152600b60205260409020611e1490826126e6565b6060600061180f836126f2565b6001600160a01b0382166000908152600a60205260409020611e14908261274e565b60008281526011602052604090205482906001600160a01b0316611ec25760405162461bcd60e51b815260206004820152600f60248201526e155b9adb9bdddb881c995c5d595cdd608a1b60448201526064016104b9565b60008381526012602090815260408083206011909252909120815460ff191660031782558054611efb906001600160a01b031686611e48565b60008481526013602052604090206005810154611f21906001600160a01b0316866122c3565b6004808201546000888152601160209081526040808320815160a0808201845282546001600160a01b03168252835160e081018552600184015467ffffffffffffffff908116825260028501548288015260038501548287015298840154606082015260058401546080820152600684015491810191909152600783015490971660c08801529283019590955280518082018252600886018054949661207e959093850192919082908290611fd590612e56565b80601f016020809104026020016040519081016040528092919081815260200182805461200190612e56565b801561204e5780601f106120235761010080835404028352916020019161204e565b820191906000526020600020905b81548152906001019060200180831161203157829003601f168201915b505050505081526020016001820154815250508152602001600a8201548152602001600b8201548152505061275a565b6120889190612e0b565b9050806014600101600082825461209f9190612e0b565b9091555050815460ff191660049081178355600583015460405163a9059cbb60e01b81526001600160a01b0391821692810192909252602482018390527f0000000000000000000000000000000000000000000000000000000000000000169063a9059cbb906044016020604051808303816000875af1158015612127573d6000803e3d6000fd5b505050506040513d601f19601f8201168201806040525081019061214b9190612e1e565b611be057611be0612e40565b60008281526011602052604090205482906001600160a01b03166121af5760405162461bcd60e51b815260206004820152600f60248201526e155b9adb9bdddb881c995c5d595cdd608a1b60448201526064016104b9565b600082815260136020526040902060058101546121d5906001600160a01b0316846122c3565b600081600401546121ea868460020154611c74565b6121f49190612e0b565b9050806014600101600082825461220b9190612e0b565b9091555050815460ff191660049081178355600583015460405163a9059cbb60e01b81526001600160a01b0391821692810192909252602482018390527f0000000000000000000000000000000000000000000000000000000000000000169063a9059cbb906044016020604051808303816000875af1158015612293573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906122b79190612e1e565b6106a5576106a5612e40565b6001600160a01b0382166000908152600b60205260409020611e14908261274e565b60008060006122f385611429565b6000868152600560205260408120549192509061230f90612660565b9050600182600581111561232557612325612a55565b14158061233957506123378582612776565b155b1561234c576000809350935050506123c1565b6123568686611c08565b925060006123638461268c565b6002549091506000906101009061237d9060ff168261341b565b60008a81526006602052604090205461239a9161ffff1690612de0565b6123a49190612df7565b90508015806123ba57506123b88183612dcc565b155b9550505050505b9250929050565b6000816040516020016123db9190612d7a565b604051602081830303815290604052805190602001209050919050565b6001600160a01b0382166000908152600a60205260409020611e1490826126e6565b60006124258261275a565b6020830151516106c0919067ffffffffffffffff16612de0565b600080600061244e85856122e5565b9092509050818015612469575060025460ff90811690821610155b95945050505050565b600060ff1982168161045d82612780565b60008061180f83612780565b6000838152600860205260408120906124a6611be9565b815260208101919091526040016000205460ff16156125075760405162461bcd60e51b815260206004820152601760248201527f50726f6f6620616c7265616479207375626d697474656400000000000000000060448201526064016104b9565b600480546040517f94c8919d0000000000000000000000000000000000000000000000000000000081526001600160a01b03909116916394c8919d91612551918691869101613436565b602060405180830381865afa15801561256e573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906125929190612e1e565b6125de5760405162461bcd60e51b815260206004820152600d60248201527f496e76616c69642070726f6f660000000000000000000000000000000000000060448201526064016104b9565b60008381526008602052604081206001916125f7611be9565b815260200190815260200160002060006101000a81548160ff0219169083151502179055507f3b989d183b84b02259d7c14b34a9c9eb0fccb4c355a920d25e581e25aef4993d8360405161123391815260200190565b60006106c061265b836127f2565b6127ff565b60006106c07f000000000000000000000000000000000000000000000000000000000000000083612df7565b60008060ff831661269e600143612da3565b6126a89190612da3565b40905060008190036126bc576126bc612e40565b60408051602081018390520160405160208183030381529060405280519060200120915050919050565b600061180f838361282b565b60608160000180548060200260200160405190810160405280929190818152602001828054801561274257602002820191906000526020600020905b81548152602001906001019080831161272e575b50505050509050919050565b600061180f838361287a565b602081015160808101516040909101516000916106c091612de0565b600081831161180f565b7fff00000000000000000000000000000000000000000000000000000000000000811660015b602081101561055157600891821c916127c0908290612de0565b83901b7fff000000000000000000000000000000000000000000000000000000000000001691909117906001016127a6565b60006106c0826001612e0b565b60006106c07f000000000000000000000000000000000000000000000000000000000000000083612de0565b6000818152600183016020526040812054612872575081546001818101845560008481526020808220909301849055845484825282860190935260409020919091556106c0565b5060006106c0565b6000818152600183016020526040812054801561296357600061289e600183612da3565b85549091506000906128b290600190612da3565b90508181146129175760008660000182815481106128d2576128d2613405565b90600052602060002001549050808760000184815481106128f5576128f5613405565b6000918252602080832090910192909255918252600188019052604090208390555b8554869080612928576129286134e1565b6001900381819060005260206000200160009055905585600101600086815260200190815260200160002060009055600193505050506106c0565b60009150506106c0565b5092915050565b6040518060400160405280612987612994565b8152602001600081525090565b6040518060a0016040528060006001600160a01b03168152602001612a036040518060e00160405280600067ffffffffffffffff1681526020016000815260200160008152602001600081526020016000815260200160008152602001600067ffffffffffffffff1681525090565b8152602001612a28604051806040016040528060608152602001600080191681525090565b815260006020820181905260409091015290565b600060208284031215612a4e57600080fd5b5035919050565b634e487b7160e01b600052602160045260246000fd5b6020810160058310612a7f57612a7f612a55565b91905290565b60008060408385031215612a9857600080fd5b50508035926020909101359150565b6000610100828403121561055157600080fd5b60008060006101408486031215612ad057600080fd5b8335925060208401359150612ae88560408601612aa7565b90509250925092565b6020808252825182820181905260009190848201906040850190845b81811015612b2957835183529284019291840191600101612b0d565b50909695505050505050565b6000815180845260005b81811015612b5b57602081850181015186830182015201612b3f565b506000602082860101526020601f19601f83011685010191505092915050565b602081526000825160ff815116602084015260ff602082015116604084015261ffff604082015116606084015260ff606082015116608084015250602083015160a080840152805160c0840152602081015160e084015260ff60408201511661010084015260608101519050608061012084015261045d610140840182612b35565b600060208284031215612c0f57600080fd5b813567ffffffffffffffff811115612c2657600080fd5b8201610160818503121561180f57600080fd5b6000815160408452612c4e6040850182612b35565b602093840151949093019390935250919050565b60006101606001600160a01b038351168452602083015167ffffffffffffffff808251166020870152602082015160408701526040820151606087015260608201516080870152608082015160a087015260a082015160c08701528060c08301511660e08701525050604083015181610100860152612ce382860182612c39565b915050606083015161012085015260808301516101408501528091505092915050565b602081526000825160406020840152612d226060840182612c62565b9050602084015160408401528091505092915050565b6020810160068310612a7f57612a7f612a55565b6000806101208385031215612d6057600080fd5b82359150612d718460208501612aa7565b90509250929050565b60208152600061180f6020830184612c62565b634e487b7160e01b600052601160045260246000fd5b818103818111156106c0576106c0612d8d565b634e487b7160e01b600052601260045260246000fd5b600082612ddb57612ddb612db6565b500690565b80820281158282048414176106c0576106c0612d8d565b600082612e0657612e06612db6565b500490565b808201808211156106c0576106c0612d8d565b600060208284031215612e3057600080fd5b8151801515811461180f57600080fd5b634e487b7160e01b600052600160045260246000fd5b600181811c90821680612e6a57607f821691505b60208210810361055157634e487b7160e01b600052602260045260246000fd5b6001600160a01b0381168114612e9f57600080fd5b50565b600060208284031215612eb457600080fd5b813561180f81612e8a565b634e487b7160e01b600052604160045260246000fd5b6040805190810167ffffffffffffffff81118282101715612ef857612ef8612ebf565b60405290565b60405160a0810167ffffffffffffffff81118282101715612ef857612ef8612ebf565b60405160e0810167ffffffffffffffff81118282101715612ef857612ef8612ebf565b604051601f8201601f1916810167ffffffffffffffff81118282101715612f6d57612f6d612ebf565b604052919050565b67ffffffffffffffff81168114612e9f57600080fd5b600060408284031215612f9d57600080fd5b612fa5612ed5565b9050813567ffffffffffffffff80821115612fbf57600080fd5b818401915084601f830112612fd357600080fd5b8135602082821115612fe757612fe7612ebf565b612ff9601f8301601f19168201612f44565b9250818352868183860101111561300f57600080fd5b8181850182850137600081838501015282855280860135818601525050505092915050565b600081360361016081121561304857600080fd5b613050612efe565b833561305b81612e8a565b815260e0601f198301121561306f57600080fd5b613077612f21565b9150602084013561308781612f75565b8083525060408401356020830152606084013560408301526080840135606083015260a0840135608083015260c084013560a083015260e08401356130cb81612f75565b60c083015260208101919091526101008301359067ffffffffffffffff8211156130f457600080fd5b61310036838601612f8b565b604082015261012084013560608201526101409093013560808401525090919050565b600081356106c081612f75565b60008235603e1983360301811261314657600080fd5b9190910192915050565b601f821115611e14576000816000526020600020601f850160051c810160208610156131795750805b601f850160051c820191505b8181101561163f57828155600101613185565b8135601e198336030181126131ac57600080fd5b8201803567ffffffffffffffff8111156131c557600080fd5b602081360381840113156131d857600080fd5b6131ec826131e68654612e56565b86613150565b6000601f831160018114613222576000841561320a57508482018301355b600019600386901b1c1916600185901b17865561327f565b600086815260209020601f19851690835b82811015613254578785018601358255938501936001909101908501613233565b50858210156132735760001960f88760031b161c198585890101351681555b505060018460011b0186555b50508085013560018501555050505050565b813561329c81612e8a565b6001600160a01b03811673ffffffffffffffffffffffffffffffffffffffff198354161782555060208201356132d181612f75565b60018201805467ffffffffffffffff191667ffffffffffffffff83161790555060408201356002820155606082013560038201556080820135600482015560a0820135600582015560c0820135600682015561335561333260e08401613123565b6007830167ffffffffffffffff821667ffffffffffffffff198254161781555050565b61336f613366610100840184613130565b60088301613198565b610120820135600a820155610140820135600b8201555050565b8381526101208101833561339c81612f75565b67ffffffffffffffff8082166020850152602086013560408501526040860135606085015260608601356080850152608086013560a085015260a086013560c085015260c086013591506133ef82612f75565b1660e08301526101009091019190915292915050565b634e487b7160e01b600052603260045260246000fd5b61ffff82811682821603908082111561296d5761296d612d8d565b82358152602080840135908201526000610120828101613466604085016040880180358252602090810135910152565b613480608085016080880180358252602090810135910152565b61349a60c0850160c0880180358252602090810135910152565b61010084019190915283519081905261014083019060209081860160005b828110156134d4578151855293830193908301906001016134b8565b5092979650505050505050565b634e487b7160e01b600052603160045260246000fdfea264697066735822122018af34de337780d47ca891540a9390f1b4ef028567fed32e08410582d7c9120564736f6c63430008170033"; + public MarketplaceDeploymentBase() : base(BYTECODE) { } + public MarketplaceDeploymentBase(string byteCode) : base(byteCode) { } + [Parameter("tuple", "configuration", 1)] + public virtual MarketplaceConfig Configuration { get; set; } + [Parameter("address", "token_", 2)] + public virtual string Token { get; set; } + [Parameter("address", "verifier", 3)] + public virtual string Verifier { get; set; } + } + public partial class ConfigFunction : ConfigFunctionBase { } [Function("config", typeof(ConfigOutputDTO))] @@ -24,8 +43,8 @@ namespace CodexContractsPlugin.Marketplace public virtual byte[] RequestId { get; set; } [Parameter("uint256", "slotIndex", 2)] public virtual BigInteger SlotIndex { get; set; } - [Parameter("bytes", "proof", 3)] - public virtual byte[] Proof { get; set; } + [Parameter("tuple", "proof", 3)] + public virtual Groth16Proof Proof { get; set; } } public partial class FreeSlotFunction : FreeSlotFunctionBase { } @@ -136,6 +155,15 @@ namespace CodexContractsPlugin.Marketplace public virtual byte[] RequestId { get; set; } } + public partial class RequestExpiryFunction : RequestExpiryFunctionBase { } + + [Function("requestExpiry", "uint256")] + public class RequestExpiryFunctionBase : FunctionMessage + { + [Parameter("bytes32", "requestId", 1)] + public virtual byte[] RequestId { get; set; } + } + public partial class RequestStateFunction : RequestStateFunctionBase { } [Function("requestState", "uint8")] @@ -170,8 +198,8 @@ namespace CodexContractsPlugin.Marketplace { [Parameter("bytes32", "id", 1)] public virtual byte[] Id { get; set; } - [Parameter("bytes", "proof", 2)] - public virtual byte[] Proof { get; set; } + [Parameter("tuple", "proof", 2)] + public virtual Groth16Proof Proof { get; set; } } public partial class TokenFunction : TokenFunctionBase { } @@ -207,8 +235,6 @@ namespace CodexContractsPlugin.Marketplace { [Parameter("bytes32", "id", 1, false)] public virtual byte[] Id { get; set; } - [Parameter("bytes", "proof", 2, false)] - public virtual byte[] Proof { get; set; } } public partial class RequestCancelledEventDTO : RequestCancelledEventDTOBase { } @@ -278,10 +304,8 @@ namespace CodexContractsPlugin.Marketplace [FunctionOutput] public class ConfigOutputDTOBase : IFunctionOutputDTO { - [Parameter("tuple", "collateral", 1)] - public virtual CollateralConfig Collateral { get; set; } - [Parameter("tuple", "proofs", 2)] - public virtual ProofConfig Proofs { get; set; } + [Parameter("tuple", "", 1)] + public virtual MarketplaceConfig ReturnValue1 { get; set; } } @@ -380,6 +404,15 @@ namespace CodexContractsPlugin.Marketplace public virtual BigInteger ReturnValue1 { get; set; } } + public partial class RequestExpiryOutputDTO : RequestExpiryOutputDTOBase { } + + [FunctionOutput] + public class RequestExpiryOutputDTOBase : IFunctionOutputDTO + { + [Parameter("uint256", "", 1)] + public virtual BigInteger ReturnValue1 { get; set; } + } + public partial class RequestStateOutputDTO : RequestStateOutputDTOBase { } [FunctionOutput] @@ -446,6 +479,8 @@ namespace CodexContractsPlugin.Marketplace public virtual BigInteger Timeout { get; set; } [Parameter("uint8", "downtime", 3)] public virtual byte Downtime { get; set; } + [Parameter("string", "zkeyHash", 4)] + public virtual string ZkeyHash { get; set; } } public partial class MarketplaceConfig : MarketplaceConfigBase { } @@ -478,6 +513,48 @@ namespace CodexContractsPlugin.Marketplace public virtual ulong MaxSlotLoss { get; set; } } + public partial class G1Point : G1PointBase { } + + public class G1PointBase + { + [Parameter("uint256", "x", 1)] + public virtual BigInteger X { get; set; } + [Parameter("uint256", "y", 2)] + public virtual BigInteger Y { get; set; } + } + + public partial class Fp2Element : Fp2ElementBase { } + + public class Fp2ElementBase + { + [Parameter("uint256", "real", 1)] + public virtual BigInteger Real { get; set; } + [Parameter("uint256", "imag", 2)] + public virtual BigInteger Imag { get; set; } + } + + public partial class G2Point : G2PointBase { } + + public class G2PointBase + { + [Parameter("tuple", "x", 1)] + public virtual Fp2Element X { get; set; } + [Parameter("tuple", "y", 2)] + public virtual Fp2Element Y { get; set; } + } + + public partial class Groth16Proof : Groth16ProofBase { } + + public class Groth16ProofBase + { + [Parameter("tuple", "a", 1)] + public virtual G1Point A { get; set; } + [Parameter("tuple", "b", 2)] + public virtual G2Point B { get; set; } + [Parameter("tuple", "c", 3)] + public virtual G1Point C { get; set; } + } + public partial class Content : ContentBase { } public class ContentBase @@ -514,4 +591,5 @@ namespace CodexContractsPlugin.Marketplace public virtual BigInteger SlotIndex { get; set; } } } + #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. diff --git a/ProjectPlugins/CodexContractsPlugin/Marketplace/README.md b/ProjectPlugins/CodexContractsPlugin/Marketplace/README.md index bfb23ce1..2b8ddff5 100644 --- a/ProjectPlugins/CodexContractsPlugin/Marketplace/README.md +++ b/ProjectPlugins/CodexContractsPlugin/Marketplace/README.md @@ -1 +1,14 @@ This code was generated using the Nethereum code generator, here: http://playground.nethereum.com + +1. Go to site -> Abi Code Gen. +1. Contract name = "Marketplace". +1. In container, get "/hardhat/artifacts/contracts/Marketplace.sol/Marketplace.json". +1. Save only ABI section as new JSON. (top-level is a json array.) +1. From original JSON get byte code. +1. Put ABI JSON and byte code into site. +1. Generate. +1. From site generated code, copy `public partial class MarketplaceDeployment` and everything after it. (be considerate of namespace brackets!) +1. In Marketplace/Marketplace.cs, replace content of 'namespace CodexContractsPlugin.Marketplace'. + + + diff --git a/ProjectPlugins/CodexContractsPlugin/TestTokenExtensions.cs b/ProjectPlugins/CodexContractsPlugin/TestTokenExtensions.cs index a19abde4..e94032a7 100644 --- a/ProjectPlugins/CodexContractsPlugin/TestTokenExtensions.cs +++ b/ProjectPlugins/CodexContractsPlugin/TestTokenExtensions.cs @@ -1,45 +1,102 @@ -namespace CodexContractsPlugin +using System.Numerics; + +namespace CodexContractsPlugin { public class TestToken : IComparable { - public TestToken(decimal amount) + public static BigInteger WeiFactor = new BigInteger(1000000000000000000); + + public TestToken(BigInteger tstWei) { - Amount = amount; + TstWei = tstWei; + Tst = tstWei / WeiFactor; } - public decimal Amount { get; } + public BigInteger TstWei { get; } + public BigInteger Tst { get; } public int CompareTo(TestToken? other) { - return Amount.CompareTo(other!.Amount); + return TstWei.CompareTo(other!.TstWei); } public override bool Equals(object? obj) { - return obj is TestToken token && Amount == token.Amount; + return obj is TestToken token && TstWei == token.TstWei; } public override int GetHashCode() { - return HashCode.Combine(Amount); + return HashCode.Combine(TstWei); } public override string ToString() { - return $"{Amount} TestTokens"; + var weiOnly = TstWei % WeiFactor; + + var tokens = new List(); + if (Tst > 0) tokens.Add($"{Tst} TST"); + if (weiOnly > 0) tokens.Add($"{weiOnly} TSTWEI"); + + return string.Join(" + ", tokens); + } + + public static TestToken operator +(TestToken a, TestToken b) + { + return new TestToken(a.TstWei + b.TstWei); + } + + public static bool operator <(TestToken a, TestToken b) + { + return a.TstWei < b.TstWei; + } + + public static bool operator >(TestToken a, TestToken b) + { + return a.TstWei > b.TstWei; + } + + public static bool operator ==(TestToken a, TestToken b) + { + return a.TstWei == b.TstWei; + } + + public static bool operator !=(TestToken a, TestToken b) + { + return a.TstWei != b.TstWei; } } - public static class TokensIntExtensions + public static class TestTokensExtensions { - public static TestToken TestTokens(this int i) + public static TestToken TstWei(this int i) { - return TestTokens(Convert.ToDecimal(i)); + return TstWei(Convert.ToDecimal(i)); } - public static TestToken TestTokens(this decimal i) + public static TestToken TstWei(this decimal i) + { + return new TestToken(new BigInteger(i)); + } + + public static TestToken TstWei(this BigInteger i) { return new TestToken(i); } + + public static TestToken Tst(this int i) + { + return Tst(Convert.ToDecimal(i)); + } + + public static TestToken Tst(this decimal i) + { + return new TestToken(new BigInteger(i) * TestToken.WeiFactor); + } + + public static TestToken Tst(this BigInteger i) + { + return new TestToken(i * TestToken.WeiFactor); + } } } diff --git a/ProjectPlugins/CodexDiscordBotPlugin/CodexDiscordBotPlugin.cs b/ProjectPlugins/CodexDiscordBotPlugin/CodexDiscordBotPlugin.cs index cad50c12..556ef152 100644 --- a/ProjectPlugins/CodexDiscordBotPlugin/CodexDiscordBotPlugin.cs +++ b/ProjectPlugins/CodexDiscordBotPlugin/CodexDiscordBotPlugin.cs @@ -1,11 +1,13 @@ using Core; using KubernetesWorkflow; using KubernetesWorkflow.Types; +using Utils; namespace CodexDiscordBotPlugin { public class CodexDiscordBotPlugin : IProjectPlugin, IHasLogPrefix, IHasMetadata { + private const string ExpectedStartupMessage = "Debug option is set. Discord connection disabled!"; private readonly IPluginTools tools; public CodexDiscordBotPlugin(IPluginTools tools) @@ -29,31 +31,76 @@ namespace CodexDiscordBotPlugin { } - public RunningContainers Deploy(DiscordBotStartupConfig config) + public RunningPod Deploy(DiscordBotStartupConfig config) { var workflow = tools.CreateWorkflow(); return StartContainer(workflow, config); } - public RunningContainers DeployRewarder(RewarderBotStartupConfig config) + public RunningPod DeployRewarder(RewarderBotStartupConfig config) { var workflow = tools.CreateWorkflow(); return StartRewarderContainer(workflow, config); } - private RunningContainers StartContainer(IStartupWorkflow workflow, DiscordBotStartupConfig config) + private RunningPod StartContainer(IStartupWorkflow workflow, DiscordBotStartupConfig config) { var startupConfig = new StartupConfig(); startupConfig.NameOverride = config.Name; startupConfig.Add(config); - return workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig); + var pod = workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig).WaitForOnline(); + WaitForStartupMessage(workflow, pod); + workflow.CreateCrashWatcher(pod.Containers.Single()).Start(); + return pod; } - private RunningContainers StartRewarderContainer(IStartupWorkflow workflow, RewarderBotStartupConfig config) + private RunningPod StartRewarderContainer(IStartupWorkflow workflow, RewarderBotStartupConfig config) { var startupConfig = new StartupConfig(); + startupConfig.NameOverride = config.Name; startupConfig.Add(config); - return workflow.Start(1, new RewarderBotContainerRecipe(), startupConfig); + var pod = workflow.Start(1, new RewarderBotContainerRecipe(), startupConfig).WaitForOnline(); + workflow.CreateCrashWatcher(pod.Containers.Single()).Start(); + return pod; + } + + private void WaitForStartupMessage(IStartupWorkflow workflow, RunningPod pod) + { + var finder = new LogLineFinder(ExpectedStartupMessage, workflow); + Time.WaitUntil(() => + { + finder.FindLine(pod); + return finder.Found; + }, nameof(WaitForStartupMessage)); + } + + public class LogLineFinder : LogHandler + { + private readonly string message; + private readonly IStartupWorkflow workflow; + + public LogLineFinder(string message, IStartupWorkflow workflow) + { + this.message = message; + this.workflow = workflow; + } + + public void FindLine(RunningPod pod) + { + Found = false; + foreach (var c in pod.Containers) + { + workflow.DownloadContainerLog(c, this); + if (Found) return; + } + } + + public bool Found { get; private set; } + + protected override void ProcessLine(string line) + { + if (!Found && line.Contains(message)) Found = true; + } } } } diff --git a/ProjectPlugins/CodexDiscordBotPlugin/CoreInterfaceExtensions.cs b/ProjectPlugins/CodexDiscordBotPlugin/CoreInterfaceExtensions.cs index c17cec15..c91d7118 100644 --- a/ProjectPlugins/CodexDiscordBotPlugin/CoreInterfaceExtensions.cs +++ b/ProjectPlugins/CodexDiscordBotPlugin/CoreInterfaceExtensions.cs @@ -5,12 +5,12 @@ namespace CodexDiscordBotPlugin { public static class CoreInterfaceExtensions { - public static RunningContainers DeployCodexDiscordBot(this CoreInterface ci, DiscordBotStartupConfig config) + public static RunningPod DeployCodexDiscordBot(this CoreInterface ci, DiscordBotStartupConfig config) { return Plugin(ci).Deploy(config); } - public static RunningContainers DeployRewarderBot(this CoreInterface ci, RewarderBotStartupConfig config) + public static RunningPod DeployRewarderBot(this CoreInterface ci, RewarderBotStartupConfig config) { return Plugin(ci).DeployRewarder(config); } diff --git a/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotContainerRecipe.cs b/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotContainerRecipe.cs index 9a2e3fed..0a9d9872 100644 --- a/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotContainerRecipe.cs +++ b/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotContainerRecipe.cs @@ -7,7 +7,7 @@ namespace CodexDiscordBotPlugin public class DiscordBotContainerRecipe : ContainerRecipeFactory { public override string AppName => "discordbot-bibliotech"; - public override string Image => "codexstorage/codex-discordbot:sha-8c64352"; + public override string Image => "codexstorage/codex-discordbot:sha-8033da1"; public static string RewardsPort = "bot_rewards_port"; @@ -33,6 +33,8 @@ namespace CodexDiscordBotPlugin AddEnvVar("CODEXCONTRACTS_TOKENADDRESS", gethInfo.TokenAddress); AddEnvVar("CODEXCONTRACTS_ABI", gethInfo.Abi); + AddEnvVar("NODISCORD", "1"); + AddInternalPortAndVar("REWARDAPIPORT", RewardsPort); if (!string.IsNullOrEmpty(config.DataPath)) diff --git a/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotStartupConfig.cs b/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotStartupConfig.cs index bbef0b1a..e77552e5 100644 --- a/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotStartupConfig.cs +++ b/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotStartupConfig.cs @@ -27,8 +27,9 @@ public class RewarderBotStartupConfig { - public RewarderBotStartupConfig(string discordBotHost, int discordBotPort, string intervalMinutes, DateTime historyStartUtc, DiscordBotGethInfo gethInfo, string? dataPath) + public RewarderBotStartupConfig(string name, string discordBotHost, int discordBotPort, int intervalMinutes, DateTime historyStartUtc, DiscordBotGethInfo gethInfo, string? dataPath) { + Name = name; DiscordBotHost = discordBotHost; DiscordBotPort = discordBotPort; IntervalMinutes = intervalMinutes; @@ -37,9 +38,10 @@ DataPath = dataPath; } + public string Name { get; } public string DiscordBotHost { get; } public int DiscordBotPort { get; } - public string IntervalMinutes { get; } + public int IntervalMinutes { get; } public DateTime HistoryStartUtc { get; } public DiscordBotGethInfo GethInfo { get; } public string? DataPath { get; set; } diff --git a/ProjectPlugins/CodexDiscordBotPlugin/RewarderBotContainerRecipe.cs b/ProjectPlugins/CodexDiscordBotPlugin/RewarderBotContainerRecipe.cs index 816c910d..b35195b3 100644 --- a/ProjectPlugins/CodexDiscordBotPlugin/RewarderBotContainerRecipe.cs +++ b/ProjectPlugins/CodexDiscordBotPlugin/RewarderBotContainerRecipe.cs @@ -7,7 +7,7 @@ namespace CodexDiscordBotPlugin public class RewarderBotContainerRecipe : ContainerRecipeFactory { public override string AppName => "discordbot-rewarder"; - public override string Image => "codexstorage/codex-rewarderbot:sha-2ab84e2"; + public override string Image => "codexstorage/codex-rewarderbot:sha-8033da1"; protected override void Initialize(StartupConfig startupConfig) { @@ -17,7 +17,7 @@ namespace CodexDiscordBotPlugin AddEnvVar("DISCORDBOTHOST", config.DiscordBotHost); AddEnvVar("DISCORDBOTPORT", config.DiscordBotPort.ToString()); - AddEnvVar("INTERVALMINUTES", config.IntervalMinutes); + AddEnvVar("INTERVALMINUTES", config.IntervalMinutes.ToString()); var offset = new DateTimeOffset(config.HistoryStartUtc); AddEnvVar("CHECKHISTORY", offset.ToUnixTimeSeconds().ToString()); diff --git a/ProjectPlugins/CodexPlugin/ApiChecker.cs b/ProjectPlugins/CodexPlugin/ApiChecker.cs index 498aeb1d..d394405c 100644 --- a/ProjectPlugins/CodexPlugin/ApiChecker.cs +++ b/ProjectPlugins/CodexPlugin/ApiChecker.cs @@ -9,7 +9,7 @@ namespace CodexPlugin public class ApiChecker { // - private const string OpenApiYamlHash = "63-7F-46-5E-2C-60-7A-BD-0C-EC-32-87-61-1B-79-FA-C2-EF-73-81-BA-FA-28-77-33-02-81-30-80-5D-00-97"; + private const string OpenApiYamlHash = "67-76-AB-FC-54-4F-EB-81-F5-E4-F8-27-DF-82-92-41-63-A5-EA-1B-17-14-0C-BE-20-9C-B3-DF-CE-E4-AA-38"; private const string OpenApiFilePath = "/codex/openapi.yaml"; private const string DisableEnvironmentVariable = "CODEXPLUGIN_DISABLE_APICHECK"; @@ -38,7 +38,7 @@ namespace CodexPlugin if (string.IsNullOrEmpty(OpenApiYamlHash)) throw new Exception("OpenAPI yaml hash was not inserted by pre-build trigger."); } - public void CheckCompatibility(RunningContainers[] containers) + public void CheckCompatibility(RunningPod[] containers) { if (checkPassed) return; diff --git a/ProjectPlugins/CodexPlugin/CodexAccess.cs b/ProjectPlugins/CodexPlugin/CodexAccess.cs index bd2582ef..110a386f 100644 --- a/ProjectPlugins/CodexPlugin/CodexAccess.cs +++ b/ProjectPlugins/CodexPlugin/CodexAccess.cs @@ -2,27 +2,29 @@ using Core; using KubernetesWorkflow; using KubernetesWorkflow.Types; +using Logging; +using Newtonsoft.Json; using Utils; namespace CodexPlugin { - public class CodexAccess : ILogHandler + public class CodexAccess { + private readonly ILog log; private readonly IPluginTools tools; private readonly Mapper mapper = new Mapper(); - private bool hasContainerCrashed; - public CodexAccess(IPluginTools tools, RunningContainer container, CrashWatcher crashWatcher) + public CodexAccess(IPluginTools tools, RunningPod container, CrashWatcher crashWatcher) { this.tools = tools; + log = tools.GetLog(); Container = container; CrashWatcher = crashWatcher; - hasContainerCrashed = false; - CrashWatcher.Start(this); + CrashWatcher.Start(); } - public RunningContainer Container { get; } + public RunningPod Container { get; } public CrashWatcher CrashWatcher { get; } public DebugInfo GetDebugInfo() @@ -33,20 +35,23 @@ namespace CodexPlugin public DebugPeer GetDebugPeer(string peerId) { // Cannot use openAPI: debug/peer endpoint is not specified there. - var endpoint = GetEndpoint(); - var str = endpoint.HttpGetString($"debug/peer/{peerId}"); - - if (str.ToLowerInvariant() == "unable to find peer!") + return CrashCheck(() => { - return new DebugPeer - { - IsPeerFound = false - }; - } + var endpoint = GetEndpoint(); + var str = endpoint.HttpGetString($"debug/peer/{peerId}"); - var result = endpoint.Deserialize(str); - result.IsPeerFound = true; - return result; + if (str.ToLowerInvariant() == "unable to find peer!") + { + return new DebugPeer + { + IsPeerFound = false + }; + } + + var result = endpoint.Deserialize(str); + result.IsPeerFound = true; + return result; + }); } public void ConnectToPeer(string peerId, string[] peerMultiAddresses) @@ -58,14 +63,19 @@ namespace CodexPlugin }); } - public string UploadFile(FileStream fileStream) + public string UploadFile(FileStream fileStream, Action onFailure) { - return OnCodex(api => api.UploadAsync(fileStream)); + return OnCodex( + api => api.UploadAsync(fileStream), + CreateRetryConfig(nameof(UploadFile), onFailure)); } - public Stream DownloadFile(string contentId) + public Stream DownloadFile(string contentId, Action onFailure) { - var fileResponse = OnCodex(api => api.DownloadNetworkAsync(contentId)); + var fileResponse = OnCodex( + api => api.DownloadNetworkAsync(contentId), + CreateRetryConfig(nameof(DownloadFile), onFailure)); + if (fileResponse.StatusCode != 200) throw new Exception("Download failed with StatusCode: " + fileResponse.StatusCode); return fileResponse.Stream; } @@ -88,9 +98,27 @@ namespace CodexPlugin return OnCodex(api => api.CreateStorageRequestAsync(request.ContentId.Id, body)); } + public CodexSpace Space() + { + var space = OnCodex(api => api.SpaceAsync()); + return mapper.Map(space); + } + public StoragePurchase GetPurchaseStatus(string purchaseId) { - return mapper.Map(OnCodex(api => api.GetPurchaseAsync(purchaseId))); + return CrashCheck(() => + { + var endpoint = GetEndpoint(); + return Time.Retry(() => + { + var str = endpoint.HttpGetString($"storage/purchases/{purchaseId}"); + if (string.IsNullOrEmpty(str)) throw new Exception("Empty response."); + return JsonConvert.DeserializeObject(str)!; + }, nameof(GetPurchaseStatus)); + }); + + // TODO: current getpurchase api does not line up with its openapi spec. + // return mapper.Map(OnCodex(api => api.GetPurchaseAsync(purchaseId))); } public string GetName() @@ -104,19 +132,67 @@ namespace CodexPlugin return workflow.GetPodInfo(Container); } + public void LogDiskSpace(string msg) + { + try + { + var diskInfo = tools.CreateWorkflow().ExecuteCommand(Container.Containers.Single(), "df", "--sync"); + Log($"{msg} - Disk info: {diskInfo}"); + } + catch (Exception e) + { + Log("Failed to get disk info: " + e); + } + } + + public void DeleteRepoFolder() + { + try + { + var containerNumber = Container.Containers.First().Recipe.Number; + var dataDir = $"datadir{containerNumber}"; + var workflow = tools.CreateWorkflow(); + workflow.ExecuteCommand(Container.Containers.First(), "rm", "-Rfv", $"/codex/{dataDir}/repo"); + Log("Deleted repo folder."); + } + catch (Exception e) + { + Log("Unable to delete repo folder: " + e); + } + } + private T OnCodex(Func> action) { - var address = GetAddress(); - var result = tools.CreateHttp(CheckContainerCrashed) - .OnClient(client => - { - var api = new CodexApi(client); - api.BaseUrl = $"{address.Host}:{address.Port}/api/codex/v1"; - return Time.Wait(action(api)); - }); + var result = tools.CreateHttp(CheckContainerCrashed).OnClient(client => CallCodex(client, action)); return result; } + private T OnCodex(Func> action, Retry retry) + { + var result = tools.CreateHttp(CheckContainerCrashed).OnClient(client => CallCodex(client, action), retry); + return result; + } + + private T CallCodex(HttpClient client, Func> action) + { + var address = GetAddress(); + var api = new CodexApi(client); + api.BaseUrl = $"{address.Host}:{address.Port}/api/codex/v1"; + return CrashCheck(() => Time.Wait(action(api))); + } + + private T CrashCheck(Func action) + { + try + { + return action(); + } + finally + { + CrashWatcher.HasContainerCrashed(); + } + } + private IEndpoint GetEndpoint() { return tools @@ -126,31 +202,67 @@ namespace CodexPlugin private Address GetAddress() { - return Container.GetAddress(tools.GetLog(), CodexContainerRecipe.ApiPortTag); + return Container.Containers.Single().GetAddress(log, CodexContainerRecipe.ApiPortTag); } private void CheckContainerCrashed(HttpClient client) { - if (hasContainerCrashed) throw new Exception("Container has crashed."); + if (CrashWatcher.HasContainerCrashed()) throw new Exception($"Container {GetName()} has crashed."); } - public void Log(Stream crashLog) + private Retry CreateRetryConfig(string description, Action onFailure) { - var log = tools.GetLog(); - var file = log.CreateSubfile(); - log.Log($"Container {Container.Name} has crashed. Downloading crash log to '{file.FullFilename}'..."); - file.Write($"Container Crash Log for {Container.Name}."); + var timeSet = tools.TimeSet; - using var reader = new StreamReader(crashLog); - var line = reader.ReadLine(); - while (line != null) + return new Retry(description, timeSet.HttpRetryTimeout(), timeSet.HttpCallRetryDelay(), failure => { - file.Write(line); - line = reader.ReadLine(); + onFailure(failure); + Investigate(failure, timeSet); + }); + } + + private void Investigate(Failure failure, ITimeSet timeSet) + { + Log($"Retry {failure.TryNumber} took {Time.FormatDuration(failure.Duration)} and failed with '{failure.Exception}'. " + + $"(HTTP timeout = {Time.FormatDuration(timeSet.HttpCallTimeout())}) " + + $"Checking if node responds to debug/info..."); + + LogDiskSpace("After retry failure"); + + try + { + var debugInfo = GetDebugInfo(); + if (string.IsNullOrEmpty(debugInfo.Spr)) + { + Log("Did not get value debug/info response."); + Throw(failure); + } + else + { + Log("Got valid response from debug/info."); + } + } + catch (Exception ex) + { + Log("Got exception from debug/info call: " + ex); + Throw(failure); } - log.Log("Crash log successfully downloaded."); - hasContainerCrashed = true; + if (failure.Duration < timeSet.HttpCallTimeout()) + { + Log("Retry failed within HTTP timeout duration."); + Throw(failure); + } + } + + private void Throw(Failure failure) + { + throw failure.Exception; + } + + private void Log(string msg) + { + log.Log($"{GetName()} {msg}"); } } } diff --git a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs index 4e2cc8f9..bb884acf 100644 --- a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs +++ b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs @@ -7,7 +7,8 @@ namespace CodexPlugin { public class CodexContainerRecipe : ContainerRecipeFactory { - private const string DefaultDockerImage = "codexstorage/nim-codex:sha-f2f1dd5-dist-tests"; + private const string DefaultDockerImage = "codexstorage/nim-codex:sha-471ebb2-dist-tests"; + public const string ApiPortTag = "codex_api_port"; public const string ListenPortTag = "codex_listen_port"; public const string MetricsPortTag = "codex_metrics_port"; @@ -108,8 +109,9 @@ namespace CodexPlugin // Custom scripting in the Codex test image will write this variable to a private-key file, // and pass the correct filename to Codex. - AddEnvVar("PRIV_KEY", marketplaceSetup.EthAccount.PrivateKey); - Additional(marketplaceSetup.EthAccount); + var account = marketplaceSetup.EthAccountSetup.GetNew(); + AddEnvVar("PRIV_KEY", account.PrivateKey); + Additional(account); SetCommandOverride(marketplaceSetup); if (marketplaceSetup.IsValidator) @@ -118,7 +120,7 @@ namespace CodexPlugin } } - if(!string.IsNullOrEmpty(config.NameOverride)) + if (!string.IsNullOrEmpty(config.NameOverride)) { AddEnvVar("CODEX_NODENAME", config.NameOverride); } @@ -158,7 +160,7 @@ namespace CodexPlugin private ByteSize GetVolumeCapacity(CodexStartupConfig config) { - if (config.StorageQuota != null) return config.StorageQuota; + if (config.StorageQuota != null) return config.StorageQuota.Multiply(1.2); // Default Codex quota: 8 Gb, using +20% to be safe. return 8.GB().Multiply(1.2); } diff --git a/ProjectPlugins/CodexPlugin/CodexDeployment.cs b/ProjectPlugins/CodexPlugin/CodexDeployment.cs index 9f74dd31..7cbf4506 100644 --- a/ProjectPlugins/CodexPlugin/CodexDeployment.cs +++ b/ProjectPlugins/CodexPlugin/CodexDeployment.cs @@ -7,8 +7,8 @@ namespace CodexPlugin public class CodexDeployment { public CodexDeployment(CodexInstance[] codexInstances, GethDeployment gethDeployment, - CodexContractsDeployment codexContractsDeployment, RunningContainers? prometheusContainer, - RunningContainers? discordBotContainer, DeploymentMetadata metadata, + CodexContractsDeployment codexContractsDeployment, RunningPod? prometheusContainer, + RunningPod? discordBotContainer, DeploymentMetadata metadata, String id) { Id = id; @@ -24,20 +24,20 @@ namespace CodexPlugin public CodexInstance[] CodexInstances { get; } public GethDeployment GethDeployment { get; } public CodexContractsDeployment CodexContractsDeployment { get; } - public RunningContainers? PrometheusContainer { get; } - public RunningContainers? DiscordBotContainer { get; } + public RunningPod? PrometheusContainer { get; } + public RunningPod? DiscordBotContainer { get; } public DeploymentMetadata Metadata { get; } } public class CodexInstance { - public CodexInstance(RunningContainers containers, DebugInfo info) + public CodexInstance(RunningPod pod, DebugInfo info) { - Containers = containers; + Pod = pod; Info = info; } - public RunningContainers Containers { get; } + public RunningPod Pod { get; } public DebugInfo Info { get; } } diff --git a/ProjectPlugins/CodexPlugin/CodexLogLine.cs b/ProjectPlugins/CodexPlugin/CodexLogLine.cs new file mode 100644 index 00000000..d2758145 --- /dev/null +++ b/ProjectPlugins/CodexPlugin/CodexLogLine.cs @@ -0,0 +1,105 @@ +using System.Globalization; + +namespace CodexPlugin +{ + public class CodexLogLine + { + public static CodexLogLine? Parse(string line) + { + try + { + if (string.IsNullOrEmpty(line) || + line.Length < 34 || + line[3] != ' ' || + line[33] != ' ') return null; + + line = line.Replace(Environment.NewLine, string.Empty); + + var level = line.Substring(0, 3); + var dtLine = line.Substring(4, 23); + + var firstEqualSign = line.IndexOf('='); + var msgStart = 34; + var msgEnd = line.Substring(0, firstEqualSign).LastIndexOf(' '); + var msg = line.Substring(msgStart, msgEnd - msgStart).Trim(); + var attrsLine = line.Substring(msgEnd); + + var attrs = SplitAttrs(attrsLine); + + var format = "yyyy-MM-dd HH:mm:ss.fff"; + var dt = DateTime.ParseExact(dtLine, format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal).ToUniversalTime(); + + return new CodexLogLine() + { + LogLevel = level, + TimestampUtc = dt, + Message = msg, + Attributes = attrs + }; + } + catch + { + return null; + } + } + + public string LogLevel { get; set; } = string.Empty; + public DateTime TimestampUtc { get; set; } + public string Message { get; set; } = string.Empty; + public Dictionary Attributes { get; private set; } = new Dictionary(); + + /// + /// After too much time spent cursing at regexes, here's what I got: + /// Parses input string into 'key=value' pair, considerate of quoted (") values. + /// + private static Dictionary SplitAttrs(string input) + { + input += " "; + var result = new Dictionary(); + + var key = string.Empty; + var value = string.Empty; + var mode = 1; + var inQuote = false; + + foreach (var c in input) + { + if (mode == 1) + { + if (c == '=') mode = 2; + else if (c == ' ') + { + if (string.IsNullOrEmpty(key)) continue; + else + { + result.Add(key, string.Empty); + key = string.Empty; + value = string.Empty; + } + } + else key += c; + } + else if (mode == 2) + { + if (c == ' ' && !inQuote) + { + result.Add(key, value); + key = string.Empty; + value = string.Empty; + mode = 1; + } + else if (c == '\"') + { + inQuote = !inQuote; + } + else + { + value += c; + } + } + } + + return result; + } + } +} diff --git a/ProjectPlugins/CodexPlugin/CodexNode.cs b/ProjectPlugins/CodexPlugin/CodexNode.cs index e1e475ce..c5372f65 100644 --- a/ProjectPlugins/CodexPlugin/CodexNode.cs +++ b/ProjectPlugins/CodexPlugin/CodexNode.cs @@ -15,14 +15,24 @@ namespace CodexPlugin DebugInfo GetDebugInfo(); DebugPeer GetDebugPeer(string peerId); ContentId UploadFile(TrackedFile file); + ContentId UploadFile(TrackedFile file, Action onFailure); TrackedFile? DownloadContent(ContentId contentId, string fileLabel = ""); + TrackedFile? DownloadContent(ContentId contentId, Action onFailure, string fileLabel = ""); LocalDatasetList LocalFiles(); + CodexSpace Space(); void ConnectToPeer(ICodexNode node); DebugInfoVersion Version { get; } IMarketplaceAccess Marketplace { get; } CrashWatcher CrashWatcher { get; } PodInfo GetPodInfo(); ITransferSpeeds TransferSpeeds { get; } + EthAccount EthAccount { get; } + + /// + /// Warning! The node is not usable after this. + /// TODO: Replace with delete-blocks debug call once available in Codex. + /// + void DeleteRepoFolder(); void Stop(bool waitTillStopped); } @@ -30,13 +40,13 @@ namespace CodexPlugin { private const string UploadFailedMessage = "Unable to store block"; private readonly IPluginTools tools; - private readonly EthAddress? ethAddress; + private readonly EthAccount? ethAccount; private readonly TransferSpeeds transferSpeeds; - public CodexNode(IPluginTools tools, CodexAccess codexAccess, CodexNodeGroup group, IMarketplaceAccess marketplaceAccess, EthAddress? ethAddress) + public CodexNode(IPluginTools tools, CodexAccess codexAccess, CodexNodeGroup group, IMarketplaceAccess marketplaceAccess, EthAccount? ethAccount) { this.tools = tools; - this.ethAddress = ethAddress; + this.ethAccount = ethAccount; CodexAccess = codexAccess; Group = group; Marketplace = marketplaceAccess; @@ -44,7 +54,9 @@ namespace CodexPlugin transferSpeeds = new TransferSpeeds(); } - public RunningContainer Container { get { return CodexAccess.Container; } } + public RunningPod Pod { get { return CodexAccess.Container; } } + + public RunningContainer Container { get { return Pod.Containers.Single(); } } public CodexAccess CodexAccess { get; } public CrashWatcher CrashWatcher { get => CodexAccess.CrashWatcher; } public CodexNodeGroup Group { get; } @@ -56,7 +68,7 @@ namespace CodexPlugin { get { - return new MetricsScrapeTarget(CodexAccess.Container, CodexContainerRecipe.MetricsPortTag); + return new MetricsScrapeTarget(CodexAccess.Container.Containers.First(), CodexContainerRecipe.MetricsPortTag); } } @@ -64,21 +76,30 @@ namespace CodexPlugin { get { - if (ethAddress == null) throw new Exception("Marketplace is not enabled for this Codex node. Please start it with the option '.EnableMarketplace(...)' to enable it."); - return ethAddress; + EnsureMarketplace(); + return ethAccount!.EthAddress; + } + } + + public EthAccount EthAccount + { + get + { + EnsureMarketplace(); + return ethAccount!; } } public string GetName() { - return CodexAccess.Container.Name; + return Container.Name; } public DebugInfo GetDebugInfo() { var debugInfo = CodexAccess.GetDebugInfo(); var known = string.Join(",", debugInfo.Table.Nodes.Select(n => n.PeerId)); - Log($"Got DebugInfo with id: '{debugInfo.Id}'. This node knows: {known}"); + Log($"Got DebugInfo with id: {debugInfo.Id}. This node knows: [{known}]"); return debugInfo; } @@ -89,13 +110,20 @@ namespace CodexPlugin public ContentId UploadFile(TrackedFile file) { + return UploadFile(file, DoNothing); + } + + public ContentId UploadFile(TrackedFile file, Action onFailure) + { + CodexAccess.LogDiskSpace("Before upload"); + using var fileStream = File.OpenRead(file.Filename); var logMessage = $"Uploading file {file.Describe()}..."; Log(logMessage); var measurement = Stopwatch.Measure(tools.GetLog(), logMessage, () => { - return CodexAccess.UploadFile(fileStream); + return CodexAccess.UploadFile(fileStream, onFailure); }); var response = measurement.Value; @@ -105,15 +133,22 @@ namespace CodexPlugin if (response.StartsWith(UploadFailedMessage)) FrameworkAssert.Fail("Node failed to store block."); Log($"Uploaded file. Received contentId: '{response}'."); + CodexAccess.LogDiskSpace("After upload"); + return new ContentId(response); } public TrackedFile? DownloadContent(ContentId contentId, string fileLabel = "") + { + return DownloadContent(contentId, DoNothing, fileLabel); + } + + public TrackedFile? DownloadContent(ContentId contentId, Action onFailure, string fileLabel = "") { var logMessage = $"Downloading for contentId: '{contentId.Id}'..."; Log(logMessage); var file = tools.GetFileManager().CreateEmptyFile(fileLabel); - var measurement = Stopwatch.Measure(tools.GetLog(), logMessage, () => DownloadToFile(contentId.Id, file)); + var measurement = Stopwatch.Measure(tools.GetLog(), logMessage, () => DownloadToFile(contentId.Id, file, onFailure)); transferSpeeds.AddDownloadSample(file.GetFilesize(), measurement); Log($"Downloaded file {file.Describe()} to '{file.Filename}'."); return file; @@ -124,6 +159,11 @@ namespace CodexPlugin return CodexAccess.LocalFiles(); } + public CodexSpace Space() + { + return CodexAccess.Space(); + } + public void ConnectToPeer(ICodexNode node) { var peer = (CodexNode)node; @@ -140,13 +180,16 @@ namespace CodexPlugin return CodexAccess.GetPodInfo(); } + public void DeleteRepoFolder() + { + CodexAccess.DeleteRepoFolder(); + } + public void Stop(bool waitTillStopped) { - if (Group.Count() > 1) throw new InvalidOperationException("Codex-nodes that are part of a group cannot be " + - "individually shut down. Use 'BringOffline()' on the group object to stop the group. This method is only " + - "available for codex-nodes in groups of 1."); - - Group.BringOffline(waitTillStopped); + Log("Stopping..."); + CrashWatcher.Stop(); + Group.Stop(this, waitTillStopped); } public void EnsureOnlineGetVersionResponse() @@ -171,19 +214,21 @@ namespace CodexPlugin // The peer we want to connect is in a different pod. // We must replace the default IP with the pod IP in the multiAddress. var workflow = tools.CreateWorkflow(); - var podInfo = workflow.GetPodInfo(peer.Container); + var podInfo = workflow.GetPodInfo(peer.Pod); return peerInfo.Addrs.Select(a => a .Replace("0.0.0.0", podInfo.Ip)) .ToArray(); } - private void DownloadToFile(string contentId, TrackedFile file) + private void DownloadToFile(string contentId, TrackedFile file, Action onFailure) { + CodexAccess.LogDiskSpace("Before download"); + using var fileStream = File.OpenWrite(file.Filename); try { - using var downloadStream = CodexAccess.DownloadFile(contentId); + using var downloadStream = CodexAccess.DownloadFile(contentId, onFailure); downloadStream.CopyTo(fileStream); } catch @@ -191,11 +236,22 @@ namespace CodexPlugin Log($"Failed to download file '{contentId}'."); throw; } + + CodexAccess.LogDiskSpace("After download"); + } + + private void EnsureMarketplace() + { + if (ethAccount == null) throw new Exception("Marketplace is not enabled for this Codex node. Please start it with the option '.EnableMarketplace(...)' to enable it."); } private void Log(string msg) { tools.GetLog().Log($"{GetName()}: {msg}"); } + + private void DoNothing(Failure failure) + { + } } } diff --git a/ProjectPlugins/CodexPlugin/CodexNodeFactory.cs b/ProjectPlugins/CodexPlugin/CodexNodeFactory.cs index 05b80b28..425d489d 100644 --- a/ProjectPlugins/CodexPlugin/CodexNodeFactory.cs +++ b/ProjectPlugins/CodexPlugin/CodexNodeFactory.cs @@ -22,22 +22,22 @@ namespace CodexPlugin public CodexNode CreateOnlineCodexNode(CodexAccess access, CodexNodeGroup group) { - var ethAddress = GetEthAddress(access); - var marketplaceAccess = GetMarketplaceAccess(access, ethAddress); - return new CodexNode(tools, access, group, marketplaceAccess, ethAddress); + var ethAccount = GetEthAccount(access); + var marketplaceAccess = GetMarketplaceAccess(access, ethAccount); + return new CodexNode(tools, access, group, marketplaceAccess, ethAccount); } - private IMarketplaceAccess GetMarketplaceAccess(CodexAccess codexAccess, EthAddress? ethAddress) + private IMarketplaceAccess GetMarketplaceAccess(CodexAccess codexAccess, EthAccount? ethAccount) { - if (ethAddress == null) return new MarketplaceUnavailable(); + if (ethAccount == null) return new MarketplaceUnavailable(); return new MarketplaceAccess(tools.GetLog(), codexAccess); } - private EthAddress? GetEthAddress(CodexAccess access) + private EthAccount? GetEthAccount(CodexAccess access) { - var ethAccount = access.Container.Recipe.Additionals.Get(); + var ethAccount = access.Container.Containers.Single().Recipe.Additionals.Get(); if (ethAccount == null) return null; - return ethAccount.EthAddress; + return ethAccount; } public CrashWatcher CreateCrashWatcher(RunningContainer c) diff --git a/ProjectPlugins/CodexPlugin/CodexNodeGroup.cs b/ProjectPlugins/CodexPlugin/CodexNodeGroup.cs index 1f7ed4eb..47bf35d9 100644 --- a/ProjectPlugins/CodexPlugin/CodexNodeGroup.cs +++ b/ProjectPlugins/CodexPlugin/CodexNodeGroup.cs @@ -15,11 +15,11 @@ namespace CodexPlugin { private readonly CodexStarter starter; - public CodexNodeGroup(CodexStarter starter, IPluginTools tools, RunningContainers[] containers, ICodexNodeFactory codexNodeFactory) + public CodexNodeGroup(CodexStarter starter, IPluginTools tools, RunningPod[] containers, ICodexNodeFactory codexNodeFactory) { this.starter = starter; Containers = containers; - Nodes = containers.Containers().Select(c => CreateOnlineCodexNode(c, tools, codexNodeFactory)).ToArray(); + Nodes = containers.Select(c => CreateOnlineCodexNode(c, tools, codexNodeFactory)).ToArray(); Version = new DebugInfoVersion(); } @@ -39,7 +39,14 @@ namespace CodexPlugin Containers = null!; } - public RunningContainers[] Containers { get; private set; } + public void Stop(CodexNode node, bool waitTillStopped) + { + starter.Stop(node.Pod, waitTillStopped); + Nodes = Nodes.Where(n => n != node).ToArray(); + Containers = Containers.Where(c => c != node.Pod).ToArray(); + } + + public RunningPod[] Containers { get; private set; } public CodexNode[] Nodes { get; private set; } public DebugInfoVersion Version { get; private set; } public IMetricsScrapeTarget[] ScrapeTargets => Nodes.Select(n => n.MetricsScrapeTarget).ToArray(); @@ -74,9 +81,9 @@ namespace CodexPlugin Version = first; } - private CodexNode CreateOnlineCodexNode(RunningContainer c, IPluginTools tools, ICodexNodeFactory factory) + private CodexNode CreateOnlineCodexNode(RunningPod c, IPluginTools tools, ICodexNodeFactory factory) { - var watcher = factory.CreateCrashWatcher(c); + var watcher = factory.CreateCrashWatcher(c.Containers.Single()); var access = new CodexAccess(tools, c, watcher); return factory.CreateOnlineCodexNode(access, this); } diff --git a/ProjectPlugins/CodexPlugin/CodexPlugin.cs b/ProjectPlugins/CodexPlugin/CodexPlugin.cs index 9b8586e8..7b722ed8 100644 --- a/ProjectPlugins/CodexPlugin/CodexPlugin.cs +++ b/ProjectPlugins/CodexPlugin/CodexPlugin.cs @@ -32,13 +32,13 @@ namespace CodexPlugin { } - public RunningContainers[] DeployCodexNodes(int numberOfNodes, Action setup) + public RunningPod[] DeployCodexNodes(int numberOfNodes, Action setup) { var codexSetup = GetSetup(numberOfNodes, setup); return codexStarter.BringOnline(codexSetup); } - public ICodexNodeGroup WrapCodexContainers(CoreInterface coreInterface, RunningContainers[] containers) + public ICodexNodeGroup WrapCodexContainers(CoreInterface coreInterface, RunningPod[] containers) { containers = containers.Select(c => SerializeGate.Gate(c)).ToArray(); return codexStarter.WrapCodexContainers(coreInterface, containers); diff --git a/ProjectPlugins/CodexPlugin/CodexSetup.cs b/ProjectPlugins/CodexPlugin/CodexSetup.cs index 6f9d7679..44454c77 100644 --- a/ProjectPlugins/CodexPlugin/CodexSetup.cs +++ b/ProjectPlugins/CodexPlugin/CodexSetup.cs @@ -52,6 +52,7 @@ namespace CodexPlugin public CodexLogLevel Libp2p { get; set; } public CodexLogLevel ContractClock { get; set; } = CodexLogLevel.Warn; public CodexLogLevel? BlockExchange { get; } + public CodexLogLevel JsonSerialize { get; set; } = CodexLogLevel.Warn; } public class CodexSetup : CodexStartupConfig, ICodexSetup @@ -167,8 +168,8 @@ namespace CodexPlugin public bool IsStorageNode { get; private set; } public bool IsValidator { get; private set; } public Ether InitialEth { get; private set; } = 0.Eth(); - public TestToken InitialTestTokens { get; private set; } = 0.TestTokens(); - public EthAccount EthAccount { get; private set; } = EthAccount.GenerateNew(); + public TestToken InitialTestTokens { get; private set; } = 0.Tst(); + public EthAccountSetup EthAccountSetup { get; private set; } = new EthAccountSetup(); public IMarketplaceSetup AsStorageNode() { @@ -184,7 +185,7 @@ namespace CodexPlugin public IMarketplaceSetup WithAccount(EthAccount account) { - EthAccount = account; + EthAccountSetup.Pin(account); return this; } @@ -200,10 +201,41 @@ namespace CodexPlugin var result = "[(clientNode)"; // When marketplace is enabled, being a clientNode is implicit. result += IsStorageNode ? "(storageNode)" : "()"; result += IsValidator ? "(validator)" : "() "; - result += $"Address: '{EthAccount.EthAddress}' "; - result += $"InitialEth/TT({InitialEth.Eth}/{InitialTestTokens.Amount})"; + result += $"Address: '{EthAccountSetup}' "; + result += $"{InitialEth.Eth} / {InitialTestTokens}"; result += "] "; return result; } } + + public class EthAccountSetup + { + private readonly List accounts = new List(); + private bool pinned = false; + + public void Pin(EthAccount account) + { + accounts.Add(account); + pinned = true; + } + + public EthAccount GetNew() + { + if (pinned) return accounts.Last(); + + var a = EthAccount.GenerateNew(); + accounts.Add(a); + return a; + } + + public EthAccount[] GetAll() + { + return accounts.ToArray(); + } + + public override string ToString() + { + return string.Join(",", accounts.Select(a => a.ToString()).ToArray()); + } + } } diff --git a/ProjectPlugins/CodexPlugin/CodexStarter.cs b/ProjectPlugins/CodexPlugin/CodexStarter.cs index 05db6bdf..77fd89a6 100644 --- a/ProjectPlugins/CodexPlugin/CodexStarter.cs +++ b/ProjectPlugins/CodexPlugin/CodexStarter.cs @@ -19,7 +19,7 @@ namespace CodexPlugin apiChecker = new ApiChecker(pluginTools); } - public RunningContainers[] BringOnline(CodexSetup codexSetup) + public RunningPod[] BringOnline(CodexSetup codexSetup) { LogSeparator(); Log($"Starting {codexSetup.Describe()}..."); @@ -33,15 +33,15 @@ namespace CodexPlugin foreach (var rc in containers) { var podInfo = GetPodInfo(rc); - var podInfos = string.Join(", ", rc.Containers.Select(c => $"Container: '{c.Name}' runs at '{podInfo.K8SNodeName}'={podInfo.Ip}")); - Log($"Started {codexSetup.NumberOfNodes} nodes of image '{containers.Containers().First().Recipe.Image}'. ({podInfos})"); + var podInfos = string.Join(", ", rc.Containers.Select(c => $"Container: '{c.Name}' PodLabel: '{c.RunningPod.StartResult.Deployment.PodLabel}' runs at '{podInfo.K8SNodeName}'={podInfo.Ip}")); + Log($"Started {codexSetup.NumberOfNodes} nodes of image '{containers.First().Containers.First().Recipe.Image}'. ({podInfos})"); } LogSeparator(); return containers; } - public ICodexNodeGroup WrapCodexContainers(CoreInterface coreInterface, RunningContainers[] containers) + public ICodexNodeGroup WrapCodexContainers(CoreInterface coreInterface, RunningPod[] containers) { var codexNodeFactory = new CodexNodeFactory(pluginTools); @@ -65,6 +65,14 @@ namespace CodexPlugin Log("Stopped."); } + public void Stop(RunningPod pod, bool waitTillStopped) + { + Log($"Stopping node..."); + var workflow = pluginTools.CreateWorkflow(); + workflow.Stop(pod, waitTillStopped); + Log("Stopped."); + } + public string GetCodexId() { if (versionResponse != null) return versionResponse.Version; @@ -85,24 +93,27 @@ namespace CodexPlugin return startupConfig; } - private RunningContainers[] StartCodexContainers(StartupConfig startupConfig, int numberOfNodes, ILocation location) + private RunningPod[] StartCodexContainers(StartupConfig startupConfig, int numberOfNodes, ILocation location) { - var result = new List(); + var futureContainers = new List(); for (var i = 0; i < numberOfNodes; i++) { var workflow = pluginTools.CreateWorkflow(); - result.Add(workflow.Start(1, location, recipe, startupConfig)); + futureContainers.Add(workflow.Start(1, location, recipe, startupConfig)); } - return result.ToArray(); + + return futureContainers + .Select(f => f.WaitForOnline()) + .ToArray(); } - private PodInfo GetPodInfo(RunningContainers rc) + private PodInfo GetPodInfo(RunningPod rc) { var workflow = pluginTools.CreateWorkflow(); return workflow.GetPodInfo(rc); } - private CodexNodeGroup CreateCodexGroup(CoreInterface coreInterface, RunningContainers[] runningContainers, CodexNodeFactory codexNodeFactory) + private CodexNodeGroup CreateCodexGroup(CoreInterface coreInterface, RunningPod[] runningContainers, CodexNodeFactory codexNodeFactory) { var group = new CodexNodeGroup(this, pluginTools, runningContainers, codexNodeFactory); @@ -119,10 +130,10 @@ namespace CodexPlugin return group; } - private void CodexNodesNotOnline(CoreInterface coreInterface, RunningContainers[] runningContainers) + private void CodexNodesNotOnline(CoreInterface coreInterface, RunningPod[] runningContainers) { Log("Codex nodes failed to start"); - foreach (var container in runningContainers.Containers()) coreInterface.DownloadLog(container); + foreach (var container in runningContainers.First().Containers) coreInterface.DownloadLog(container); } private void LogSeparator() diff --git a/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs b/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs index dbed45ab..8e7ead12 100644 --- a/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs +++ b/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs @@ -73,11 +73,18 @@ namespace CodexPlugin "contracts", "clock" }; + var jsonSerializeTopics = new[] + { + "serde", + "json", + "serialization" + }; level = $"{level};" + $"{CustomTopics.DiscV5.ToString()!.ToLowerInvariant()}:{string.Join(",", discV5Topics)};" + $"{CustomTopics.Libp2p.ToString()!.ToLowerInvariant()}:{string.Join(",", libp2pTopics)};" + - $"{CustomTopics.ContractClock.ToString().ToLowerInvariant()}:{string.Join(",", contractClockTopics)}"; + $"{CustomTopics.ContractClock.ToString().ToLowerInvariant()}:{string.Join(",", contractClockTopics)};" + + $"{CustomTopics.JsonSerialize.ToString().ToLowerInvariant()}:{string.Join(",", jsonSerializeTopics)}"; if (CustomTopics.BlockExchange != null) { diff --git a/ProjectPlugins/CodexPlugin/CodexTypes.cs b/ProjectPlugins/CodexPlugin/CodexTypes.cs index 945251e5..d40fb13b 100644 --- a/ProjectPlugins/CodexPlugin/CodexTypes.cs +++ b/ProjectPlugins/CodexPlugin/CodexTypes.cs @@ -105,4 +105,18 @@ namespace CodexPlugin return HashCode.Combine(Id); } } + + public class CodexSpace + { + public long TotalBlocks { get; set; } + public long QuotaMaxBytes { get; set; } + public long QuotaUsedBytes { get; set; } + public long QuotaReservedBytes { get; set; } + public long FreeBytes => QuotaMaxBytes - (QuotaUsedBytes + QuotaReservedBytes); + + public override string ToString() + { + return JsonConvert.SerializeObject(this); + } + } } diff --git a/ProjectPlugins/CodexPlugin/CoreInterfaceExtensions.cs b/ProjectPlugins/CodexPlugin/CoreInterfaceExtensions.cs index 529b7fd8..5f6065df 100644 --- a/ProjectPlugins/CodexPlugin/CoreInterfaceExtensions.cs +++ b/ProjectPlugins/CodexPlugin/CoreInterfaceExtensions.cs @@ -5,12 +5,12 @@ namespace CodexPlugin { public static class CoreInterfaceExtensions { - public static RunningContainers[] DeployCodexNodes(this CoreInterface ci, int number, Action setup) + public static RunningPod[] DeployCodexNodes(this CoreInterface ci, int number, Action setup) { return Plugin(ci).DeployCodexNodes(number, setup); } - public static ICodexNodeGroup WrapCodexContainers(this CoreInterface ci, RunningContainers[] containers) + public static ICodexNodeGroup WrapCodexContainers(this CoreInterface ci, RunningPod[] containers) { return Plugin(ci).WrapCodexContainers(ci, containers); } diff --git a/ProjectPlugins/CodexPlugin/Mapper.cs b/ProjectPlugins/CodexPlugin/Mapper.cs index 9db2a84c..c175ae14 100644 --- a/ProjectPlugins/CodexPlugin/Mapper.cs +++ b/ProjectPlugins/CodexPlugin/Mapper.cs @@ -1,4 +1,5 @@ using CodexContractsPlugin; +using CodexOpenApi; using Newtonsoft.Json.Linq; using System.Numerics; using Utils; @@ -56,34 +57,78 @@ namespace CodexPlugin ProofProbability = ToDecInt(purchase.ProofProbability), Reward = ToDecInt(purchase.PricePerSlotPerSecond), Collateral = ToDecInt(purchase.RequiredCollateral), - Expiry = ToDecInt(DateTimeOffset.UtcNow.ToUnixTimeSeconds() + purchase.Expiry.TotalSeconds), + Expiry = ToDecInt(purchase.Expiry.TotalSeconds), Nodes = Convert.ToInt32(purchase.MinRequiredNumberOfNodes), Tolerance = Convert.ToInt32(purchase.NodeFailureTolerance) }; } - public StoragePurchase Map(CodexOpenApi.Purchase purchase) - { - return new StoragePurchase - { - State = purchase.State, - Error = purchase.Error - }; - } + // TODO: Fix openapi spec for this call. + //public StoragePurchase Map(CodexOpenApi.Purchase purchase) + //{ + // return new StoragePurchase(Map(purchase.Request)) + // { + // State = purchase.State, + // Error = purchase.Error + // }; + //} + + //public StorageRequest Map(CodexOpenApi.StorageRequest request) + //{ + // return new StorageRequest(Map(request.Ask), Map(request.Content)) + // { + // Id = request.Id, + // Client = request.Client, + // Expiry = TimeSpan.FromSeconds(Convert.ToInt64(request.Expiry)), + // Nonce = request.Nonce + // }; + //} + + //public StorageAsk Map(CodexOpenApi.StorageAsk ask) + //{ + // return new StorageAsk + // { + // Duration = TimeSpan.FromSeconds(Convert.ToInt64(ask.Duration)), + // MaxSlotLoss = ask.MaxSlotLoss, + // ProofProbability = ask.ProofProbability, + // Reward = Convert.ToDecimal(ask.Reward).TstWei(), + // Slots = ask.Slots, + // SlotSize = new ByteSize(Convert.ToInt64(ask.SlotSize)) + // }; + //} + + //public StorageContent Map(CodexOpenApi.Content content) + //{ + // return new StorageContent + // { + // Cid = content.Cid + // }; + //} public StorageAvailability Map(CodexOpenApi.SalesAvailabilityREAD read) { return new StorageAvailability( - totalSpace: new Utils.ByteSize(Convert.ToInt64(read.TotalSize)), + totalSpace: new ByteSize(Convert.ToInt64(read.TotalSize)), maxDuration: TimeSpan.FromSeconds(Convert.ToDouble(read.Duration)), - minPriceForTotalSpace: new TestToken(Convert.ToDecimal(read.MinPrice)), - maxCollateral: new TestToken(Convert.ToDecimal(read.MaxCollateral)) + minPriceForTotalSpace: new TestToken(BigInteger.Parse(read.MinPrice)), + maxCollateral: new TestToken(BigInteger.Parse(read.MaxCollateral)) ) { Id = read.Id }; } + public CodexSpace Map(Space space) + { + return new CodexSpace + { + QuotaMaxBytes = space.QuotaMaxBytes, + QuotaReservedBytes = space.QuotaReservedBytes, + QuotaUsedBytes = space.QuotaUsedBytes, + TotalBlocks = space.TotalBlocks + }; + } + private DebugInfoVersion MapDebugInfoVersion(JObject obj) { return new DebugInfoVersion @@ -98,7 +143,7 @@ namespace CodexPlugin return new DebugInfoTable { LocalNode = MapDebugInfoTableNode(obj.GetValue("localNode")), - Nodes = new DebugInfoTableNode[0] + Nodes = MapDebugInfoTableNodeArray(obj.GetValue("nodes") as JArray) }; } @@ -117,6 +162,16 @@ namespace CodexPlugin }; } + private DebugInfoTableNode[] MapDebugInfoTableNodeArray(JArray? nodes) + { + if (nodes == null || nodes.Count == 0) + { + return new DebugInfoTableNode[0]; + } + + return nodes.Select(MapDebugInfoTableNode).ToArray(); + } + private Manifest MapManifest(CodexOpenApi.ManifestItem manifest) { return new Manifest @@ -165,8 +220,7 @@ namespace CodexPlugin private string ToDecInt(TestToken t) { - var i = new BigInteger(t.Amount); - return i.ToString("D"); + return t.TstWei.ToString("D"); } } } diff --git a/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs b/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs index c0d7d180..11f3b60e 100644 --- a/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs +++ b/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs @@ -1,5 +1,4 @@ using Logging; -using Newtonsoft.Json; using Utils; namespace CodexPlugin @@ -7,7 +6,7 @@ namespace CodexPlugin public interface IMarketplaceAccess { string MakeStorageAvailable(StorageAvailability availability); - StoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase); + IStoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase); } public class MarketplaceAccess : IMarketplaceAccess @@ -21,7 +20,7 @@ namespace CodexPlugin this.codexAccess = codexAccess; } - public StoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase) + public IStoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase) { purchase.Log(log); @@ -38,7 +37,9 @@ namespace CodexPlugin Log($"Storage requested successfully. PurchaseId: '{response}'."); - return new StoragePurchaseContract(log, codexAccess, response, purchase); + var contract = new StoragePurchaseContract(log, codexAccess, response, purchase); + contract.WaitForStorageContractSubmitted(); + return contract; } public string MakeStorageAvailable(StorageAvailability availability) @@ -54,7 +55,7 @@ namespace CodexPlugin private void Log(string msg) { - log.Log($"{codexAccess.Container.Name} {msg}"); + log.Log($"{codexAccess.Container.Containers.Single().Name} {msg}"); } } @@ -66,7 +67,7 @@ namespace CodexPlugin throw new NotImplementedException(); } - public StoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase) + public IStoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase) { Unavailable(); throw new NotImplementedException(); @@ -78,78 +79,4 @@ namespace CodexPlugin throw new InvalidOperationException(); } } - - public class StoragePurchaseContract - { - private readonly ILog log; - private readonly CodexAccess codexAccess; - private readonly TimeSpan gracePeriod = TimeSpan.FromSeconds(10); - private DateTime? contractStartUtc; - - public StoragePurchaseContract(ILog log, CodexAccess codexAccess, string purchaseId, StoragePurchaseRequest purchase) - { - this.log = log; - this.codexAccess = codexAccess; - PurchaseId = purchaseId; - Purchase = purchase; - } - - public string PurchaseId { get; } - public StoragePurchaseRequest Purchase { get; } - - public void WaitForStorageContractStarted() - { - var timeout = Purchase.Expiry + gracePeriod; - - WaitForStorageContractState(timeout, "started"); - contractStartUtc = DateTime.UtcNow; - } - - public void WaitForStorageContractFinished() - { - if (!contractStartUtc.HasValue) - { - WaitForStorageContractStarted(); - } - var currentContractTime = DateTime.UtcNow - contractStartUtc!.Value; - var timeout = (Purchase.Duration - currentContractTime) + gracePeriod; - WaitForStorageContractState(timeout, "finished"); - } - - public StoragePurchase GetPurchaseStatus(string purchaseId) - { - return codexAccess.GetPurchaseStatus(purchaseId); - } - - private void WaitForStorageContractState(TimeSpan timeout, string desiredState) - { - var lastState = ""; - var waitStart = DateTime.UtcNow; - - log.Log($"Waiting for {Time.FormatDuration(timeout)} for contract '{PurchaseId}' to reach state '{desiredState}'."); - while (lastState != desiredState) - { - var purchaseStatus = codexAccess.GetPurchaseStatus(PurchaseId); - var statusJson = JsonConvert.SerializeObject(purchaseStatus); - if (purchaseStatus != null && purchaseStatus.State != lastState) - { - lastState = purchaseStatus.State; - log.Debug("Purchase status: " + statusJson); - } - - Thread.Sleep(1000); - - if (lastState == "errored") - { - FrameworkAssert.Fail("Contract errored: " + statusJson); - } - - if (DateTime.UtcNow - waitStart > timeout) - { - FrameworkAssert.Fail($"Contract did not reach '{desiredState}' within {Time.FormatDuration(timeout)} timeout. {statusJson}"); - } - } - log.Log($"Contract '{desiredState}'."); - } - } } diff --git a/ProjectPlugins/CodexPlugin/MarketplaceTypes.cs b/ProjectPlugins/CodexPlugin/MarketplaceTypes.cs index 5edc4560..21aa86a6 100644 --- a/ProjectPlugins/CodexPlugin/MarketplaceTypes.cs +++ b/ProjectPlugins/CodexPlugin/MarketplaceTypes.cs @@ -1,5 +1,7 @@ using CodexContractsPlugin; +using CodexOpenApi; using Logging; +using System.Data; using Utils; namespace CodexPlugin @@ -12,8 +14,8 @@ namespace CodexPlugin } public ContentId ContentId { get; set; } - public TestToken PricePerSlotPerSecond { get; set; } = 1.TestTokens(); - public TestToken RequiredCollateral { get; set; } = 1.TestTokens(); + public TestToken PricePerSlotPerSecond { get; set; } = 1.TstWei(); + public TestToken RequiredCollateral { get; set; } = 1.TstWei(); public uint MinRequiredNumberOfNodes { get; set; } public uint NodeFailureTolerance { get; set; } public int ProofProbability { get; set; } @@ -37,6 +39,34 @@ namespace CodexPlugin { public string State { get; set; } = string.Empty; public string Error { get; set; } = string.Empty; + public StorageRequest Request { get; set; } = null!; + } + + public class StorageRequest + { + public string Id { get; set; } = string.Empty; + public string Client { get; set; } = string.Empty; + public StorageAsk Ask { get; set; } = null!; + public StorageContent Content { get; set; } = null!; + public string Expiry { get; set; } = string.Empty; + public string Nonce { get; set; } = string.Empty; + } + + public class StorageAsk + { + public int Slots { get; set; } + public string SlotSize { get; set; } = string.Empty; + public string Duration { get; set; } = string.Empty; + public string ProofProbability { get; set; } = string.Empty; + public string Reward { get; set; } = string.Empty; + public int MaxSlotLoss { get; set; } + } + + public class StorageContent + { + public string Cid { get; set; } = string.Empty; + //public ErasureParameters Erasure { get; set; } + //public PoRParameters Por { get; set; } } public class StorageAvailability diff --git a/ProjectPlugins/CodexPlugin/StoragePurchaseContract.cs b/ProjectPlugins/CodexPlugin/StoragePurchaseContract.cs new file mode 100644 index 00000000..34462310 --- /dev/null +++ b/ProjectPlugins/CodexPlugin/StoragePurchaseContract.cs @@ -0,0 +1,146 @@ +using Logging; +using Newtonsoft.Json; +using Utils; + +namespace CodexPlugin +{ + public interface IStoragePurchaseContract + { + string PurchaseId { get; } + StoragePurchaseRequest Purchase { get; } + ContentId ContentId { get; } + void WaitForStorageContractSubmitted(); + void WaitForStorageContractStarted(); + void WaitForStorageContractFinished(); + } + + public class StoragePurchaseContract : IStoragePurchaseContract + { + private readonly ILog log; + private readonly CodexAccess codexAccess; + private readonly TimeSpan gracePeriod = TimeSpan.FromSeconds(30); + private readonly DateTime contractPendingUtc = DateTime.UtcNow; + private DateTime? contractSubmittedUtc = DateTime.UtcNow; + private DateTime? contractStartedUtc; + private DateTime? contractFinishedUtc; + + public StoragePurchaseContract(ILog log, CodexAccess codexAccess, string purchaseId, StoragePurchaseRequest purchase) + { + this.log = log; + this.codexAccess = codexAccess; + PurchaseId = purchaseId; + Purchase = purchase; + + ContentId = new ContentId(codexAccess.GetPurchaseStatus(purchaseId).Request.Content.Cid); + } + + public string PurchaseId { get; } + public StoragePurchaseRequest Purchase { get; } + public ContentId ContentId { get; } + + public TimeSpan? PendingToSubmitted => contractSubmittedUtc - contractPendingUtc; + public TimeSpan? SubmittedToStarted => contractStartedUtc - contractSubmittedUtc; + public TimeSpan? SubmittedToFinished => contractFinishedUtc - contractSubmittedUtc; + + public void WaitForStorageContractSubmitted() + { + WaitForStorageContractState(gracePeriod, "submitted", sleep: 200); + contractSubmittedUtc = DateTime.UtcNow; + LogSubmittedDuration(); + AssertDuration(PendingToSubmitted, gracePeriod, nameof(PendingToSubmitted)); + } + + public void WaitForStorageContractStarted() + { + var timeout = Purchase.Expiry + gracePeriod; + + WaitForStorageContractState(timeout, "started"); + contractStartedUtc = DateTime.UtcNow; + LogStartedDuration(); + AssertDuration(SubmittedToStarted, timeout, nameof(SubmittedToStarted)); + } + + public void WaitForStorageContractFinished() + { + if (!contractStartedUtc.HasValue) + { + WaitForStorageContractStarted(); + } + var currentContractTime = DateTime.UtcNow - contractSubmittedUtc!.Value; + var timeout = (Purchase.Duration - currentContractTime) + gracePeriod; + WaitForStorageContractState(timeout, "finished"); + contractFinishedUtc = DateTime.UtcNow; + LogFinishedDuration(); + AssertDuration(SubmittedToFinished, timeout, nameof(SubmittedToFinished)); + } + + public StoragePurchase GetPurchaseStatus(string purchaseId) + { + return codexAccess.GetPurchaseStatus(purchaseId); + } + + private void WaitForStorageContractState(TimeSpan timeout, string desiredState, int sleep = 1000) + { + var lastState = ""; + var waitStart = DateTime.UtcNow; + + Log($"Waiting for {Time.FormatDuration(timeout)} to reach state '{desiredState}'."); + while (lastState != desiredState) + { + var purchaseStatus = codexAccess.GetPurchaseStatus(PurchaseId); + var statusJson = JsonConvert.SerializeObject(purchaseStatus); + if (purchaseStatus != null && purchaseStatus.State != lastState) + { + lastState = purchaseStatus.State; + log.Debug("Purchase status: " + statusJson); + } + + Thread.Sleep(sleep); + + if (lastState == "errored") + { + FrameworkAssert.Fail("Contract errored: " + statusJson); + } + + if (DateTime.UtcNow - waitStart > timeout) + { + FrameworkAssert.Fail($"Contract did not reach '{desiredState}' within {Time.FormatDuration(timeout)} timeout. {statusJson}"); + } + } + } + + private void LogSubmittedDuration() + { + Log($"Pending to Submitted in {Time.FormatDuration(PendingToSubmitted)} " + + $"( < {Time.FormatDuration(gracePeriod)})"); + } + + private void LogStartedDuration() + { + Log($"Submitted to Started in {Time.FormatDuration(SubmittedToStarted)} " + + $"( < {Time.FormatDuration(Purchase.Expiry + gracePeriod)})"); + } + + private void LogFinishedDuration() + { + Log($"Submitted to Finished in {Time.FormatDuration(SubmittedToFinished)} " + + $"( < {Time.FormatDuration(Purchase.Duration + gracePeriod)})"); + } + + private void AssertDuration(TimeSpan? span, TimeSpan max, string message) + { + if (span == null) throw new ArgumentNullException(nameof(MarketplaceAccess) + ": " + message + " (IsNull)"); + if (span.Value.TotalDays >= max.TotalSeconds) + { + throw new Exception(nameof(MarketplaceAccess) + + $": Duration out of range. Max: {Time.FormatDuration(max)} but was: {Time.FormatDuration(span.Value)} " + + message); + } + } + + private void Log(string msg) + { + log.Log($"[{PurchaseId}] {msg}"); + } + } +} diff --git a/ProjectPlugins/CodexPlugin/openapi.yaml b/ProjectPlugins/CodexPlugin/openapi.yaml index 6099ed55..94450bf3 100644 --- a/ProjectPlugins/CodexPlugin/openapi.yaml +++ b/ProjectPlugins/CodexPlugin/openapi.yaml @@ -65,6 +65,22 @@ components: description: A timestamp as seconds since unix epoch at which this request expires if the Request does not find requested amount of nodes to host the data. default: 10 minutes + SPR: + type: string + description: Signed Peer Record (libp2p) + + SPRRead: + type: object + properties: + spr: + $ref: "#/components/schemas/SPR" + + PeerIdRead: + type: object + properties: + id: + $ref: "#/components/schemas/PeerId" + ErasureParameters: type: object properties: @@ -106,8 +122,7 @@ components: type: string description: Path of the data repository where all nodes data are stored spr: - type: string - description: Signed Peer Record to advertise DHT connection information + $ref: "#/components/schemas/SPR" SalesAvailability: type: object @@ -198,8 +213,7 @@ components: description: Number as decimal string that represents how much collateral is asked from hosts that wants to fill a slots expiry: type: string - description: Number as decimal string that represents expiry time of the request (in unix timestamp) - + description: Number as decimal string that represents expiry threshold in seconds from when the Request is submitted. When the threshold is reached and the Request does not find requested amount of nodes to host the data, the Request is voided. The number of seconds can not be higher then the Request's duration itself. StorageAsk: type: object required: @@ -275,6 +289,7 @@ components: description: "Root hash of the content" originalBytes: type: integer + format: int64 description: "Length of original content in bytes" blockSize: type: integer @@ -289,14 +304,18 @@ components: totalBlocks: description: "Number of blocks stored by the node" type: integer + format: int64 quotaMaxBytes: type: integer + format: int64 description: "Maximum storage space used by the node" quotaUsedBytes: type: integer + format: int64 description: "Amount of storage space currently in use" quotaReservedBytes: type: integer + format: int64 description: "Amount of storage space reserved" servers: @@ -685,6 +704,40 @@ paths: "503": description: Purchasing is unavailable + "/node/spr": + get: + summary: "Get Node's SPR" + operationId: getSPR + tags: [ Node ] + responses: + "200": + description: Node's SPR + content: + plain/text: + schema: + $ref: "#/components/schemas/SPR" + application/json: + schema: + $ref: "#/components/schemas/SPRRead" + "503": + description: Node SPR not ready, try again later + + "/node/peerid": + get: + summary: "Get Node's PeerID" + operationId: getPeerId + tags: [ Node ] + responses: + "200": + description: Node's Peer ID + content: + plain/text: + schema: + $ref: "#/components/schemas/PeerId" + application/json: + schema: + $ref: "#/components/schemas/PeerIdRead" + "/debug/chronicles/loglevel": post: summary: "Set log level at run time" diff --git a/ProjectPlugins/DeployAndRunPlugin/DeployAndRunPlugin.cs b/ProjectPlugins/DeployAndRunPlugin/DeployAndRunPlugin.cs index 070b9fea..c290c0d5 100644 --- a/ProjectPlugins/DeployAndRunPlugin/DeployAndRunPlugin.cs +++ b/ProjectPlugins/DeployAndRunPlugin/DeployAndRunPlugin.cs @@ -30,7 +30,7 @@ namespace DeployAndRunPlugin startupConfig.Add(config); var location = workflow.GetAvailableLocations().Get("fixed-s-4vcpu-16gb-amd-yz8rd"); - var containers = workflow.Start(1, location, new DeployAndRunContainerRecipe(), startupConfig); + var containers = workflow.Start(1, location, new DeployAndRunContainerRecipe(), startupConfig).WaitForOnline(); return containers.Containers.Single(); } } diff --git a/ProjectPlugins/GethPlugin/EthAccount.cs b/ProjectPlugins/GethPlugin/EthAccount.cs index 9f1318b7..60bf40d8 100644 --- a/ProjectPlugins/GethPlugin/EthAccount.cs +++ b/ProjectPlugins/GethPlugin/EthAccount.cs @@ -24,5 +24,10 @@ namespace GethPlugin return new EthAccount(ethAddress, account.PrivateKey); } + + public override string ToString() + { + return EthAddress.ToString(); + } } } diff --git a/ProjectPlugins/GethPlugin/GethDeployment.cs b/ProjectPlugins/GethPlugin/GethDeployment.cs index 8d7f0dd5..e463ce42 100644 --- a/ProjectPlugins/GethPlugin/GethDeployment.cs +++ b/ProjectPlugins/GethPlugin/GethDeployment.cs @@ -7,9 +7,9 @@ namespace GethPlugin { public class GethDeployment : IHasContainer { - public GethDeployment(RunningContainers containers, Port discoveryPort, Port httpPort, Port wsPort, GethAccount account, string pubKey) + public GethDeployment(RunningPod pod, Port discoveryPort, Port httpPort, Port wsPort, GethAccount account, string pubKey) { - Containers = containers; + Pod = pod; DiscoveryPort = discoveryPort; HttpPort = httpPort; WsPort = wsPort; @@ -17,9 +17,9 @@ namespace GethPlugin PubKey = pubKey; } - public RunningContainers Containers { get; } + public RunningPod Pod { get; } [JsonIgnore] - public RunningContainer Container { get { return Containers.Containers.Single(); } } + public RunningContainer Container { get { return Pod.Containers.Single(); } } public Port DiscoveryPort { get; } public Port HttpPort { get; } public Port WsPort { get; } diff --git a/ProjectPlugins/GethPlugin/GethNode.cs b/ProjectPlugins/GethPlugin/GethNode.cs index bfa3d0f9..08d07366 100644 --- a/ProjectPlugins/GethPlugin/GethNode.cs +++ b/ProjectPlugins/GethPlugin/GethNode.cs @@ -5,6 +5,7 @@ using Nethereum.ABI.FunctionEncoding.Attributes; using Nethereum.Contracts; using Nethereum.RPC.Eth.DTOs; using NethereumWorkflow; +using NethereumWorkflow.BlockUtils; using Utils; namespace GethPlugin @@ -27,6 +28,7 @@ namespace GethPlugin List> GetEvents(string address, BlockInterval blockRange) where TEvent : IEventDTO, new(); List> GetEvents(string address, TimeRange timeRange) where TEvent : IEventDTO, new(); BlockInterval ConvertTimeRangeToBlockRange(TimeRange timeRange); + BlockTimeEntry GetBlockForNumber(ulong number); } public class DeploymentGethNode : BaseGethNode, IGethNode @@ -160,6 +162,11 @@ namespace GethPlugin return StartInteraction().ConvertTimeRangeToBlockRange(timeRange); } + public BlockTimeEntry GetBlockForNumber(ulong number) + { + return StartInteraction().GetBlockForNumber(number); + } + protected abstract NethereumInteraction StartInteraction(); } } diff --git a/ProjectPlugins/GethPlugin/GethStarter.cs b/ProjectPlugins/GethPlugin/GethStarter.cs index c5ad801f..edfba4e8 100644 --- a/ProjectPlugins/GethPlugin/GethStarter.cs +++ b/ProjectPlugins/GethPlugin/GethStarter.cs @@ -21,7 +21,7 @@ namespace GethPlugin startupConfig.NameOverride = gethStartupConfig.NameOverride; var workflow = tools.CreateWorkflow(); - var containers = workflow.Start(1, new GethContainerRecipe(), startupConfig); + var containers = workflow.Start(1, new GethContainerRecipe(), startupConfig).WaitForOnline(); if (containers.Containers.Length != 1) throw new InvalidOperationException("Expected 1 Geth bootstrap node to be created. Test infra failure."); var container = containers.Containers[0]; diff --git a/ProjectPlugins/MetricsPlugin/CoreInterfaceExtensions.cs b/ProjectPlugins/MetricsPlugin/CoreInterfaceExtensions.cs index 83920825..6d5859da 100644 --- a/ProjectPlugins/MetricsPlugin/CoreInterfaceExtensions.cs +++ b/ProjectPlugins/MetricsPlugin/CoreInterfaceExtensions.cs @@ -6,24 +6,24 @@ namespace MetricsPlugin { public static class CoreInterfaceExtensions { - public static RunningContainers DeployMetricsCollector(this CoreInterface ci, params IHasMetricsScrapeTarget[] scrapeTargets) + public static RunningPod DeployMetricsCollector(this CoreInterface ci, params IHasMetricsScrapeTarget[] scrapeTargets) { return Plugin(ci).DeployMetricsCollector(scrapeTargets.Select(t => t.MetricsScrapeTarget).ToArray()); } - public static RunningContainers DeployMetricsCollector(this CoreInterface ci, params IMetricsScrapeTarget[] scrapeTargets) + public static RunningPod DeployMetricsCollector(this CoreInterface ci, params IMetricsScrapeTarget[] scrapeTargets) { return Plugin(ci).DeployMetricsCollector(scrapeTargets); } - public static IMetricsAccess WrapMetricsCollector(this CoreInterface ci, RunningContainers metricsContainer, IHasMetricsScrapeTarget scrapeTarget) + public static IMetricsAccess WrapMetricsCollector(this CoreInterface ci, RunningPod metricsPod, IHasMetricsScrapeTarget scrapeTarget) { - return ci.WrapMetricsCollector(metricsContainer, scrapeTarget.MetricsScrapeTarget); + return ci.WrapMetricsCollector(metricsPod, scrapeTarget.MetricsScrapeTarget); } - public static IMetricsAccess WrapMetricsCollector(this CoreInterface ci, RunningContainers metricsContainer, IMetricsScrapeTarget scrapeTarget) + public static IMetricsAccess WrapMetricsCollector(this CoreInterface ci, RunningPod metricsPod, IMetricsScrapeTarget scrapeTarget) { - return Plugin(ci).WrapMetricsCollectorDeployment(metricsContainer, scrapeTarget); + return Plugin(ci).WrapMetricsCollectorDeployment(metricsPod, scrapeTarget); } public static IMetricsAccess[] GetMetricsFor(this CoreInterface ci, params IHasManyMetricScrapeTargets[] manyScrapeTargets) diff --git a/ProjectPlugins/MetricsPlugin/MetricsPlugin.cs b/ProjectPlugins/MetricsPlugin/MetricsPlugin.cs index b2c2b8ea..bc7d926b 100644 --- a/ProjectPlugins/MetricsPlugin/MetricsPlugin.cs +++ b/ProjectPlugins/MetricsPlugin/MetricsPlugin.cs @@ -31,15 +31,15 @@ namespace MetricsPlugin { } - public RunningContainers DeployMetricsCollector(IMetricsScrapeTarget[] scrapeTargets) + public RunningPod DeployMetricsCollector(IMetricsScrapeTarget[] scrapeTargets) { return starter.CollectMetricsFor(scrapeTargets); } - public IMetricsAccess WrapMetricsCollectorDeployment(RunningContainers runningContainer, IMetricsScrapeTarget target) + public IMetricsAccess WrapMetricsCollectorDeployment(RunningPod runningPod, IMetricsScrapeTarget target) { - runningContainer = SerializeGate.Gate(runningContainer); - return starter.CreateAccessForTarget(runningContainer, target); + runningPod = SerializeGate.Gate(runningPod); + return starter.CreateAccessForTarget(runningPod, target); } public LogFile? DownloadAllMetrics(IMetricsAccess metricsAccess, string targetName) diff --git a/ProjectPlugins/MetricsPlugin/MetricsQuery.cs b/ProjectPlugins/MetricsPlugin/MetricsQuery.cs index 199015bc..cdab6971 100644 --- a/ProjectPlugins/MetricsPlugin/MetricsQuery.cs +++ b/ProjectPlugins/MetricsPlugin/MetricsQuery.cs @@ -1,4 +1,5 @@ using Core; +using IdentityModel; using KubernetesWorkflow.Types; using Logging; using System.Globalization; @@ -177,6 +178,41 @@ namespace MetricsPlugin { return "[" + string.Join(',', Sets.Select(s => s.ToString())) + "]"; } + + public string AsCsv() + { + var allTimestamps = Sets.SelectMany(s => s.Values.Select(v => v.Timestamp)).Distinct().OrderDescending().ToArray(); + + var lines = new List(); + MakeLine(lines, e => + { + e.Add("Metrics"); + foreach (var ts in allTimestamps) e.Add(ts.ToEpochTime().ToString()); + }); + + foreach (var set in Sets) + { + MakeLine(lines, e => + { + e.Add(set.Name); + foreach (var ts in allTimestamps) + { + var value = set.Values.SingleOrDefault(v => v.Timestamp == ts); + if (value == null) e.Add(" "); + else e.Add(value.Value.ToString()); + } + }); + } + + return string.Join(Environment.NewLine, lines.ToArray()); + } + + private void MakeLine(List lines, Action> values) + { + var list = new List(); + values(list); + lines.Add(string.Join(",", list)); + } } public class MetricsSet diff --git a/ProjectPlugins/MetricsPlugin/PrometheusStarter.cs b/ProjectPlugins/MetricsPlugin/PrometheusStarter.cs index 53f784e4..159947a2 100644 --- a/ProjectPlugins/MetricsPlugin/PrometheusStarter.cs +++ b/ProjectPlugins/MetricsPlugin/PrometheusStarter.cs @@ -16,7 +16,7 @@ namespace MetricsPlugin this.tools = tools; } - public RunningContainers CollectMetricsFor(IMetricsScrapeTarget[] targets) + public RunningPod CollectMetricsFor(IMetricsScrapeTarget[] targets) { if (!targets.Any()) throw new ArgumentException(nameof(targets) + " must not be empty."); @@ -25,16 +25,16 @@ namespace MetricsPlugin startupConfig.Add(new PrometheusStartupConfig(GeneratePrometheusConfig(targets))); var workflow = tools.CreateWorkflow(); - var runningContainers = workflow.Start(1, recipe, startupConfig); + var runningContainers = workflow.Start(1, recipe, startupConfig).WaitForOnline(); if (runningContainers.Containers.Length != 1) throw new InvalidOperationException("Expected only 1 Prometheus container to be created."); Log("Metrics server started."); return runningContainers; } - public MetricsAccess CreateAccessForTarget(RunningContainers metricsContainer, IMetricsScrapeTarget target) + public MetricsAccess CreateAccessForTarget(RunningPod metricsPod, IMetricsScrapeTarget target) { - var metricsQuery = new MetricsQuery(tools, metricsContainer.Containers.Single()); + var metricsQuery = new MetricsQuery(tools, metricsPod.Containers.Single()); return new MetricsAccess(metricsQuery, target); } diff --git a/Tests/CodexContinuousTests/ContinuousTestRunner.cs b/Tests/CodexContinuousTests/ContinuousTestRunner.cs index b860c86f..fde65aa3 100644 --- a/Tests/CodexContinuousTests/ContinuousTestRunner.cs +++ b/Tests/CodexContinuousTests/ContinuousTestRunner.cs @@ -148,7 +148,7 @@ namespace ContinuousTests log.Log($"Clearing namespace '{test.CustomK8sNamespace}'..."); var entryPoint = entryPointFactory.CreateEntryPoint(config.KubeConfigFile, config.DataPath, test.CustomK8sNamespace, log); - entryPoint.Tools.CreateWorkflow().DeleteNamespacesStartingWith(test.CustomK8sNamespace); + entryPoint.Tools.CreateWorkflow().DeleteNamespacesStartingWith(test.CustomK8sNamespace, wait: true); } private void PerformCleanup(ILog log) @@ -157,7 +157,7 @@ namespace ContinuousTests log.Log("Cleaning up test namespace..."); var entryPoint = entryPointFactory.CreateEntryPoint(config.KubeConfigFile, config.DataPath, config.CodexDeployment.Metadata.KubeNamespace, log); - entryPoint.Decommission(deleteKubernetesResources: true, deleteTrackedFiles: true); + entryPoint.Decommission(deleteKubernetesResources: true, deleteTrackedFiles: true, waitTillDone: true); log.Log("Cleanup finished."); } } diff --git a/Tests/CodexContinuousTests/ElasticSearchLogDownloader.cs b/Tests/CodexContinuousTests/ElasticSearchLogDownloader.cs index 80203af3..10c909ca 100644 --- a/Tests/CodexContinuousTests/ElasticSearchLogDownloader.cs +++ b/Tests/CodexContinuousTests/ElasticSearchLogDownloader.cs @@ -49,8 +49,8 @@ namespace ContinuousTests var start = startUtc.ToString("o"); var end = endUtc.ToString("o"); - var containerName = container.RunningContainers.StartResult.Deployment.Name; - var namespaceName = container.RunningContainers.StartResult.Cluster.Configuration.KubernetesNamespace; + var containerName = container.RunningPod.StartResult.Deployment.Name; + var namespaceName = container.RunningPod.StartResult.Cluster.Configuration.KubernetesNamespace; //container_name : codex3-5 - deploymentName as stored in pod // pod_namespace : codex - continuous - nolimits - tests - 1 diff --git a/Tests/CodexContinuousTests/NodeRunner.cs b/Tests/CodexContinuousTests/NodeRunner.cs index 31f1f2eb..e58facca 100644 --- a/Tests/CodexContinuousTests/NodeRunner.cs +++ b/Tests/CodexContinuousTests/NodeRunner.cs @@ -64,7 +64,7 @@ namespace ContinuousTests } finally { - entryPoint.Tools.CreateWorkflow().DeleteNamespace(); + entryPoint.Tools.CreateWorkflow().DeleteNamespace(wait: false); } } diff --git a/Tests/CodexContinuousTests/SingleTestRun.cs b/Tests/CodexContinuousTests/SingleTestRun.cs index 7c6c5a6f..95d04660 100644 --- a/Tests/CodexContinuousTests/SingleTestRun.cs +++ b/Tests/CodexContinuousTests/SingleTestRun.cs @@ -54,7 +54,8 @@ namespace ContinuousTests entryPoint.Decommission( deleteKubernetesResources: false, // This would delete the continuous test net. - deleteTrackedFiles: true + deleteTrackedFiles: true, + waitTillDone: false ); runFinishedHandle.Set(); } @@ -125,8 +126,8 @@ namespace ContinuousTests foreach (var node in nodes) { var container = node.Container; - var deploymentName = container.RunningContainers.StartResult.Deployment.Name; - var namespaceName = container.RunningContainers.StartResult.Cluster.Configuration.KubernetesNamespace; + var deploymentName = container.RunningPod.StartResult.Deployment.Name; + var namespaceName = container.RunningPod.StartResult.Cluster.Configuration.KubernetesNamespace; var openingLine = $"{namespaceName} - {deploymentName} = {node.Container.Name} = {node.GetDebugInfo().Id}"; elasticSearchLogDownloader.Download(fixtureLog.CreateSubfile(), node.Container, effectiveStart, @@ -295,13 +296,13 @@ namespace ContinuousTests return entryPoint.CreateInterface().WrapCodexContainers(containers).ToArray(); } - private RunningContainers[] SelectRandomContainers() + private RunningPod[] SelectRandomContainers() { var number = handle.Test.RequiredNumberOfNodes; - var containers = config.CodexDeployment.CodexInstances.Select(i => i.Containers).ToList(); + var containers = config.CodexDeployment.CodexInstances.Select(i => i.Pod).ToList(); if (number == -1) return containers.ToArray(); - var result = new RunningContainers[number]; + var result = new RunningPod[number]; for (var i = 0; i < number; i++) { result[i] = containers.PickOneRandom(); diff --git a/Tests/CodexContinuousTests/StartupChecker.cs b/Tests/CodexContinuousTests/StartupChecker.cs index d3fb456c..57c383c8 100644 --- a/Tests/CodexContinuousTests/StartupChecker.cs +++ b/Tests/CodexContinuousTests/StartupChecker.cs @@ -43,13 +43,13 @@ namespace ContinuousTests var workflow = entryPoint.Tools.CreateWorkflow(); foreach (var instance in deployment.CodexInstances) { - foreach (var container in instance.Containers.Containers) + foreach (var container in instance.Pod.Containers) { var podInfo = workflow.GetPodInfo(container); log.Log($"Codex environment variables for '{container.Name}':"); log.Log( - $"Namespace: {container.RunningContainers.StartResult.Cluster.Configuration.KubernetesNamespace} - " + - $"Pod name: {podInfo.Name} - Deployment name: {instance.Containers.StartResult.Deployment.Name}"); + $"Namespace: {container.RunningPod.StartResult.Cluster.Configuration.KubernetesNamespace} - " + + $"Pod name: {podInfo.Name} - Deployment name: {instance.Pod.StartResult.Deployment.Name}"); var codexVars = container.Recipe.EnvVars; foreach (var vars in codexVars) log.Log(vars.ToString()); log.Log(""); @@ -92,7 +92,7 @@ namespace ContinuousTests private void CheckCodexNodes(BaseLog log, Configuration config) { var nodes = entryPoint.CreateInterface() - .WrapCodexContainers(config.CodexDeployment.CodexInstances.Select(i => i.Containers).ToArray()); + .WrapCodexContainers(config.CodexDeployment.CodexInstances.Select(i => i.Pod).ToArray()); var pass = true; foreach (var n in nodes) { diff --git a/Tests/CodexLongTests/BasicTests/DownloadTests.cs b/Tests/CodexLongTests/BasicTests/DownloadTests.cs index 533934df..0a95a643 100644 --- a/Tests/CodexLongTests/BasicTests/DownloadTests.cs +++ b/Tests/CodexLongTests/BasicTests/DownloadTests.cs @@ -1,3 +1,4 @@ +using CodexPlugin; using CodexTests; using DistTestCore; using FileUtils; @@ -7,35 +8,45 @@ using Utils; namespace CodexLongTests.BasicTests { [TestFixture] - public class DownloadTests : CodexDistTest + public class DownloadTests : AutoBootstrapDistTest { - [TestCase(3, 500)] - [TestCase(5, 100)] - [TestCase(10, 256)] + [Test] + [Combinatorial] [UseLongTimeouts] - public void ParallelDownload(int numberOfNodes, int filesizeMb) + public void ParallelDownload( + [Values(1, 3, 5)] int numberOfFiles, + [Values(10, 50, 100)] int filesizeMb) { - var group = AddCodex(numberOfNodes); - var host = AddCodex(); + var host = StartCodex(); + var client = StartCodex(); - foreach (var node in group) + var testfiles = new List(); + var contentIds = new List(); + var downloadedFiles = new List(); + + for (int i = 0; i < numberOfFiles; i++) { - host.ConnectToPeer(node); + testfiles.Add(GenerateTestFile(filesizeMb.MB())); + contentIds.Add(new ContentId()); + downloadedFiles.Add(null); } - var testFile = GenerateTestFile(filesizeMb.MB()); - var contentId = host.UploadFile(testFile); - var list = new List>(); - - foreach (var node in group) + for (int i = 0; i < numberOfFiles; i++) { - list.Add(Task.Run(() => { return node.DownloadContent(contentId); })); + contentIds[i] = host.UploadFile(testfiles[i]); } - Task.WaitAll(list.ToArray()); - foreach (var task in list) + var downloadTasks = new List(); + for (int i = 0; i < numberOfFiles; i++) { - testFile.AssertIsEqual(task.Result); + downloadTasks.Add(Task.Run(() => { downloadedFiles[i] = client.DownloadContent(contentIds[i]); })); + } + + Task.WaitAll(downloadTasks.ToArray()); + + for (int i = 0; i < numberOfFiles; i++) + { + testfiles[i].AssertIsEqual(downloadedFiles[i]); } } } diff --git a/Tests/CodexLongTests/BasicTests/LargeFileTests.cs b/Tests/CodexLongTests/BasicTests/LargeFileTests.cs index 1a6e21cc..0470e35a 100644 --- a/Tests/CodexLongTests/BasicTests/LargeFileTests.cs +++ b/Tests/CodexLongTests/BasicTests/LargeFileTests.cs @@ -48,7 +48,7 @@ namespace CodexLongTests.BasicTests var expectedFile = GenerateTestFile(sizeMB); - var node = AddCodex(s => s.WithStorageQuota((size + 10).MB())); + var node = StartCodex(s => s.WithStorageQuota((size + 10).MB())); var uploadStart = DateTime.UtcNow; var cid = node.UploadFile(expectedFile); diff --git a/Tests/CodexLongTests/BasicTests/TestInfraTests.cs b/Tests/CodexLongTests/BasicTests/TestInfraTests.cs index 5720457b..7dcf02b0 100644 --- a/Tests/CodexLongTests/BasicTests/TestInfraTests.cs +++ b/Tests/CodexLongTests/BasicTests/TestInfraTests.cs @@ -6,10 +6,12 @@ namespace CodexLongTests.BasicTests { public class TestInfraTests : CodexDistTest { - [Test, UseLongTimeouts] + [Test] + [UseLongTimeouts] + [Ignore("Not supported atm")] public void TestInfraShouldHave1000AddressSpacesPerPod() { - var group = AddCodex(1000, s => s.EnableMetrics()); + var group = StartCodex(1000, s => s.EnableMetrics()); var nodeIds = group.Select(n => n.GetDebugInfo().Id).ToArray(); @@ -17,12 +19,14 @@ namespace CodexLongTests.BasicTests "Not all created nodes provided a unique id."); } - [Test, UseLongTimeouts] + [Test] + [UseLongTimeouts] + [Ignore("Not supported atm")] public void TestInfraSupportsManyConcurrentPods() { for (var i = 0; i < 20; i++) { - var n = AddCodex(); + var n = StartCodex(); Assert.That(!string.IsNullOrEmpty(n.GetDebugInfo().Id)); } diff --git a/Tests/CodexLongTests/BasicTests/UploadTests.cs b/Tests/CodexLongTests/BasicTests/UploadTests.cs index 57e02965..361c3a73 100644 --- a/Tests/CodexLongTests/BasicTests/UploadTests.cs +++ b/Tests/CodexLongTests/BasicTests/UploadTests.cs @@ -8,41 +8,39 @@ using Utils; namespace CodexLongTests.BasicTests { [TestFixture] - public class UploadTests : CodexDistTest + public class UploadTests : AutoBootstrapDistTest { - [TestCase(3, 50)] - [TestCase(5, 75)] - [TestCase(10, 25)] + [Test] + [Combinatorial] [UseLongTimeouts] - public void ParallelUpload(int numberOfNodes, int filesizeMb) + public void ParallelUpload( + [Values(1, 3, 5)] int numberOfFiles, + [Values(10, 50, 100)] int filesizeMb) { - var group = AddCodex(numberOfNodes); - var host = AddCodex(); - - foreach (var node in group) - { - host.ConnectToPeer(node); - } + var host = StartCodex(); + var client = StartCodex(); var testfiles = new List(); - var contentIds = new List>(); + var contentIds = new List(); - for (int i = 0; i < group.Count(); i++) + for (int i = 0; i < numberOfFiles; i++) { testfiles.Add(GenerateTestFile(filesizeMb.MB())); - var n = i; - contentIds.Add(Task.Run(() => { return host.UploadFile(testfiles[n]); })); + contentIds.Add(new ContentId()); } - var downloads = new List>(); - for (int i = 0; i < group.Count(); i++) + + var uploadTasks = new List(); + for (int i = 0; i < numberOfFiles; i++) { - var n = i; - downloads.Add(Task.Run(() => { return group[n].DownloadContent(contentIds[n].Result); })); + uploadTasks.Add(Task.Run(() => { contentIds[i] = host.UploadFile(testfiles[i]); })); } - Task.WaitAll(downloads.ToArray()); - for (int i = 0; i < group.Count(); i++) + + Task.WaitAll(uploadTasks.ToArray()); + + for (int i = 0; i < numberOfFiles; i++) { - testfiles[i].AssertIsEqual(downloads[i].Result); + var downloaded = client.DownloadContent(contentIds[i]); + testfiles[i].AssertIsEqual(downloaded); } } } diff --git a/Tests/CodexLongTests/DownloadConnectivityTests/LongFullyConnectedDownloadTests.cs b/Tests/CodexLongTests/DownloadConnectivityTests/LongFullyConnectedDownloadTests.cs index 8f57e38a..eaa999eb 100644 --- a/Tests/CodexLongTests/DownloadConnectivityTests/LongFullyConnectedDownloadTests.cs +++ b/Tests/CodexLongTests/DownloadConnectivityTests/LongFullyConnectedDownloadTests.cs @@ -15,9 +15,9 @@ namespace CodexLongTests.DownloadConnectivityTests [Values(10, 15, 20)] int numberOfNodes, [Values(10, 100)] int sizeMBs) { - for (var i = 0; i < numberOfNodes; i++) AddCodex(); + var nodes = StartCodex(numberOfNodes); - CreatePeerDownloadTestHelpers().AssertFullDownloadInterconnectivity(GetAllOnlineCodexNodes(), sizeMBs.MB()); + CreatePeerDownloadTestHelpers().AssertFullDownloadInterconnectivity(nodes, sizeMBs.MB()); } } } diff --git a/Tests/CodexLongTests/ScalabilityTests/MultiPeerDownloadTests.cs b/Tests/CodexLongTests/ScalabilityTests/MultiPeerDownloadTests.cs new file mode 100644 index 00000000..b1640b6d --- /dev/null +++ b/Tests/CodexLongTests/ScalabilityTests/MultiPeerDownloadTests.cs @@ -0,0 +1,120 @@ +using DistTestCore; +using NUnit.Framework; +using Utils; + +namespace CodexTests.ScalabilityTests +{ + [TestFixture] + public class MultiPeerDownloadTests : AutoBootstrapDistTest + { + [Test] + [DontDownloadLogs] + [UseLongTimeouts] + [Combinatorial] + public void MultiPeerDownload( + [Values(5, 10, 20)] int numberOfHosts, + [Values(100, 1000)] int fileSize + ) + { + var hosts = StartCodex(numberOfHosts, s => s.WithLogLevel(CodexPlugin.CodexLogLevel.Trace)); + var file = GenerateTestFile(fileSize.MB()); + var cid = hosts[0].UploadFile(file); + var tailOfManifestCid = cid.Id.Substring(cid.Id.Length - 6); + + var uploadLog = Ci.DownloadLog(hosts[0]); + var expectedNumberOfBlocks = RoundUp(fileSize.MB().SizeInBytes, 64.KB().SizeInBytes) + 1; // +1 for manifest block. + var blockCids = uploadLog + .FindLinesThatContain("Putting block into network store") + .Select(s => + { + var start = s.IndexOf("cid=") + 4; + var end = s.IndexOf(" count="); + var len = end - start; + return s.Substring(start, len); + }) + .ToArray(); + + Assert.That(blockCids.Length, Is.EqualTo(expectedNumberOfBlocks)); + + foreach (var h in hosts) h.DownloadContent(cid); + + var client = StartCodex(s => s.WithLogLevel(CodexPlugin.CodexLogLevel.Trace)); + var resultFile = client.DownloadContent(cid); + resultFile!.AssertIsEqual(file); + + var downloadLog = Ci.DownloadLog(client); + var host = string.Empty; + var blockCidHostMap = new Dictionary(); + downloadLog.IterateLines(line => + { + if (line.Contains("peer=") && line.Contains(" len=")) + { + var start = line.IndexOf("peer=") + 5; + var end = line.IndexOf(" len="); + var len = end - start; + host = line.Substring(start, len); + } + else if (!string.IsNullOrEmpty(host) && line.Contains("Storing block with key")) + { + var start = line.IndexOf("cid=") + 4; + var end = line.IndexOf(" count="); + var len = end - start; + var blockCid = line.Substring(start, len); + + blockCidHostMap.Add(blockCid, host); + host = string.Empty; + } + }); + + var totalFetched = blockCidHostMap.Count(p => !string.IsNullOrEmpty(p.Value)); + //PrintFullMap(blockCidHostMap); + PrintOverview(blockCidHostMap); + + Log("Expected number of blocks: " + expectedNumberOfBlocks); + Log("Total number of block CIDs found in dataset + manifest block: " + blockCids.Length); + Log("Total blocks fetched by hosts: " + totalFetched); + Assert.That(totalFetched, Is.EqualTo(expectedNumberOfBlocks)); + } + + private void PrintOverview(Dictionary blockCidHostMap) + { + var overview = new Dictionary(); + foreach (var pair in blockCidHostMap) + { + if (!overview.ContainsKey(pair.Value)) overview.Add(pair.Value, 1); + else overview[pair.Value]++; + } + + Log("Blocks fetched per host:"); + foreach (var pair in overview) + { + Log($"Host: {pair.Key} = {pair.Value}"); + } + } + + private void PrintFullMap(Dictionary blockCidHostMap) + { + Log("Per block, host it was fetched from:"); + foreach (var pair in blockCidHostMap) + { + if (string.IsNullOrEmpty(pair.Value)) + { + Log($"block: {pair.Key} = Not seen"); + } + else + { + Log($"block: {pair.Key} = '{pair.Value}'"); + } + } + } + + private long RoundUp(long filesize, long blockSize) + { + double f = filesize; + double b = blockSize; + + var result = Math.Ceiling(f / b); + return Convert.ToInt64(result); + } + } +} diff --git a/Tests/CodexLongTests/ScalabilityTests/ScalabilityTests.cs b/Tests/CodexLongTests/ScalabilityTests/ScalabilityTests.cs new file mode 100644 index 00000000..c3b05f7a --- /dev/null +++ b/Tests/CodexLongTests/ScalabilityTests/ScalabilityTests.cs @@ -0,0 +1,132 @@ +using CodexPlugin; +using DistTestCore; +using FileUtils; +using NUnit.Framework; +using Utils; + +namespace CodexTests.ScalabilityTests; + +[TestFixture] +public class ScalabilityTests : CodexDistTest +{ + /// + /// We upload a file to node A, then download it with B. + /// Then we stop node A, and download again with node C. + /// + [Test] + [Combinatorial] + [UseLongTimeouts] + [DontDownloadLogs] + [WaitForCleanup] + public void ShouldMaintainFileInNetwork( + [Values(4, 5, 6)] int numberOfNodes, // TODO: include 10, 40, 80 and 100, not 5 + [Values(4000, 5000, 6000, 7000, 8000, 9000, 10000)] int fileSizeInMb + ) + { + var logLevel = CodexLogLevel.Trace; + + var bootstrap = StartCodex(s => s.WithLogLevel(logLevel)); + var nodes = StartCodex(numberOfNodes - 1, s => s + .WithBootstrapNode(bootstrap) + .WithLogLevel(logLevel) + .WithStorageQuota((fileSizeInMb + 50).MB()) + ).ToList(); + + var uploader = nodes.PickOneRandom(); + var downloader = nodes.PickOneRandom(); + + var testFile = GenerateTestFile(fileSizeInMb.MB()); + + LogNodeStatus(uploader); + var contentId = uploader.UploadFile(testFile, f => LogNodeStatus(uploader)); + LogNodeStatus(uploader); + + LogNodeStatus(downloader); + var downloadedFile = downloader.DownloadContent(contentId, f => LogNodeStatus(downloader)); + LogNodeStatus(downloader); + + downloadedFile!.AssertIsEqual(testFile); + + uploader.DeleteRepoFolder(); + uploader.Stop(true); + + var otherDownloader = nodes.PickOneRandom(); + downloadedFile = otherDownloader.DownloadContent(contentId); + + downloadedFile!.AssertIsEqual(testFile); + + downloader.DeleteRepoFolder(); + otherDownloader.DeleteRepoFolder(); + } + + /// + /// We upload a file to each node, to put a more wide-spread load on the network. + /// Then we run the same test as ShouldMaintainFileInNetwork. + /// + [Ignore("Fix ShouldMaintainFileInNetwork for all values first")] + [Test] + [Combinatorial] + [UseLongTimeouts] + [DontDownloadLogs] + [WaitForCleanup] + public void EveryoneGetsAFile( + [Values(10, 40, 80, 100)] int numberOfNodes, + [Values(100, 1000, 5000, 10000)] int fileSizeInMb + ) + { + var logLevel = CodexLogLevel.Info; + + var bootstrap = StartCodex(s => s.WithLogLevel(logLevel)); + var nodes = StartCodex(numberOfNodes - 1, s => s + .WithBootstrapNode(bootstrap) + .WithLogLevel(logLevel) + .WithStorageQuota((fileSizeInMb + 50).MB()) + ).ToList(); + + var pairTasks = nodes.Select(n => + { + return Task.Run(() => + { + var file = GenerateTestFile(fileSizeInMb.MB()); + var cid = n.UploadFile(file); + return new NodeFilePair(n, file, cid); + }); + }); + + var pairs = pairTasks.Select(t => Time.Wait(t)).ToList(); + + RunDoubleDownloadTest( + pairs.PickOneRandom(), + pairs.PickOneRandom(), + pairs.PickOneRandom() + ); + } + + private void RunDoubleDownloadTest(NodeFilePair source, NodeFilePair dl1, NodeFilePair dl2) + { + var expectedFile = source.File; + var cid = source.Cid; + + var file1 = dl1.Node.DownloadContent(cid); + file1!.AssertIsEqual(expectedFile); + + source.Node.Stop(true); + + var file2 = dl2.Node.DownloadContent(cid); + file2!.AssertIsEqual(expectedFile); + } + + public class NodeFilePair + { + public NodeFilePair(ICodexNode node, TrackedFile file, ContentId cid) + { + Node = node; + File = file; + Cid = cid; + } + + public ICodexNode Node { get; } + public TrackedFile File { get; } + public ContentId Cid { get; } + } +} diff --git a/Tests/CodexTests/AutoBootstrapDistTest.cs b/Tests/CodexTests/AutoBootstrapDistTest.cs index 5c70496e..fa103208 100644 --- a/Tests/CodexTests/AutoBootstrapDistTest.cs +++ b/Tests/CodexTests/AutoBootstrapDistTest.cs @@ -8,7 +8,7 @@ namespace CodexTests [SetUp] public void SetUpBootstrapNode() { - BootstrapNode = AddCodex(s => s.WithName("BOOTSTRAP")); + BootstrapNode = StartCodex(s => s.WithName("BOOTSTRAP")); } [TearDown] diff --git a/Tests/CodexTests/BasicTests/ContinuousSubstitute.cs b/Tests/CodexTests/BasicTests/ContinuousSubstitute.cs deleted file mode 100644 index 299c4b94..00000000 --- a/Tests/CodexTests/BasicTests/ContinuousSubstitute.cs +++ /dev/null @@ -1,298 +0,0 @@ -using CodexContractsPlugin; -using CodexPlugin; -using GethPlugin; -using KubernetesWorkflow.Types; -using Logging; -using MetricsPlugin; -using NUnit.Framework; -using Utils; - -namespace CodexTests.BasicTests -{ - [Ignore("Used for debugging continuous tests")] - [TestFixture] - public class ContinuousSubstitute : AutoBootstrapDistTest - { - [Test] - public void ContinuousTestSubstitute() - { - var geth = Ci.StartGethNode(s => s.IsMiner().WithName("geth")); - var contract = Ci.StartCodexContracts(geth); - - var group = AddCodex(5, o => o - .EnableMetrics() - .EnableMarketplace(geth, contract, s => s - .WithInitial(10.Eth(), 100000.TestTokens()) - .AsStorageNode() - .AsValidator()) - .WithBlockTTL(TimeSpan.FromMinutes(5)) - .WithBlockMaintenanceInterval(TimeSpan.FromSeconds(10)) - .WithBlockMaintenanceNumber(100) - .WithStorageQuota(1.GB())); - - var nodes = group.Cast().ToArray(); - - var rc = Ci.DeployMetricsCollector(nodes); - - var availability = new StorageAvailability( - totalSpace: 500.MB(), - maxDuration: TimeSpan.FromMinutes(5), - minPriceForTotalSpace: 500.TestTokens(), - maxCollateral: 1024.TestTokens() - ); - - foreach (var node in nodes) - { - node.Marketplace.MakeStorageAvailable(availability); - } - - var endTime = DateTime.UtcNow + TimeSpan.FromHours(10); - while (DateTime.UtcNow < endTime) - { - var allNodes = nodes.ToList(); - var primary = allNodes.PickOneRandom(); - var secondary = allNodes.PickOneRandom(); - - Log("Run Test"); - PerformTest(primary, secondary, rc); - - Thread.Sleep(TimeSpan.FromSeconds(5)); - } - } - - private void LogBytesPerMillisecond(Action action) - { - var sw = Stopwatch.Begin(GetTestLog()); - action(); - var duration = sw.End(); - double totalMs = duration.TotalMilliseconds; - double totalBytes = fileSize.SizeInBytes; - - var bytesPerMs = totalBytes / totalMs; - Log($"Bytes per millisecond: {bytesPerMs}"); - } - - [Test] - public void PeerTest() - { - var group = AddCodex(5, o => o - //.EnableMetrics() - //.EnableMarketplace(100000.TestTokens(), 0.Eth(), isValidator: true) - .WithBlockTTL(TimeSpan.FromMinutes(2)) - .WithBlockMaintenanceInterval(TimeSpan.FromMinutes(2)) - .WithBlockMaintenanceNumber(10000) - .WithBlockTTL(TimeSpan.FromMinutes(2)) - .WithStorageQuota(1.GB())); - - var nodes = group.Cast().ToArray(); - - var checkTime = DateTime.UtcNow + TimeSpan.FromMinutes(1); - var endTime = DateTime.UtcNow + TimeSpan.FromHours(10); - while (DateTime.UtcNow < endTime) - { - //CreatePeerConnectionTestHelpers().AssertFullyConnected(GetAllOnlineCodexNodes()); - //CheckRoutingTables(GetAllOnlineCodexNodes()); - - var node = nodes.ToList().PickOneRandom(); - var file = GenerateTestFile(50.MB()); - node.UploadFile(file); - - Thread.Sleep(20000); - } - } - - private void CheckRoutingTables(IEnumerable nodes) - { - var all = nodes.ToArray(); - var allIds = all.Select(n => n.GetDebugInfo().Table.LocalNode.NodeId).ToArray(); - - var errors = all.Select(n => AreAllPresent(n, allIds)).Where(s => !string.IsNullOrEmpty(s)).ToArray(); - - if (errors.Any()) - { - Assert.Fail(string.Join(Environment.NewLine, errors)); - } - } - - private string AreAllPresent(ICodexNode n, string[] allIds) - { - var info = n.GetDebugInfo(); - var known = info.Table.Nodes.Select(n => n.NodeId).ToArray(); - var expected = allIds.Where(i => i != info.Table.LocalNode.NodeId).ToArray(); - - if (!expected.All(ex => known.Contains(ex))) - { - return $"Not all of '{string.Join(",", expected)}' were present in routing table: '{string.Join(",", known)}'"; - } - - return string.Empty; - } - - private ByteSize fileSize = 80.MB(); - - private const string BytesStoredMetric = "codexRepostoreBytesUsed"; - - private void PerformTest(ICodexNode primary, ICodexNode secondary, RunningContainers rc) - { - ScopedTestFiles(() => - { - var testFile = GenerateTestFile(fileSize); - - var metrics = Ci.WrapMetricsCollector(rc, primary); - var beforeBytesStored = metrics.GetMetric(BytesStoredMetric); - - ContentId contentId = null!; - LogBytesPerMillisecond(() => contentId = primary.UploadFile(testFile)); - - var low = fileSize.SizeInBytes; - var high = low * 1.2; - Log("looking for: " + low + " < " + high); - - Time.WaitUntil(() => - { - var afterBytesStored = metrics.GetMetric(BytesStoredMetric); - var newBytes = Convert.ToInt64(afterBytesStored.Values.Last().Value - beforeBytesStored.Values.Last().Value); - - return high > newBytes && newBytes > low; - }, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(2)); - - FileUtils.TrackedFile? downloadedFile = null; - LogBytesPerMillisecond(() => downloadedFile = secondary.DownloadContent(contentId)); - - testFile.AssertIsEqual(downloadedFile); - }); - } - - [Test] - public void HoldMyBeerTest() - { - var blockExpirationTime = TimeSpan.FromMinutes(3); - var group = AddCodex(3, o => o - .EnableMetrics() - .WithBlockTTL(blockExpirationTime) - .WithBlockMaintenanceInterval(TimeSpan.FromMinutes(2)) - .WithBlockMaintenanceNumber(10000) - .WithStorageQuota(2000.MB())); - - var nodes = group.Cast().ToArray(); - - var endTime = DateTime.UtcNow + TimeSpan.FromHours(24); - - var filesize = 80.MB(); - double codexDefaultBlockSize = 31 * 64 * 33; - var numberOfBlocks = Convert.ToInt64(Math.Ceiling(filesize.SizeInBytes / codexDefaultBlockSize)); - var sizeInBytes = filesize.SizeInBytes; - Assert.That(numberOfBlocks, Is.EqualTo(1282)); - - var startTime = DateTime.UtcNow; - var successfulUploads = 0; - var successfulDownloads = 0; - - while (DateTime.UtcNow < endTime) - { - foreach (var node in nodes) - { - try - { - Thread.Sleep(TimeSpan.FromSeconds(5)); - - ScopedTestFiles(() => - { - var uploadStartTime = DateTime.UtcNow; - var file = GenerateTestFile(filesize); - var cid = node.UploadFile(file); - - var cidTag = cid.Id.Substring(cid.Id.Length - 6); - Measure("upload-log-asserts", () => - { - var uploadLog = Ci.DownloadLog(node, tailLines: 50000); - - var storeLines = uploadLog.FindLinesThatContain("Stored data", "topics=\"codex node\""); - uploadLog.DeleteFile(); - - var storeLine = GetLineForCidTag(storeLines, cidTag); - AssertStoreLineContains(storeLine, numberOfBlocks, sizeInBytes); - }); - successfulUploads++; - - var uploadTimeTaken = DateTime.UtcNow - uploadStartTime; - if (uploadTimeTaken >= blockExpirationTime.Subtract(TimeSpan.FromSeconds(10))) - { - Assert.Fail("Upload took too long. Blocks already expired."); - } - - var dl = node.DownloadContent(cid); - file.AssertIsEqual(dl); - - Measure("download-log-asserts", () => - { - var downloadLog = Ci.DownloadLog(node, tailLines: 50000); - - var sentLines = downloadLog.FindLinesThatContain("Sent bytes", "topics=\"codex restapi\""); - downloadLog.DeleteFile(); - - var sentLine = GetLineForCidTag(sentLines, cidTag); - AssertSentLineContains(sentLine, sizeInBytes); - }); - successfulDownloads++; - }); - } - catch - { - var testDuration = DateTime.UtcNow - startTime; - Log("Test failed. Delaying shut-down by 30 seconds to collect metrics."); - Log($"Test failed after {Time.FormatDuration(testDuration)} and {successfulUploads} successful uploads and {successfulDownloads} successful downloads"); - Thread.Sleep(TimeSpan.FromSeconds(30)); - throw; - } - } - - Thread.Sleep(TimeSpan.FromSeconds(5)); - } - } - - private void AssertSentLineContains(string sentLine, long sizeInBytes) - { - var tag = "bytes="; - var token = sentLine.Substring(sentLine.IndexOf(tag) + tag.Length); - var bytes = Convert.ToInt64(token); - Assert.AreEqual(sizeInBytes, bytes, $"Sent bytes: Number of bytes incorrect. Line: '{sentLine}'"); - } - - private void AssertStoreLineContains(string storeLine, long numberOfBlocks, long sizeInBytes) - { - var tokens = storeLine.Split(" "); - - var blocksToken = GetToken(tokens, "blocks="); - var sizeToken = GetToken(tokens, "size="); - if (blocksToken == null) Assert.Fail("blockToken not found in " + storeLine); - if (sizeToken == null) Assert.Fail("sizeToken not found in " + storeLine); - - var blocks = Convert.ToInt64(blocksToken); - var size = Convert.ToInt64(sizeToken?.Replace("'NByte", "")); - - var lineLog = $" Line: '{storeLine}'"; - Assert.AreEqual(numberOfBlocks, blocks, "Stored data: Number of blocks incorrect." + lineLog); - Assert.AreEqual(sizeInBytes, size, "Stored data: Number of blocks incorrect." + lineLog); - } - - private string GetLineForCidTag(string[] lines, string cidTag) - { - var result = lines.SingleOrDefault(l => l.Contains(cidTag)); - if (result == null) - { - Assert.Fail($"Failed to find '{cidTag}' in lines: '{string.Join(",", lines)}'"); - throw new Exception(); - } - - return result; - } - - private string? GetToken(string[] tokens, string tag) - { - var token = tokens.SingleOrDefault(t => t.StartsWith(tag)); - if (token == null) return null; - return token.Substring(tag.Length); - } - } -} diff --git a/Tests/CodexTests/BasicTests/DiscordBotTests.cs b/Tests/CodexTests/BasicTests/DiscordBotTests.cs deleted file mode 100644 index 64309234..00000000 --- a/Tests/CodexTests/BasicTests/DiscordBotTests.cs +++ /dev/null @@ -1,110 +0,0 @@ -using CodexContractsPlugin; -using CodexDiscordBotPlugin; -using CodexPlugin; -using GethPlugin; -using NUnit.Framework; -using Utils; - -namespace CodexTests.BasicTests -{ - [TestFixture] - public class DiscordBotTests : AutoBootstrapDistTest - { - [Test] - public void BotRewardTest() - { - var myAccount = EthAccount.GenerateNew(); - - var sellerInitialBalance = 234.TestTokens(); - var buyerInitialBalance = 100000.TestTokens(); - var fileSize = 11.MB(); - - var geth = Ci.StartGethNode(s => s.IsMiner().WithName("disttest-geth")); - var contracts = Ci.StartCodexContracts(geth); - - // start bot and rewarder - var gethInfo = new DiscordBotGethInfo( - host: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Host, - port: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Port, - privKey: geth.StartResult.Account.PrivateKey, - marketplaceAddress: contracts.Deployment.MarketplaceAddress, - tokenAddress: contracts.Deployment.TokenAddress, - abi: contracts.Deployment.Abi - ); - var bot = Ci.DeployCodexDiscordBot(new DiscordBotStartupConfig( - name: "bot", - token: "aaa", - serverName: "ThatBen's server", - adminRoleName: "bottest-admins", - adminChannelName: "admin-channel", - rewardChannelName: "rewards-channel", - kubeNamespace: "notneeded", - gethInfo: gethInfo - )); - var botContainer = bot.Containers.Single(); - Ci.DeployRewarderBot(new RewarderBotStartupConfig( - //discordBotHost: "http://" + botContainer.GetAddress(GetTestLog(), DiscordBotContainerRecipe.RewardsPort).Host, - //discordBotPort: botContainer.GetAddress(GetTestLog(), DiscordBotContainerRecipe.RewardsPort).Port, - discordBotHost: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Host, - discordBotPort: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Port, - intervalMinutes: "1", - historyStartUtc: GetTestRunTimeRange().From - TimeSpan.FromMinutes(3), - gethInfo: gethInfo, - dataPath: null - )); - - var numberOfHosts = 3; - - for (var i = 0; i < numberOfHosts; i++) - { - var seller = AddCodex(s => s - .WithName("Seller") - .WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn) - { - ContractClock = CodexLogLevel.Trace, - }) - .WithStorageQuota(11.GB()) - .EnableMarketplace(geth, contracts, m => m - .WithAccount(myAccount) - .WithInitial(10.Eth(), sellerInitialBalance) - .AsStorageNode() - .AsValidator())); - - var availability = new StorageAvailability( - totalSpace: 10.GB(), - maxDuration: TimeSpan.FromMinutes(30), - minPriceForTotalSpace: 1.TestTokens(), - maxCollateral: 20.TestTokens() - ); - seller.Marketplace.MakeStorageAvailable(availability); - } - - var testFile = GenerateTestFile(fileSize); - - var buyer = AddCodex(s => s - .WithName("Buyer") - .EnableMarketplace(geth, contracts, m => m - .WithAccount(myAccount) - .WithInitial(10.Eth(), buyerInitialBalance))); - - var contentId = buyer.UploadFile(testFile); - - var purchase = new StoragePurchaseRequest(contentId) - { - PricePerSlotPerSecond = 2.TestTokens(), - RequiredCollateral = 10.TestTokens(), - MinRequiredNumberOfNodes = 5, - NodeFailureTolerance = 2, - ProofProbability = 5, - Duration = TimeSpan.FromMinutes(6), - Expiry = TimeSpan.FromMinutes(5) - }; - - var purchaseContract = buyer.Marketplace.RequestStorage(purchase); - - purchaseContract.WaitForStorageContractStarted(); - - purchaseContract.WaitForStorageContractFinished(); - } - } -} diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index e7453b5e..f9d5b89e 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -13,7 +13,7 @@ namespace CodexTests.BasicTests [Test] public void CodexLogExample() { - var primary = AddCodex(s => s.WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Warn, CodexLogLevel.Warn))); + var primary = StartCodex(s => s.WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Warn, CodexLogLevel.Warn))); var cid = primary.UploadFile(GenerateTestFile(5.MB())); @@ -28,8 +28,8 @@ namespace CodexTests.BasicTests [Test] public void TwoMetricsExample() { - var group = AddCodex(2, s => s.EnableMetrics()); - var group2 = AddCodex(2, s => s.EnableMetrics()); + var group = StartCodex(2, s => s.EnableMetrics()); + var group2 = StartCodex(2, s => s.EnableMetrics()); var primary = group[0]; var secondary = group[1]; @@ -45,6 +45,9 @@ namespace CodexTests.BasicTests metrics[0].AssertThat("libp2p_peers", Is.EqualTo(1)); metrics[1].AssertThat("libp2p_peers", Is.EqualTo(1)); + + LogNodeStatus(primary, metrics[0]); + LogNodeStatus(primary2, metrics[1]); } [Test] diff --git a/Tests/CodexTests/BasicTests/MarketplaceTests.cs b/Tests/CodexTests/BasicTests/MarketplaceTests.cs index 7474696e..4a0f404a 100644 --- a/Tests/CodexTests/BasicTests/MarketplaceTests.cs +++ b/Tests/CodexTests/BasicTests/MarketplaceTests.cs @@ -13,42 +13,42 @@ namespace CodexTests.BasicTests [Test] public void MarketplaceExample() { - var hostInitialBalance = 234.TestTokens(); - var clientInitialBalance = 100000.TestTokens(); + var hostInitialBalance = 234.TstWei(); + var clientInitialBalance = 100000.TstWei(); var fileSize = 10.MB(); var geth = Ci.StartGethNode(s => s.IsMiner().WithName("disttest-geth")); var contracts = Ci.StartCodexContracts(geth); + + var numberOfHosts = 5; + var hosts = StartCodex(numberOfHosts, s => s + .WithName("Host") + .WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn) + { + ContractClock = CodexLogLevel.Trace, + }) + .WithStorageQuota(11.GB()) + .EnableMarketplace(geth, contracts, m => m + .WithInitial(10.Eth(), hostInitialBalance) + .AsStorageNode() + .AsValidator())); - var numberOfHosts = 3; - for (var i = 0; i < numberOfHosts; i++) + foreach (var host in hosts) { - var host = AddCodex(s => s - .WithName("Host") - .WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn) - { - ContractClock = CodexLogLevel.Trace, - }) - .WithStorageQuota(11.GB()) - .EnableMarketplace(geth, contracts, m => m - .WithInitial(10.Eth(), hostInitialBalance) - .AsStorageNode() - .AsValidator())); - AssertBalance(contracts, host, Is.EqualTo(hostInitialBalance)); var availability = new StorageAvailability( totalSpace: 10.GB(), maxDuration: TimeSpan.FromMinutes(30), - minPriceForTotalSpace: 1.TestTokens(), - maxCollateral: 20.TestTokens() + minPriceForTotalSpace: 1.TstWei(), + maxCollateral: 20.TstWei() ); host.Marketplace.MakeStorageAvailable(availability); } var testFile = GenerateTestFile(fileSize); - var client = AddCodex(s => s + var client = StartCodex(s => s .WithName("Client") .EnableMarketplace(geth, contracts, m => m .WithInitial(10.Eth(), clientInitialBalance))); @@ -59,13 +59,13 @@ namespace CodexTests.BasicTests var purchase = new StoragePurchaseRequest(contentId) { - PricePerSlotPerSecond = 2.TestTokens(), - RequiredCollateral = 10.TestTokens(), + PricePerSlotPerSecond = 2.TstWei(), + RequiredCollateral = 10.TstWei(), MinRequiredNumberOfNodes = 5, NodeFailureTolerance = 2, ProofProbability = 5, - Duration = TimeSpan.FromMinutes(5), - Expiry = TimeSpan.FromMinutes(4) + Duration = TimeSpan.FromMinutes(6), + Expiry = TimeSpan.FromMinutes(5) }; var purchaseContract = client.Marketplace.RequestStorage(purchase); @@ -84,17 +84,57 @@ namespace CodexTests.BasicTests Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Finished)); } + [Test] + public void CanDownloadContentFromContractCid() + { + var fileSize = 10.MB(); + var geth = Ci.StartGethNode(s => s.IsMiner().WithName("disttest-geth")); + var contracts = Ci.StartCodexContracts(geth); + var testFile = GenerateTestFile(fileSize); + + var client = StartCodex(s => s + .WithName("Client") + .EnableMarketplace(geth, contracts, m => m + .WithInitial(10.Eth(), 10.Tst()))); + + var uploadCid = client.UploadFile(testFile); + + var purchase = new StoragePurchaseRequest(uploadCid) + { + PricePerSlotPerSecond = 2.TstWei(), + RequiredCollateral = 10.TstWei(), + MinRequiredNumberOfNodes = 5, + NodeFailureTolerance = 2, + ProofProbability = 5, + Duration = TimeSpan.FromMinutes(5), + Expiry = TimeSpan.FromMinutes(4) + }; + + var purchaseContract = client.Marketplace.RequestStorage(purchase); + var contractCid = purchaseContract.ContentId; + Assert.That(uploadCid.Id, Is.Not.EqualTo(contractCid.Id)); + + // Download both from client. + testFile.AssertIsEqual(client.DownloadContent(uploadCid)); + testFile.AssertIsEqual(client.DownloadContent(contractCid)); + + // Download both from another node. + var downloader = StartCodex(s => s.WithName("Downloader")); + testFile.AssertIsEqual(downloader.DownloadContent(uploadCid)); + testFile.AssertIsEqual(downloader.DownloadContent(contractCid)); + } + private void WaitForAllSlotFilledEvents(ICodexContracts contracts, StoragePurchaseRequest purchase, IGethNode geth) { Time.Retry(() => { - var blockRange = geth.ConvertTimeRangeToBlockRange(GetTestRunTimeRange()); - var slotFilledEvents = contracts.GetSlotFilledEvents(blockRange); + var events = contracts.GetEvents(GetTestRunTimeRange()); + var slotFilledEvents = events.GetSlotFilledEvents(); - Log($"SlotFilledEvents: {slotFilledEvents.Length} - NumSlots: {purchase.MinRequiredNumberOfNodes}"); - - if (slotFilledEvents.Length != purchase.MinRequiredNumberOfNodes) throw new Exception(); - }, Convert.ToInt32(purchase.Duration.TotalSeconds / 5) + 10, TimeSpan.FromSeconds(5), "Checking SlotFilled events"); + var msg = $"SlotFilledEvents: {slotFilledEvents.Length} - NumSlots: {purchase.MinRequiredNumberOfNodes}"; + Debug(msg); + if (slotFilledEvents.Length != purchase.MinRequiredNumberOfNodes) throw new Exception(msg); + }, purchase.Expiry + TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(5), "Checking SlotFilled events"); } private void AssertStorageRequest(Request request, StoragePurchaseRequest purchase, ICodexContracts contracts, ICodexNode buyer) @@ -106,7 +146,8 @@ namespace CodexTests.BasicTests private Request GetOnChainStorageRequest(ICodexContracts contracts, IGethNode geth) { - var requests = contracts.GetStorageRequests(geth.ConvertTimeRangeToBlockRange(GetTestRunTimeRange())); + var events = contracts.GetEvents(GetTestRunTimeRange()); + var requests = events.GetStorageRequests(); Assert.That(requests.Length, Is.EqualTo(1)); return requests.Single(); } diff --git a/Tests/CodexTests/BasicTests/OneClientTests.cs b/Tests/CodexTests/BasicTests/OneClientTests.cs index d8045862..908fb937 100644 --- a/Tests/CodexTests/BasicTests/OneClientTests.cs +++ b/Tests/CodexTests/BasicTests/OneClientTests.cs @@ -1,5 +1,4 @@ using CodexPlugin; -using DistTestCore; using NUnit.Framework; using Utils; @@ -11,21 +10,11 @@ namespace CodexTests.BasicTests [Test] public void OneClientTest() { - var primary = Ci.StartCodexNode(); + var primary = StartCodex(); PerformOneClientTest(primary); - } - [Test] - public void RestartTest() - { - var primary = Ci.StartCodexNode(); - - primary.Stop(waitTillStopped: true); - - primary = Ci.StartCodexNode(); - - PerformOneClientTest(primary); + LogNodeStatus(primary); } private void PerformOneClientTest(ICodexNode primary) diff --git a/Tests/CodexTests/BasicTests/ThreeClientTest.cs b/Tests/CodexTests/BasicTests/ThreeClientTest.cs index bd9cdfb4..e6f9428e 100644 --- a/Tests/CodexTests/BasicTests/ThreeClientTest.cs +++ b/Tests/CodexTests/BasicTests/ThreeClientTest.cs @@ -1,4 +1,5 @@ -using NUnit.Framework; +using CodexPlugin; +using NUnit.Framework; using Utils; namespace CodexTests.BasicTests @@ -9,8 +10,8 @@ namespace CodexTests.BasicTests [Test] public void ThreeClient() { - var primary = AddCodex(); - var secondary = AddCodex(); + var primary = StartCodex(); + var secondary = StartCodex(); var testFile = GenerateTestFile(10.MB()); @@ -20,5 +21,34 @@ namespace CodexTests.BasicTests testFile.AssertIsEqual(downloadedFile); } + + [Test] + public void DownloadingUnknownCidDoesNotCauseCrash() + { + var node = StartCodex(2).First(); + + var unknownCid = new ContentId("zDvZRwzkzHsok3Z8yMoiXE9EDBFwgr8WygB8s4ddcLzzSwwXAxLZ"); + + try + { + node.DownloadContent(unknownCid); + } + catch (Exception ex) + { + if (!ex.Message.StartsWith("Retry 'DownloadFile' timed out")) + { + throw; + } + } + + // Check that the node stays alive for at least another 5 minutes. + var start = DateTime.UtcNow; + while ((DateTime.UtcNow - start) < TimeSpan.FromMinutes(5)) + { + Thread.Sleep(5000); + var info = node.GetDebugInfo(); + Assert.That(!string.IsNullOrEmpty(info.Id)); + } + } } } diff --git a/Tests/CodexTests/BasicTests/TwoClientTests.cs b/Tests/CodexTests/BasicTests/TwoClientTests.cs index 546fbda3..ab15ff25 100644 --- a/Tests/CodexTests/BasicTests/TwoClientTests.cs +++ b/Tests/CodexTests/BasicTests/TwoClientTests.cs @@ -10,8 +10,8 @@ namespace CodexTests.BasicTests [Test] public void TwoClientTest() { - var uploader = AddCodex(s => s.WithName("Uploader")); - var downloader = AddCodex(s => s.WithName("Downloader").WithBootstrapNode(uploader)); + var uploader = StartCodex(s => s.WithName("Uploader")); + var downloader = StartCodex(s => s.WithName("Downloader").WithBootstrapNode(uploader)); PerformTwoClientTest(uploader, downloader); } diff --git a/Tests/CodexTests/CodexDistTest.cs b/Tests/CodexTests/CodexDistTest.cs index 0578c27a..e62627ff 100644 --- a/Tests/CodexTests/CodexDistTest.cs +++ b/Tests/CodexTests/CodexDistTest.cs @@ -6,14 +6,14 @@ using Core; using DistTestCore; using DistTestCore.Helpers; using DistTestCore.Logs; +using MetricsPlugin; +using Newtonsoft.Json; using NUnit.Framework.Constraints; namespace CodexTests { public class CodexDistTest : DistTest { - private readonly Dictionary> onlineCodexNodes = new Dictionary>(); - public CodexDistTest() { ProjectPlugin.Load(); @@ -29,39 +29,29 @@ namespace CodexTests localBuilder.Build(); } - protected override void LifecycleStart(TestLifecycle lifecycle) + public ICodexNode StartCodex() { - onlineCodexNodes.Add(lifecycle, new List()); + return StartCodex(s => { }); } - protected override void LifecycleStop(TestLifecycle lifecycle) + public ICodexNode StartCodex(Action setup) { - onlineCodexNodes.Remove(lifecycle); + return StartCodex(1, setup)[0]; } - public ICodexNode AddCodex() + public ICodexNodeGroup StartCodex(int numberOfNodes) { - return AddCodex(s => { }); + return StartCodex(numberOfNodes, s => { }); } - public ICodexNode AddCodex(Action setup) - { - return AddCodex(1, setup)[0]; - } - - public ICodexNodeGroup AddCodex(int numberOfNodes) - { - return AddCodex(numberOfNodes, s => { }); - } - - public ICodexNodeGroup AddCodex(int numberOfNodes, Action setup) + public ICodexNodeGroup StartCodex(int numberOfNodes, Action setup) { var group = Ci.StartCodexNodes(numberOfNodes, s => { setup(s); OnCodexSetup(s); }); - onlineCodexNodes[Get()].AddRange(group); + return group; } @@ -75,11 +65,6 @@ namespace CodexTests return new PeerDownloadTestHelpers(GetTestLog(), Get().GetFileManager()); } - public IEnumerable GetAllOnlineCodexNodes() - { - return onlineCodexNodes[Get()]; - } - public void AssertBalance(ICodexContracts contracts, ICodexNode codexNode, Constraint constraint, string msg = "") { AssertHelpers.RetryAssert(constraint, () => contracts.GetTestTokenBalance(codexNode), nameof(AssertBalance) + msg); @@ -99,17 +84,29 @@ namespace CodexTests log.AssertLogDoesNotContain("ERR "); } + public void LogNodeStatus(ICodexNode node, IMetricsAccess? metrics = null) + { + Log("Status for " + node.GetName() + Environment.NewLine + + GetBasicNodeStatus(node)); + } + + private string GetBasicNodeStatus(ICodexNode node) + { + return JsonConvert.SerializeObject(node.GetDebugInfo(), Formatting.Indented) + Environment.NewLine + + node.Space().ToString() + Environment.NewLine; + } + + // Disabled for now: Makes huge log files! + //private string GetNodeMetrics(IMetricsAccess? metrics) + //{ + // if (metrics == null) return "No metrics enabled"; + // var m = metrics.GetAllMetrics(); + // if (m == null) return "No metrics received"; + // return m.AsCsv(); + //} + protected virtual void OnCodexSetup(ICodexSetup setup) { } - - protected override void CollectStatusLogData(TestLifecycle lifecycle, Dictionary data) - { - var nodes = onlineCodexNodes[lifecycle]; - var upload = nodes.Select(n => n.TransferSpeeds.GetUploadSpeed()).ToList()!.OptionalAverage(); - var download = nodes.Select(n => n.TransferSpeeds.GetDownloadSpeed()).ToList()!.OptionalAverage(); - if (upload != null) data.Add("avgupload", upload.ToString()); - if (download != null) data.Add("avgdownload", download.ToString()); - } } } diff --git a/Tests/CodexTests/CodexTests.csproj b/Tests/CodexTests/CodexTests.csproj index 495caf6f..4a9a3edb 100644 --- a/Tests/CodexTests/CodexTests.csproj +++ b/Tests/CodexTests/CodexTests.csproj @@ -13,11 +13,13 @@ + + diff --git a/Tests/CodexTests/DownloadConnectivityTests/FullyConnectedDownloadTests.cs b/Tests/CodexTests/DownloadConnectivityTests/FullyConnectedDownloadTests.cs index 7f11171f..e6a42c50 100644 --- a/Tests/CodexTests/DownloadConnectivityTests/FullyConnectedDownloadTests.cs +++ b/Tests/CodexTests/DownloadConnectivityTests/FullyConnectedDownloadTests.cs @@ -1,4 +1,5 @@ using CodexContractsPlugin; +using CodexPlugin; using GethPlugin; using NUnit.Framework; using Utils; @@ -11,9 +12,9 @@ namespace CodexTests.DownloadConnectivityTests [Test] public void MetricsDoesNotInterfereWithPeerDownload() { - AddCodex(2, s => s.EnableMetrics()); + var nodes = StartCodex(2, s => s.EnableMetrics()); - AssertAllNodesConnected(); + AssertAllNodesConnected(nodes); } [Test] @@ -21,10 +22,10 @@ namespace CodexTests.DownloadConnectivityTests { var geth = Ci.StartGethNode(s => s.IsMiner()); var contracts = Ci.StartCodexContracts(geth); - AddCodex(2, s => s.EnableMarketplace(geth, contracts, m => m - .WithInitial(10.Eth(), 1000.TestTokens()))); + var nodes = StartCodex(2, s => s.EnableMarketplace(geth, contracts, m => m + .WithInitial(10.Eth(), 1000.TstWei()))); - AssertAllNodesConnected(); + AssertAllNodesConnected(nodes); } [Test] @@ -33,14 +34,14 @@ namespace CodexTests.DownloadConnectivityTests [Values(2, 5)] int numberOfNodes, [Values(1, 10)] int sizeMBs) { - AddCodex(numberOfNodes); + var nodes = StartCodex(numberOfNodes); - AssertAllNodesConnected(sizeMBs); + AssertAllNodesConnected(nodes, sizeMBs); } - private void AssertAllNodesConnected(int sizeMBs = 10) + private void AssertAllNodesConnected(IEnumerable nodes, int sizeMBs = 10) { - CreatePeerDownloadTestHelpers().AssertFullDownloadInterconnectivity(GetAllOnlineCodexNodes(), sizeMBs.MB()); + CreatePeerDownloadTestHelpers().AssertFullDownloadInterconnectivity(nodes, sizeMBs.MB()); } } } diff --git a/Tests/CodexTests/PeerDiscoveryTests/LayeredDiscoveryTests.cs b/Tests/CodexTests/PeerDiscoveryTests/LayeredDiscoveryTests.cs index 00c0cda9..33225d6f 100644 --- a/Tests/CodexTests/PeerDiscoveryTests/LayeredDiscoveryTests.cs +++ b/Tests/CodexTests/PeerDiscoveryTests/LayeredDiscoveryTests.cs @@ -1,4 +1,5 @@ -using NUnit.Framework; +using CodexPlugin; +using NUnit.Framework; namespace CodexTests.PeerDiscoveryTests { @@ -8,24 +9,24 @@ namespace CodexTests.PeerDiscoveryTests [Test] public void TwoLayersTest() { - var root = AddCodex(); - var l1Source = AddCodex(s => s.WithBootstrapNode(root)); - var l1Node = AddCodex(s => s.WithBootstrapNode(root)); - var l2Target = AddCodex(s => s.WithBootstrapNode(l1Node)); + var root = StartCodex(); + var l1Source = StartCodex(s => s.WithBootstrapNode(root)); + var l1Node = StartCodex(s => s.WithBootstrapNode(root)); + var l2Target = StartCodex(s => s.WithBootstrapNode(l1Node)); - AssertAllNodesConnected(); + AssertAllNodesConnected(root, l1Source, l1Node, l2Target); } [Test] public void ThreeLayersTest() { - var root = AddCodex(); - var l1Source = AddCodex(s => s.WithBootstrapNode(root)); - var l1Node = AddCodex(s => s.WithBootstrapNode(root)); - var l2Node = AddCodex(s => s.WithBootstrapNode(l1Node)); - var l3Target = AddCodex(s => s.WithBootstrapNode(l2Node)); + var root = StartCodex(); + var l1Source = StartCodex(s => s.WithBootstrapNode(root)); + var l1Node = StartCodex(s => s.WithBootstrapNode(root)); + var l2Node = StartCodex(s => s.WithBootstrapNode(l1Node)); + var l3Target = StartCodex(s => s.WithBootstrapNode(l2Node)); - AssertAllNodesConnected(); + AssertAllNodesConnected(root, l1Source, l1Node, l2Node, l3Target); } [TestCase(3)] @@ -33,18 +34,22 @@ namespace CodexTests.PeerDiscoveryTests [TestCase(10)] public void NodeChainTest(int chainLength) { - var node = AddCodex(); + var nodes = new List(); + var node = StartCodex(); + nodes.Add(node); + for (var i = 1; i < chainLength; i++) { - node = AddCodex(s => s.WithBootstrapNode(node)); + node = StartCodex(s => s.WithBootstrapNode(node)); + nodes.Add(node); } - AssertAllNodesConnected(); + AssertAllNodesConnected(nodes.ToArray()); } - private void AssertAllNodesConnected() + private void AssertAllNodesConnected(params ICodexNode[] nodes) { - CreatePeerConnectionTestHelpers().AssertFullyConnected(GetAllOnlineCodexNodes()); + CreatePeerConnectionTestHelpers().AssertFullyConnected(nodes); } } } diff --git a/Tests/CodexTests/PeerDiscoveryTests/PeerDiscoveryTests.cs b/Tests/CodexTests/PeerDiscoveryTests/PeerDiscoveryTests.cs index 1e0faa4b..938e873f 100644 --- a/Tests/CodexTests/PeerDiscoveryTests/PeerDiscoveryTests.cs +++ b/Tests/CodexTests/PeerDiscoveryTests/PeerDiscoveryTests.cs @@ -12,7 +12,7 @@ namespace CodexTests.PeerDiscoveryTests public void CanReportUnknownPeerId() { var unknownId = "16Uiu2HAkv2CHWpff3dj5iuVNERAp8AGKGNgpGjPexJZHSqUstfsK"; - var node = AddCodex(); + var node = StartCodex(); var result = node.GetDebugPeer(unknownId); Assert.That(result.IsPeerFound, Is.False); @@ -21,9 +21,9 @@ namespace CodexTests.PeerDiscoveryTests [Test] public void MetricsDoesNotInterfereWithPeerDiscovery() { - AddCodex(2, s => s.EnableMetrics()); + var nodes = StartCodex(2, s => s.EnableMetrics()); - AssertAllNodesConnected(); + AssertAllNodesConnected(nodes); } [Test] @@ -31,10 +31,10 @@ namespace CodexTests.PeerDiscoveryTests { var geth = Ci.StartGethNode(s => s.IsMiner()); var contracts = Ci.StartCodexContracts(geth); - AddCodex(2, s => s.EnableMarketplace(geth, contracts, m => m - .WithInitial(10.Eth(), 1000.TestTokens()))); + var nodes = StartCodex(2, s => s.EnableMarketplace(geth, contracts, m => m + .WithInitial(10.Eth(), 1000.TstWei()))); - AssertAllNodesConnected(); + AssertAllNodesConnected(nodes); } [TestCase(2)] @@ -42,16 +42,17 @@ namespace CodexTests.PeerDiscoveryTests [TestCase(10)] public void VariableNodes(int number) { - AddCodex(number); + var nodes = StartCodex(number); - AssertAllNodesConnected(); + AssertAllNodesConnected(nodes); } - private void AssertAllNodesConnected() + private void AssertAllNodesConnected(IEnumerable nodes) { - var allNodes = GetAllOnlineCodexNodes(); - CreatePeerConnectionTestHelpers().AssertFullyConnected(allNodes); - CheckRoutingTable(allNodes); + nodes = nodes.Concat(new[] { BootstrapNode }).ToArray()!; + + CreatePeerConnectionTestHelpers().AssertFullyConnected(nodes); + CheckRoutingTable(nodes); } private void CheckRoutingTable(IEnumerable allNodes) diff --git a/Tests/CodexTests/UtilityTests/ClusterSpeedTests.cs b/Tests/CodexTests/UtilityTests/ClusterSpeedTests.cs new file mode 100644 index 00000000..7800e132 --- /dev/null +++ b/Tests/CodexTests/UtilityTests/ClusterSpeedTests.cs @@ -0,0 +1,84 @@ +using DistTestCore; +using Logging; +using NUnit.Framework; +using Utils; + +namespace CodexTests.UtilityTests +{ + [TestFixture] + public class ClusterDiscSpeedTests : DistTest + { + private readonly Random random = new Random(); + + [Test] + [Combinatorial] + [Ignore("Used to measure disc io speeds in cluster.")] + public void DiscSpeedTest( + [Values(1, 10, 100, 1024, 1024 * 10, 1024 * 100, 1024 * 1024)] int bufferSizeKb + ) + { + long targetSize = (long)(1024 * 1024 * 1024) * 2; + long bufferSizeBytes = (long)bufferSizeKb * 1024; + + var filename = nameof(DiscSpeedTest); + + Thread.Sleep(2000); + if (File.Exists(filename)) File.Delete(filename); + Thread.Sleep(2000); + var writeSpeed = PerformWrite(targetSize, bufferSizeBytes, filename); + Thread.Sleep(2000); + var readSpeed = PerformRead(targetSize, bufferSizeBytes, filename); + + Log($"Write speed: {writeSpeed} per second."); + Log($"Read speed: {readSpeed} per second."); + } + + private ByteSize PerformWrite(long targetSize, long bufferSizeBytes, string filename) + { + long bytesWritten = 0; + var buffer = new byte[bufferSizeBytes]; + random.NextBytes(buffer); + + var sw = Stopwatch.Begin(GetTestLog()); + using (var stream = File.OpenWrite(filename)) + { + while (bytesWritten < targetSize) + { + long remaining = targetSize - bytesWritten; + long toWrite = Math.Min(bufferSizeBytes, remaining); + + stream.Write(buffer, 0, Convert.ToInt32(toWrite)); + bytesWritten += toWrite; + } + } + var duration = sw.End("WriteTime"); + double totalSeconds = duration.TotalSeconds; + double totalBytes = bytesWritten; + double bytesPerSecond = totalBytes / totalSeconds; + return new ByteSize(Convert.ToInt64(bytesPerSecond)); + } + + private ByteSize PerformRead(long targetSize, long bufferSizeBytes, string filename) + { + long bytesRead = 0; + var buffer = new byte[bufferSizeBytes]; + var sw = Stopwatch.Begin(GetTestLog()); + using (var stream = File.OpenRead(filename)) + { + while (bytesRead < targetSize) + { + long remaining = targetSize - bytesRead; + long toRead = Math.Min(bufferSizeBytes, remaining); + + var r = stream.Read(buffer, 0, Convert.ToInt32(toRead)); + bytesRead += r; + } + } + var duration = sw.End("ReadTime"); + double totalSeconds = duration.TotalSeconds; + double totalBytes = bytesRead; + double bytesPerSecond = totalBytes / totalSeconds; + return new ByteSize(Convert.ToInt64(bytesPerSecond)); + } + } +} diff --git a/Tests/CodexTests/UtilityTests/DiscordBotTests.cs b/Tests/CodexTests/UtilityTests/DiscordBotTests.cs new file mode 100644 index 00000000..2896dbb7 --- /dev/null +++ b/Tests/CodexTests/UtilityTests/DiscordBotTests.cs @@ -0,0 +1,376 @@ +using CodexContractsPlugin; +using CodexDiscordBotPlugin; +using CodexPlugin; +using Core; +using DiscordRewards; +using DistTestCore; +using GethPlugin; +using KubernetesWorkflow.Types; +using Logging; +using Newtonsoft.Json; +using NUnit.Framework; +using Utils; + +namespace CodexTests.UtilityTests +{ + [TestFixture] + public class DiscordBotTests : AutoBootstrapDistTest + { + private readonly RewardRepo repo = new RewardRepo(); + private readonly TestToken hostInitialBalance = 3000000.TstWei(); + private readonly TestToken clientInitialBalance = 1000000000.TstWei(); + private readonly EthAccount clientAccount = EthAccount.GenerateNew(); + private readonly List hostAccounts = new List(); + private readonly List rewardsSeen = new List(); + private readonly TimeSpan rewarderInterval = TimeSpan.FromMinutes(1); + private readonly List receivedEvents = new List(); + private readonly List receivedAverages = new List(); + + [Test] + [DontDownloadLogs] + [Ignore("Used to debug testnet bots.")] + public void BotRewardTest() + { + var geth = Ci.StartGethNode(s => s.IsMiner().WithName("disttest-geth")); + var contracts = Ci.StartCodexContracts(geth); + var gethInfo = CreateGethInfo(geth, contracts); + + var botContainer = StartDiscordBot(gethInfo); + var rewarderContainer = StartRewarderBot(gethInfo, botContainer); + + StartHosts(geth, contracts); + var client = StartClient(geth, contracts); + + var apiCalls = new RewardApiCalls(GetTestLog(), Ci, botContainer); + apiCalls.Start(OnCommand); + + var purchaseContract = ClientPurchasesStorage(client); + purchaseContract.WaitForStorageContractStarted(); + purchaseContract.WaitForStorageContractFinished(); + Thread.Sleep(rewarderInterval * 3); + + apiCalls.Stop(); + + AssertEventOccurance("Created as New.", 1); + AssertEventOccurance("SlotFilled", Convert.ToInt32(GetNumberOfRequiredHosts())); + AssertEventOccurance("Transit: New -> Started", 1); + AssertEventOccurance("Transit: Started -> Finished", 1); + + AssertMarketAverage(); + + foreach (var r in repo.Rewards) + { + var seen = rewardsSeen.Any(s => r.RoleId == s); + + Log($"{Lookup(r.RoleId)} = {seen}"); + } + + Assert.That(repo.Rewards.All(r => rewardsSeen.Contains(r.RoleId))); + } + + private string Lookup(ulong rewardId) + { + var reward = repo.Rewards.Single(r => r.RoleId == rewardId); + return $"({rewardId})'{reward.Message}'"; + } + + private void AssertEventOccurance(string msg, int expectedCount) + { + Assert.That(receivedEvents.Count(e => e.Contains(msg)), Is.EqualTo(expectedCount), + $"Event '{msg}' did not occure correct number of times."); + } + + private void AssertMarketAverage() + { + Assert.That(receivedAverages.Count, Is.EqualTo(1)); + var a = receivedAverages.Single(); + + Assert.That(a.NumberOfFinished, Is.EqualTo(1)); + Assert.That(a.TimeRangeSeconds, Is.EqualTo(5760)); + Assert.That(a.Price, Is.EqualTo(2.0f).Within(0.1f)); + Assert.That(a.Size, Is.EqualTo(GetMinFileSize().SizeInBytes).Within(1.0f)); + Assert.That(a.Duration, Is.EqualTo(GetMinRequiredRequestDuration().TotalSeconds).Within(1.0f)); + Assert.That(a.Collateral, Is.EqualTo(10.0f).Within(0.1f)); + Assert.That(a.ProofProbability, Is.EqualTo(5.0f).Within(0.1f)); + } + + private void OnCommand(string timestamp, GiveRewardsCommand call) + { + Log($""); + receivedAverages.AddRange(call.Averages); + foreach (var a in call.Averages) + { + Log("\tAverage: " + JsonConvert.SerializeObject(a)); + } + receivedEvents.AddRange(call.EventsOverview); + foreach (var e in call.EventsOverview) + { + Log("\tEvent: " + e); + } + foreach (var r in call.Rewards) + { + var reward = repo.Rewards.Single(a => a.RoleId == r.RewardId); + if (r.UserAddresses.Any()) rewardsSeen.Add(reward.RoleId); + foreach (var address in r.UserAddresses) + { + var user = IdentifyAccount(address); + Log("\tReward: " + user + ": " + reward.Message); + } + } + Log($""); + } + + private IStoragePurchaseContract ClientPurchasesStorage(ICodexNode client) + { + var testFile = GenerateTestFile(GetMinFileSize()); + var contentId = client.UploadFile(testFile); + var purchase = new StoragePurchaseRequest(contentId) + { + PricePerSlotPerSecond = 2.TstWei(), + RequiredCollateral = 10.TstWei(), + MinRequiredNumberOfNodes = GetNumberOfRequiredHosts(), + NodeFailureTolerance = 2, + ProofProbability = 5, + Duration = GetMinRequiredRequestDuration(), + Expiry = GetMinRequiredRequestDuration() - TimeSpan.FromMinutes(1) + }; + + return client.Marketplace.RequestStorage(purchase); + } + + private ICodexNode StartClient(IGethNode geth, ICodexContracts contracts) + { + var node = StartCodex(s => s + .WithName("Client") + .EnableMarketplace(geth, contracts, m => m + .WithAccount(clientAccount) + .WithInitial(10.Eth(), clientInitialBalance))); + + Log($"Client {node.EthAccount.EthAddress}"); + return node; + } + + private RunningPod StartRewarderBot(DiscordBotGethInfo gethInfo, RunningContainer botContainer) + { + return Ci.DeployRewarderBot(new RewarderBotStartupConfig( + name: "rewarder-bot", + discordBotHost: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Host, + discordBotPort: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Port, + intervalMinutes: Convert.ToInt32(Math.Round(rewarderInterval.TotalMinutes)), + historyStartUtc: DateTime.UtcNow, + gethInfo: gethInfo, + dataPath: null + )); + } + + private DiscordBotGethInfo CreateGethInfo(IGethNode geth, ICodexContracts contracts) + { + return new DiscordBotGethInfo( + host: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Host, + port: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Port, + privKey: geth.StartResult.Account.PrivateKey, + marketplaceAddress: contracts.Deployment.MarketplaceAddress, + tokenAddress: contracts.Deployment.TokenAddress, + abi: contracts.Deployment.Abi + ); + } + + private RunningContainer StartDiscordBot(DiscordBotGethInfo gethInfo) + { + var bot = Ci.DeployCodexDiscordBot(new DiscordBotStartupConfig( + name: "discord-bot", + token: "aaa", + serverName: "ThatBen's server", + adminRoleName: "bottest-admins", + adminChannelName: "admin-channel", + rewardChannelName: "rewards-channel", + kubeNamespace: "notneeded", + gethInfo: gethInfo + )); + return bot.Containers.Single(); + } + + private void StartHosts(IGethNode geth, ICodexContracts contracts) + { + var hosts = StartCodex(GetNumberOfLiveHosts(), s => s + .WithName("Host") + .WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn) + { + ContractClock = CodexLogLevel.Trace, + }) + .WithStorageQuota(Mult(GetMinFileSizePlus(50), GetNumberOfLiveHosts())) + .EnableMarketplace(geth, contracts, m => m + .WithInitial(10.Eth(), hostInitialBalance) + .AsStorageNode() + .AsValidator())); + + var availability = new StorageAvailability( + totalSpace: Mult(GetMinFileSize(), GetNumberOfLiveHosts()), + maxDuration: TimeSpan.FromMinutes(30), + minPriceForTotalSpace: 1.TstWei(), + maxCollateral: hostInitialBalance + ); + + foreach (var host in hosts) + { + hostAccounts.Add(host.EthAccount); + host.Marketplace.MakeStorageAvailable(availability); + } + } + + private int GetNumberOfLiveHosts() + { + return Convert.ToInt32(GetNumberOfRequiredHosts()) + 3; + } + + private ByteSize Mult(ByteSize size, int mult) + { + return new ByteSize(size.SizeInBytes * mult); + } + + private ByteSize GetMinFileSizePlus(int plusMb) + { + return new ByteSize(GetMinFileSize().SizeInBytes + plusMb.MB().SizeInBytes); + } + + private ByteSize GetMinFileSize() + { + ulong minSlotSize = 0; + ulong minNumHosts = 0; + foreach (var r in repo.Rewards) + { + var s = Convert.ToUInt64(r.CheckConfig.MinSlotSize.SizeInBytes); + var h = r.CheckConfig.MinNumberOfHosts; + if (s > minSlotSize) minSlotSize = s; + if (h > minNumHosts) minNumHosts = h; + } + + var minFileSize = ((minSlotSize + 1024) * minNumHosts); + return new ByteSize(Convert.ToInt64(minFileSize)); + } + + private uint GetNumberOfRequiredHosts() + { + return Convert.ToUInt32(repo.Rewards.Max(r => r.CheckConfig.MinNumberOfHosts)); + } + + private TimeSpan GetMinRequiredRequestDuration() + { + return repo.Rewards.Max(r => r.CheckConfig.MinDuration) + TimeSpan.FromSeconds(10); + } + + private string IdentifyAccount(string address) + { + if (address == clientAccount.EthAddress.Address) return "Client"; + try + { + var index = hostAccounts.FindIndex(a => a.EthAddress.Address == address); + return "Host" + index; + } + catch + { + return "UNKNOWN"; + } + } + + public class RewardApiCalls + { + private readonly ContainerFileMonitor monitor; + + public RewardApiCalls(ILog log, CoreInterface ci, RunningContainer botContainer) + { + monitor = new ContainerFileMonitor(log, ci, botContainer, "/app/datapath/logs/discordbot.log"); + } + + public void Start(Action onCommand) + { + monitor.Start(line => ParseLine(line, onCommand)); + } + + public void Stop() + { + monitor.Stop(); + } + + private void ParseLine(string line, Action onCommand) + { + try + { + var timestamp = line.Substring(0, 30); + var json = line.Substring(31); + + var cmd = JsonConvert.DeserializeObject(json); + if (cmd != null) + { + onCommand(timestamp, cmd); + } + } + catch + { + } + } + } + + public class ContainerFileMonitor + { + private readonly ILog log; + private readonly CoreInterface ci; + private readonly RunningContainer botContainer; + private readonly string filePath; + private readonly CancellationTokenSource cts = new CancellationTokenSource(); + private readonly List seenLines = new List(); + private Task worker = Task.CompletedTask; + private Action onNewLine = c => { }; + + public ContainerFileMonitor(ILog log, CoreInterface ci, RunningContainer botContainer, string filePath) + { + this.log = log; + this.ci = ci; + this.botContainer = botContainer; + this.filePath = filePath; + } + + public void Start(Action onNewLine) + { + this.onNewLine = onNewLine; + worker = Task.Run(Worker); + } + + public void Stop() + { + cts.Cancel(); + worker.Wait(); + } + + // did any container crash? that's why it repeats? + + + private void Worker() + { + while (!cts.IsCancellationRequested) + { + Update(); + } + } + + private void Update() + { + Thread.Sleep(TimeSpan.FromSeconds(10)); + if (cts.IsCancellationRequested) return; + + var botLog = ci.ExecuteContainerCommand(botContainer, "cat", filePath); + var lines = botLog.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) + { + // log.Log("line: " + line); + + if (!seenLines.Contains(line)) + { + seenLines.Add(line); + onNewLine(line); + } + } + } + } + } +} diff --git a/Tests/CodexTests/UtilityTests/LogHelperTests.cs b/Tests/CodexTests/UtilityTests/LogHelperTests.cs new file mode 100644 index 00000000..acc73d34 --- /dev/null +++ b/Tests/CodexTests/UtilityTests/LogHelperTests.cs @@ -0,0 +1,56 @@ +using CodexPlugin; +using NUnit.Framework; +using Utils; + +namespace CodexTests.UtilityTests +{ + [TestFixture] + public class LogHelperTests : AutoBootstrapDistTest + { + [Test] + [Ignore("Used to find the most common log messages.")] + public void FindMostCommonLogMessages() + { + var uploader = StartCodex(s => s.WithName("uploader").WithLogLevel(CodexLogLevel.Trace)); + var downloader = StartCodex(s => s.WithName("downloader").WithLogLevel(CodexLogLevel.Trace)); + + var cid = uploader.UploadFile(GenerateTestFile(100.MB())); + + Thread.Sleep(1000); + var logStartUtc = DateTime.UtcNow; + Thread.Sleep(1000); + + downloader.DownloadContent(cid); + + var map = GetLogMap(downloader, logStartUtc).OrderByDescending(p => p.Value); + Log("Downloader - Receive"); + foreach (var entry in map) + { + if (entry.Value > 9) + { + Log($"'{entry.Key}' = {entry.Value}"); + } + } + } + + private Dictionary GetLogMap(ICodexNode node, DateTime? startUtc = null) + { + var log = Ci.DownloadLog(node); + var map = new Dictionary(); + log.IterateLines(line => + { + var log = CodexLogLine.Parse(line); + if (log == null) return; + + if (startUtc.HasValue) + { + if (log.TimestampUtc < startUtc) return; + } + + if (map.ContainsKey(log.Message)) map[log.Message] += 1; + else map.Add(log.Message, 1); + }); + return map; + } + } +} diff --git a/Tests/CodexTests/BasicTests/NetworkIsolationTest.cs b/Tests/CodexTests/UtilityTests/NetworkIsolationTest.cs similarity index 89% rename from Tests/CodexTests/BasicTests/NetworkIsolationTest.cs rename to Tests/CodexTests/UtilityTests/NetworkIsolationTest.cs index ccacdba4..c100143c 100644 --- a/Tests/CodexTests/BasicTests/NetworkIsolationTest.cs +++ b/Tests/CodexTests/UtilityTests/NetworkIsolationTest.cs @@ -3,7 +3,7 @@ using DistTestCore; using NUnit.Framework; using Utils; -namespace CodexTests.BasicTests +namespace CodexTests.UtilityTests { // Warning! // This is a test to check network-isolation in the test-infrastructure. @@ -19,7 +19,7 @@ namespace CodexTests.BasicTests { node = Ci.StartCodexNode(); - Time.WaitUntil(() => node == null, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(5)); + Time.WaitUntil(() => node == null, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(5), nameof(SetUpANodeAndWait)); } [Test] @@ -27,7 +27,7 @@ namespace CodexTests.BasicTests { var myNode = Ci.StartCodexNode(); - Time.WaitUntil(() => node != null, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5)); + Time.WaitUntil(() => node != null, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5), nameof(ForeignNodeConnects)); try { diff --git a/Tests/DistTestCore/Configuration.cs b/Tests/DistTestCore/Configuration.cs index b1a94da5..3fa34b5e 100644 --- a/Tests/DistTestCore/Configuration.cs +++ b/Tests/DistTestCore/Configuration.cs @@ -24,6 +24,9 @@ namespace DistTestCore this.dataFilesPath = dataFilesPath; } + /// + /// Does not override [DontDownloadLogs] attribute. + /// public bool AlwaysDownloadContainerLogs { get; set; } public KubernetesWorkflow.Configuration GetK8sConfiguration(ITimeSet timeSet, string k8sNamespace) @@ -36,7 +39,7 @@ namespace DistTestCore var config = new KubernetesWorkflow.Configuration( kubeConfigFile: kubeConfigFile, operationTimeout: timeSet.K8sOperationTimeout(), - retryDelay: timeSet.WaitForK8sServiceDelay(), + retryDelay: timeSet.K8sOperationRetryDelay(), kubernetesNamespace: k8sNamespace ); diff --git a/Tests/DistTestCore/DistTest.cs b/Tests/DistTestCore/DistTest.cs index 76f9d5bc..7b1d09e1 100644 --- a/Tests/DistTestCore/DistTest.cs +++ b/Tests/DistTestCore/DistTest.cs @@ -52,7 +52,7 @@ namespace DistTestCore { Stopwatch.Measure(fixtureLog, "Global setup", () => { - globalEntryPoint.Tools.CreateWorkflow().DeleteNamespacesStartingWith(TestNamespacePrefix); + globalEntryPoint.Tools.CreateWorkflow().DeleteNamespacesStartingWith(TestNamespacePrefix, wait: true); }); } catch (Exception ex) @@ -72,7 +72,8 @@ namespace DistTestCore globalEntryPoint.Decommission( // There shouldn't be any of either, but clean everything up regardless. deleteKubernetesResources: true, - deleteTrackedFiles: true + deleteTrackedFiles: true, + waitTillDone: true ); } @@ -98,7 +99,7 @@ namespace DistTestCore } catch (Exception ex) { - fixtureLog.Error("Cleanup failed: " + ex.Message); + fixtureLog.Error("Cleanup failed: " + ex); GlobalTestFailure.HasFailed = true; } } @@ -185,7 +186,13 @@ namespace DistTestCore lock (lifecycleLock) { var testNamespace = TestNamespacePrefix + Guid.NewGuid().ToString(); - var lifecycle = new TestLifecycle(fixtureLog.CreateTestLog(), configuration, GetTimeSet(), testNamespace, deployId); + var lifecycle = new TestLifecycle( + fixtureLog.CreateTestLog(), + configuration, + GetTimeSet(), + testNamespace, + deployId, + ShouldWaitForCleanup()); lifecycles.Add(testName, lifecycle); LifecycleStart(lifecycle); } @@ -208,7 +215,7 @@ namespace DistTestCore IncludeLogsOnTestFailure(lifecycle); LifecycleStop(lifecycle); lifecycle.DeleteAllResources(); - lifecycle = null!; + lifecycles.Remove(GetCurrentTestName()); }); } @@ -235,10 +242,25 @@ namespace DistTestCore return new DefaultTimeSet(); } + private bool ShouldWaitForCleanup() + { + return CurrentTestMethodHasAttribute(); + } + private bool ShouldUseLongTimeouts() + { + return CurrentTestMethodHasAttribute(); + } + + private bool HasDontDownloadAttribute() + { + return CurrentTestMethodHasAttribute(); + } + + private bool CurrentTestMethodHasAttribute() where T : PropertyAttribute { // Don't be fooled! TestContext.CurrentTest.Test allows you easy access to the attributes of the current test. - // But this doesn't work for tests making use of [TestCase]. So instead, we use reflection here to figure out + // But this doesn't work for tests making use of [TestCase] or [Combinatorial]. So instead, we use reflection here to figure out // if the attribute is present. var currentTest = TestContext.CurrentContext.Test; var className = currentTest.ClassName; @@ -247,7 +269,7 @@ namespace DistTestCore var testClasses = testAssemblies.SelectMany(a => a.GetTypes()).Where(c => c.FullName == className).ToArray(); var testMethods = testClasses.SelectMany(c => c.GetMethods()).Where(m => m.Name == methodName).ToArray(); - return testMethods.Any(m => m.GetCustomAttribute() != null); + return testMethods.Any(m => m.GetCustomAttribute() != null); } private void IncludeLogsOnTestFailure(TestLifecycle lifecycle) @@ -268,9 +290,10 @@ namespace DistTestCore private bool ShouldDownloadAllLogs(TestStatus testStatus) { if (configuration.AlwaysDownloadContainerLogs) return true; + if (!IsDownloadingLogsEnabled()) return false; if (testStatus == TestStatus.Failed) { - return IsDownloadingLogsEnabled(); + return true; } return false; @@ -288,8 +311,7 @@ namespace DistTestCore private bool IsDownloadingLogsEnabled() { - var testProperties = TestContext.CurrentContext.Test.Properties; - return !testProperties.ContainsKey(DontDownloadLogsOnFailureAttribute.DontDownloadKey); + return !HasDontDownloadAttribute(); } } diff --git a/Tests/DistTestCore/DontDownloadLogsOnFailureAttribute.cs b/Tests/DistTestCore/DontDownloadLogsAttribute.cs similarity index 67% rename from Tests/DistTestCore/DontDownloadLogsOnFailureAttribute.cs rename to Tests/DistTestCore/DontDownloadLogsAttribute.cs index 800b35b8..13baab8f 100644 --- a/Tests/DistTestCore/DontDownloadLogsOnFailureAttribute.cs +++ b/Tests/DistTestCore/DontDownloadLogsAttribute.cs @@ -3,11 +3,11 @@ namespace DistTestCore { [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - public class DontDownloadLogsOnFailureAttribute : PropertyAttribute + public class DontDownloadLogsAttribute : PropertyAttribute { public const string DontDownloadKey = "DontDownloadLogs"; - public DontDownloadLogsOnFailureAttribute() + public DontDownloadLogsAttribute() : base(DontDownloadKey) { } diff --git a/Tests/DistTestCore/Helpers/AssertHelpers.cs b/Tests/DistTestCore/Helpers/AssertHelpers.cs index efd0749c..c0f995d3 100644 --- a/Tests/DistTestCore/Helpers/AssertHelpers.cs +++ b/Tests/DistTestCore/Helpers/AssertHelpers.cs @@ -14,7 +14,7 @@ namespace DistTestCore.Helpers Time.WaitUntil(() => { var c = constraint.Resolve(); return c.ApplyTo(actual()).IsSuccess; - }); + }, "RetryAssert: " + message); } catch (TimeoutException) { diff --git a/Tests/DistTestCore/TestLifecycle.cs b/Tests/DistTestCore/TestLifecycle.cs index 50cb5fb2..4191cd83 100644 --- a/Tests/DistTestCore/TestLifecycle.cs +++ b/Tests/DistTestCore/TestLifecycle.cs @@ -13,10 +13,10 @@ namespace DistTestCore private const string TestsType = "dist-tests"; private readonly EntryPoint entryPoint; private readonly Dictionary metadata; - private readonly List runningContainers = new(); + private readonly List runningContainers = new(); private readonly string deployId; - public TestLifecycle(TestLog log, Configuration configuration, ITimeSet timeSet, string testNamespace, string deployId) + public TestLifecycle(TestLog log, Configuration configuration, ITimeSet timeSet, string testNamespace, string deployId, bool waitForCleanup) { Log = log; Configuration = configuration; @@ -27,7 +27,7 @@ namespace DistTestCore metadata = entryPoint.GetPluginMetadata(); CoreInterface = entryPoint.CreateInterface(); this.deployId = deployId; - + WaitForCleanup = waitForCleanup; log.WriteLogTag(); } @@ -35,13 +35,15 @@ namespace DistTestCore public TestLog Log { get; } public Configuration Configuration { get; } public ITimeSet TimeSet { get; } + public bool WaitForCleanup { get; } public CoreInterface CoreInterface { get; } public void DeleteAllResources() { entryPoint.Decommission( deleteKubernetesResources: true, - deleteTrackedFiles: true + deleteTrackedFiles: true, + waitTillDone: WaitForCleanup ); } @@ -65,12 +67,12 @@ namespace DistTestCore return DateTime.UtcNow - TestStart; } - public void OnContainersStarted(RunningContainers rc) + public void OnContainersStarted(RunningPod rc) { runningContainers.Add(rc); } - public void OnContainersStopped(RunningContainers rc) + public void OnContainersStopped(RunningPod rc) { runningContainers.Remove(rc); } @@ -93,13 +95,20 @@ namespace DistTestCore public void DownloadAllLogs() { - foreach (var rc in runningContainers) + try { - foreach (var c in rc.Containers) + foreach (var rc in runningContainers) { - CoreInterface.DownloadLog(c); + foreach (var c in rc.Containers) + { + CoreInterface.DownloadLog(c); + } } } + catch (Exception ex) + { + Log.Error("Exception during log download: " + ex); + } } } } diff --git a/Tests/DistTestCore/WaitForCleanupAttribute.cs b/Tests/DistTestCore/WaitForCleanupAttribute.cs new file mode 100644 index 00000000..928e3c20 --- /dev/null +++ b/Tests/DistTestCore/WaitForCleanupAttribute.cs @@ -0,0 +1,15 @@ +using NUnit.Framework; + +namespace DistTestCore +{ + /// + /// By default, test system does not wait until all resources are destroyed before starting the + /// next test. This saves a lot of time but it's not always what you want. + /// If you want to be sure the resources of your test are destroyed before the next test starts, + /// add this attribute to your test method. + /// + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] + public class WaitForCleanupAttribute : PropertyAttribute + { + } +} diff --git a/Tests/FrameworkTests/CodexContractsPlugin/TestTokenTests.cs b/Tests/FrameworkTests/CodexContractsPlugin/TestTokenTests.cs new file mode 100644 index 00000000..b74ac86e --- /dev/null +++ b/Tests/FrameworkTests/CodexContractsPlugin/TestTokenTests.cs @@ -0,0 +1,46 @@ +using CodexContractsPlugin; +using NUnit.Framework; +using System.Numerics; + +namespace FrameworkTests.CodexContractsPlugin +{ + [TestFixture] + public class TestTokenTests + { + private const decimal factor = 1000000000000000000m; + + [Test] + public void RepresentsSmallAmount() + { + var t = 10.TstWei(); + + Assert.That(t.TstWei, Is.EqualTo(new BigInteger(10))); + Assert.That(t.Tst, Is.EqualTo(new BigInteger(0))); + Assert.That(t.ToString(), Is.EqualTo("10 TSTWEI")); + } + + [Test] + public void RepresentsLargeAmount() + { + var t = 10.Tst(); + + var expected = new BigInteger(10 * factor); + Assert.That(t.TstWei, Is.EqualTo(expected)); + Assert.That(t.Tst, Is.EqualTo(new BigInteger(10))); + Assert.That(t.ToString(), Is.EqualTo("10 TST")); + } + + [Test] + public void RepresentsLongAmount() + { + var a = 10.Tst(); + var b = 20.TstWei(); + var t = a + b; + + var expected = new BigInteger((10 * factor) + 20); + Assert.That(t.TstWei, Is.EqualTo(expected)); + Assert.That(t.Tst, Is.EqualTo(new BigInteger(10))); + Assert.That(t.ToString(), Is.EqualTo("10 TST + 20 TSTWEI")); + } + } +} diff --git a/Tests/FrameworkTests/FrameworkTests.csproj b/Tests/FrameworkTests/FrameworkTests.csproj index f22fe575..4bc09aa5 100644 --- a/Tests/FrameworkTests/FrameworkTests.csproj +++ b/Tests/FrameworkTests/FrameworkTests.csproj @@ -16,6 +16,7 @@ + diff --git a/Tools/BiblioTech/AdminChecker.cs b/Tools/BiblioTech/AdminChecker.cs index e1c82cda..def2870b 100644 --- a/Tools/BiblioTech/AdminChecker.cs +++ b/Tools/BiblioTech/AdminChecker.cs @@ -24,7 +24,7 @@ namespace BiblioTech public bool IsAdminChannel(IChannel channel) { - return channel.Name == Program.Config.AdminChannelName; + return channel.Id == Program.Config.AdminChannelId; } public ISocketMessageChannel GetAdminChannel() @@ -45,7 +45,7 @@ namespace BiblioTech private void UpdateAdminIds() { lastUpdate = DateTime.UtcNow; - var adminRole = guild.Roles.Single(r => r.Name == Program.Config.AdminRoleName); + var adminRole = guild.Roles.Single(r => r.Id == Program.Config.AdminRoleId); adminIds = adminRole.Members.Select(m => m.Id).ToArray(); } } diff --git a/Tools/BiblioTech/CommandHandler.cs b/Tools/BiblioTech/CommandHandler.cs index 79071773..29a17774 100644 --- a/Tools/BiblioTech/CommandHandler.cs +++ b/Tools/BiblioTech/CommandHandler.cs @@ -3,33 +3,37 @@ using Discord.WebSocket; using Discord; using Newtonsoft.Json; using BiblioTech.Rewards; +using Logging; namespace BiblioTech { public class CommandHandler { private readonly DiscordSocketClient client; + private readonly CustomReplacement replacement; private readonly BaseCommand[] commands; + private readonly ILog log; - public CommandHandler(DiscordSocketClient client, params BaseCommand[] commands) + public CommandHandler(ILog log, DiscordSocketClient client, CustomReplacement replacement, params BaseCommand[] commands) { this.client = client; + this.replacement = replacement; this.commands = commands; - + this.log = log; client.Ready += Client_Ready; client.SlashCommandExecuted += SlashCommandHandler; } private async Task Client_Ready() { - var guild = client.Guilds.Single(g => g.Name == Program.Config.ServerName); + var guild = client.Guilds.Single(g => g.Id == Program.Config.ServerId); Program.AdminChecker.SetGuild(guild); - Program.Log.Log($"Initializing for guild: '{guild.Name}'"); + log.Log($"Initializing for guild: '{guild.Name}'"); var adminChannels = guild.TextChannels.Where(Program.AdminChecker.IsAdminChannel).ToArray(); if (adminChannels == null || !adminChannels.Any()) throw new Exception("No admin message channel"); Program.AdminChecker.SetAdminChannel(adminChannels.First()); - Program.RoleDriver = new RoleDriver(client); + Program.RoleDriver = new RoleDriver(client, log, replacement); var builders = commands.Select(c => { @@ -44,7 +48,7 @@ namespace BiblioTech builder.AddOption(option.Build()); } - Program.Log.Log(msg); + log.Log(msg); return builder; }); @@ -58,7 +62,7 @@ namespace BiblioTech catch (HttpException exception) { var json = JsonConvert.SerializeObject(exception.Errors, Formatting.Indented); - Program.Log.Error(json); + log.Error(json); } } diff --git a/Tools/BiblioTech/Commands/AdminCommand.cs b/Tools/BiblioTech/Commands/AdminCommand.cs index 698fb006..d80326fb 100644 --- a/Tools/BiblioTech/Commands/AdminCommand.cs +++ b/Tools/BiblioTech/Commands/AdminCommand.cs @@ -1,4 +1,5 @@ using BiblioTech.Options; +using BiblioTech.Rewards; namespace BiblioTech.Commands { @@ -10,12 +11,14 @@ namespace BiblioTech.Commands private readonly AddSprCommand addSprCommand; private readonly ClearSprsCommand clearSprsCommand; private readonly GetSprCommand getSprCommand; + private readonly LogReplaceCommand logReplaceCommand; - public AdminCommand(SprCommand sprCommand) + public AdminCommand(SprCommand sprCommand, CustomReplacement replacement) { addSprCommand = new AddSprCommand(sprCommand); clearSprsCommand = new ClearSprsCommand(sprCommand); getSprCommand = new GetSprCommand(sprCommand); + logReplaceCommand = new LogReplaceCommand(replacement); } public override string Name => "admin"; @@ -29,7 +32,8 @@ namespace BiblioTech.Commands whoIsCommand, addSprCommand, clearSprsCommand, - getSprCommand + getSprCommand, + logReplaceCommand }; protected override async Task Invoke(CommandContext context) @@ -52,6 +56,7 @@ namespace BiblioTech.Commands await addSprCommand.CommandHandler(context); await clearSprsCommand.CommandHandler(context); await getSprCommand.CommandHandler(context); + await logReplaceCommand.CommandHandler(context); } public class ClearUserAssociationCommand : SubCommandOption @@ -194,7 +199,7 @@ namespace BiblioTech.Commands } } - public class GetSprCommand: SubCommandOption + public class GetSprCommand : SubCommandOption { private readonly SprCommand sprCommand; @@ -210,5 +215,56 @@ namespace BiblioTech.Commands await context.Followup("SPRs: " + string.Join(", ", sprCommand.Get().Select(s => $"'{s}'"))); } } + + public class LogReplaceCommand : SubCommandOption + { + private readonly CustomReplacement replacement; + private readonly StringOption fromOption = new StringOption("from", "string to replace", true); + private readonly StringOption toOption = new StringOption("to", "string to replace with", false); + + public LogReplaceCommand(CustomReplacement replacement) + : base(name: "logreplace", + description: "Replaces all occurances of 'from' with 'to' in ChainEvent messages. Leave 'to' empty to remove a replacement.") + { + this.replacement = replacement; + } + + public override CommandOption[] Options => new[] { fromOption, toOption }; + + protected override async Task onSubCommand(CommandContext context) + { + var from = await fromOption.Parse(context); + var to = await toOption.Parse(context); + + if (string.IsNullOrEmpty(from)) + { + await context.Followup("'from' not received"); + return; + } + + if (from.Length < 5) + { + await context.Followup("'from' must be length 5 or greater."); + return; + } + + if (string.IsNullOrEmpty(to)) + { + replacement.Remove(from); + await context.Followup($"Replace for '{from}' removed."); + } + else + { + if (to.Length < 5) + { + await context.Followup("'to' must be length 5 or greater."); + return; + } + + replacement.Add(from, to); + await context.Followup($"Replace added '{from}' -->> '{to}'."); + } + } + } } } diff --git a/Tools/BiblioTech/Commands/GetBalanceCommand.cs b/Tools/BiblioTech/Commands/GetBalanceCommand.cs index 964d7c27..9454c79d 100644 --- a/Tools/BiblioTech/Commands/GetBalanceCommand.cs +++ b/Tools/BiblioTech/Commands/GetBalanceCommand.cs @@ -31,8 +31,14 @@ namespace BiblioTech.Commands return; } - var eth = gethNode.GetEthBalance(addr); - var testTokens = contracts.GetTestTokenBalance(addr); + var eth = 0.Eth(); + var testTokens = 0.TstWei(); + + await Task.Run(() => + { + eth = gethNode.GetEthBalance(addr); + testTokens = contracts.GetTestTokenBalance(addr); + }); await context.Followup($"{context.Command.User.Username} has {eth} and {testTokens}."); } diff --git a/Tools/BiblioTech/Commands/MarketCommand.cs b/Tools/BiblioTech/Commands/MarketCommand.cs new file mode 100644 index 00000000..0df58d5d --- /dev/null +++ b/Tools/BiblioTech/Commands/MarketCommand.cs @@ -0,0 +1,58 @@ +using BiblioTech.Options; +using DiscordRewards; +using System.Globalization; +using Utils; + +namespace BiblioTech.Commands +{ + public class MarketCommand : BaseCommand + { + public override string Name => "market"; + public override string StartingMessage => RandomBusyMessage.Get(); + public override string Description => "Fetch some insights about current market conditions."; + + protected override async Task Invoke(CommandContext context) + { + await context.Followup(GetInsights()); + } + + private string[] GetInsights() + { + var result = Program.Averages.SelectMany(GetInsight).ToArray(); + if (result.Length > 0) + { + result = new[] + { + "No market insights available." + }; + } + return result; + } + + private string[] GetInsight(MarketAverage avg) + { + var timeRange = TimeSpan.FromSeconds(avg.TimeRangeSeconds); + var headerLine = $"[Last {Time.FormatDuration(timeRange)}] ({avg.NumberOfFinished} Contracts finished)"; + + if (avg.NumberOfFinished == 0) + { + return new[] { headerLine }; + } + + return new[] + { + headerLine, + $"Price: {Format(avg.Price)}", + $"Size: {Format(avg.Size)}", + $"Duration: {Format(avg.Duration)}", + $"Collateral: {Format(avg.Collateral)}", + $"ProofProbability: {Format(avg.ProofProbability)}" + }; + } + + private string Format(float f) + { + return f.ToString("F3", CultureInfo.InvariantCulture); + } + } +} diff --git a/Tools/BiblioTech/Commands/MintCommand.cs b/Tools/BiblioTech/Commands/MintCommand.cs index faf62c8b..d3d24519 100644 --- a/Tools/BiblioTech/Commands/MintCommand.cs +++ b/Tools/BiblioTech/Commands/MintCommand.cs @@ -33,8 +33,14 @@ namespace BiblioTech.Commands var report = new List(); - var sentEth = ProcessEth(gethNode, addr, report); - var mintedTokens = ProcessTokens(contracts, addr, report); + Transaction? sentEth = null; + Transaction? mintedTokens = null; + + await Task.Run(() => + { + sentEth = ProcessEth(gethNode, addr, report); + mintedTokens = ProcessTokens(contracts, addr, report); + }); Program.UserRepo.AddMintEventForUser(userId, addr, sentEth, mintedTokens); @@ -45,7 +51,7 @@ namespace BiblioTech.Commands { if (ShouldMintTestTokens(contracts, addr)) { - var tokens = Program.Config.MintTT.TestTokens(); + var tokens = Program.Config.MintTT.TstWei(); var transaction = contracts.MintTestTokens(addr, tokens); report.Add($"Minted {tokens} {FormatTransactionLink(transaction)}"); return new Transaction(tokens, transaction); @@ -71,7 +77,7 @@ namespace BiblioTech.Commands private bool ShouldMintTestTokens(ICodexContracts contracts, EthAddress addr) { var testTokens = contracts.GetTestTokenBalance(addr); - return testTokens.Amount < Program.Config.MintTT; + return testTokens < Program.Config.MintTT.TstWei(); } private bool ShouldSendEth(IGethNode gethNode, EthAddress addr) diff --git a/Tools/BiblioTech/Configuration.cs b/Tools/BiblioTech/Configuration.cs index 2183a540..6f27fd1f 100644 --- a/Tools/BiblioTech/Configuration.cs +++ b/Tools/BiblioTech/Configuration.cs @@ -1,4 +1,5 @@ using ArgsUniform; +using System.Numerics; namespace BiblioTech { @@ -7,52 +8,39 @@ namespace BiblioTech [Uniform("token", "t", "TOKEN", true, "Discord Application Token")] public string ApplicationToken { get; set; } = string.Empty; - [Uniform("server-name", "sn", "SERVERNAME", true, "Name of the Discord server")] - public string ServerName { get; set; } = string.Empty; + [Uniform("server-id", "sn", "SERVERID", true, "ID of the Discord server")] + public ulong ServerId { get; set; } - [Uniform("datapath", "dp", "DATAPATH", false, "Root path where all data files will be saved.")] + [Uniform("datapath", "dp", "DATAPATH", true, "Root path where all data files will be saved.")] public string DataPath { get; set; } = "datapath"; - [Uniform("admin-role", "a", "ADMINROLE", true, "Name of the Discord server admin role")] - public string AdminRoleName { get; set; } = string.Empty; + [Uniform("admin-role-id", "a", "ADMINROLEID", true, "ID of the Discord server admin role")] + public ulong AdminRoleId { get; set; } - [Uniform("admin-channel-name", "ac", "ADMINCHANNELNAME", true, "Name of the Discord server channel where admin commands are allowed.")] - public string AdminChannelName { get; set; } = "admin-channel"; + [Uniform("admin-channel-id", "ac", "ADMINCHANNELID", true, "ID of the Discord server channel where admin commands are allowed.")] + public ulong AdminChannelId{ get; set; } - [Uniform("rewards-channel-name", "rc", "REWARDSCHANNELNAME", false, "Name of the Discord server channel where participation rewards will be announced.")] - public string RewardsChannelName { get; set; } = ""; + [Uniform("rewards-channel-id", "rc", "REWARDSCHANNELID", false, "ID of the Discord server channel where participation rewards will be announced.")] + public ulong RewardsChannelId { get; set; } - [Uniform("reward-api-port", "rp", "REWARDAPIPORT", false, "TCP listen port for the reward API.")] + [Uniform("chain-events-channel-id", "cc", "CHAINEVENTSCHANNELID", false, "ID of the Discord server channel where chain events will be posted.")] + public ulong ChainEventsChannelId { get; set; } + + [Uniform("reward-api-port", "rp", "REWARDAPIPORT", true, "TCP listen port for the reward API.")] public int RewardApiPort { get; set; } = 31080; - [Uniform("send-eth", "se", "SENDETH", false, "Amount of Eth send by the mint command. Default: 10.")] + [Uniform("send-eth", "se", "SENDETH", true, "Amount of Eth send by the mint command.")] public int SendEth { get; set; } = 10; - [Uniform("mint-tt", "mt", "MINTTT", false, "Amount of TestTokens minted by the mint command. Default: 1073741824")] - public int MintTT { get; set; } = 1073741824; + [Uniform("mint-tt", "mt", "MINTTT", true, "Amount of TSTWEI minted by the mint command.")] + public BigInteger MintTT { get; set; } = 1073741824; - public string EndpointsPath - { - get - { - return Path.Combine(DataPath, "endpoints"); - } - } + [Uniform("no-discord", "nd", "NODISCORD", false, "For debugging: Bypasses all Discord API calls.")] + public int NoDiscord { get; set; } = 0; - public string UserDataPath - { - get - { - return Path.Combine(DataPath, "users"); - } - } - - public string LogPath - { - get - { - return Path.Combine(DataPath, "logs"); - } - } + public string EndpointsPath => Path.Combine(DataPath, "endpoints"); + public string UserDataPath => Path.Combine(DataPath, "users"); + public string LogPath => Path.Combine(DataPath, "logs"); + public bool DebugNoDiscord => NoDiscord == 1; } } diff --git a/Tools/BiblioTech/LoggingRoleDriver.cs b/Tools/BiblioTech/LoggingRoleDriver.cs new file mode 100644 index 00000000..9275a7a5 --- /dev/null +++ b/Tools/BiblioTech/LoggingRoleDriver.cs @@ -0,0 +1,24 @@ +using BiblioTech.Rewards; +using DiscordRewards; +using Logging; +using Newtonsoft.Json; + +namespace BiblioTech +{ + public class LoggingRoleDriver : IDiscordRoleDriver + { + private readonly ILog log; + + public LoggingRoleDriver(ILog log) + { + this.log = log; + } + + public async Task GiveRewards(GiveRewardsCommand rewards) + { + await Task.CompletedTask; + + log.Log(JsonConvert.SerializeObject(rewards, Formatting.None)); + } + } +} diff --git a/Tools/BiblioTech/Program.cs b/Tools/BiblioTech/Program.cs index 8f7c98ce..587c3a88 100644 --- a/Tools/BiblioTech/Program.cs +++ b/Tools/BiblioTech/Program.cs @@ -3,6 +3,7 @@ using BiblioTech.Commands; using BiblioTech.Rewards; using Discord; using Discord.WebSocket; +using DiscordRewards; using Logging; namespace BiblioTech @@ -10,15 +11,19 @@ namespace BiblioTech public class Program { private DiscordSocketClient client = null!; + private readonly CustomReplacement replacement = new CustomReplacement(); public static Configuration Config { get; private set; } = null!; public static UserRepo UserRepo { get; } = new UserRepo(); public static AdminChecker AdminChecker { get; private set; } = null!; public static IDiscordRoleDriver RoleDriver { get; set; } = null!; public static ILog Log { get; private set; } = null!; + public static MarketAverage[] Averages { get; set; } = Array.Empty(); public static Task Main(string[] args) { + Log = new ConsoleLog(); + var uniformArgs = new ArgsUniform(PrintHelp, args); Config = uniformArgs.Parse(); @@ -37,24 +42,15 @@ namespace BiblioTech public async Task MainAsync(string[] args) { Log.Log("Starting Codex Discord Bot..."); - client = new DiscordSocketClient(); - client.Log += ClientLog; - - var notifyCommand = new NotifyCommand(); - var associateCommand = new UserAssociateCommand(notifyCommand); - var sprCommand = new SprCommand(); - var handler = new CommandHandler(client, - new GetBalanceCommand(associateCommand), - new MintCommand(associateCommand), - sprCommand, - associateCommand, - notifyCommand, - new AdminCommand(sprCommand) - ); - - await client.LoginAsync(TokenType.Bot, Config.ApplicationToken); - await client.StartAsync(); - AdminChecker = new AdminChecker(); + if (Config.DebugNoDiscord) + { + Log.Log("Debug option is set. Discord connection disabled!"); + RoleDriver = new LoggingRoleDriver(Log); + } + else + { + await StartDiscordBot(); + } var builder = WebApplication.CreateBuilder(args); builder.WebHost.ConfigureKestrel((context, options) => @@ -70,6 +66,29 @@ namespace BiblioTech await Task.Delay(-1); } + private async Task StartDiscordBot() + { + client = new DiscordSocketClient(); + client.Log += ClientLog; + + var notifyCommand = new NotifyCommand(); + var associateCommand = new UserAssociateCommand(notifyCommand); + var sprCommand = new SprCommand(); + var handler = new CommandHandler(Log, client, replacement, + new GetBalanceCommand(associateCommand), + new MintCommand(associateCommand), + sprCommand, + associateCommand, + notifyCommand, + new AdminCommand(sprCommand, replacement), + new MarketCommand() + ); + + await client.LoginAsync(TokenType.Bot, Config.ApplicationToken); + await client.StartAsync(); + AdminChecker = new AdminChecker(); + } + private static void PrintHelp() { Log.Log("BiblioTech - Codex Discord Bot"); diff --git a/Tools/BiblioTech/Rewards/ChainEventsSender.cs b/Tools/BiblioTech/Rewards/ChainEventsSender.cs new file mode 100644 index 00000000..0ec88d11 --- /dev/null +++ b/Tools/BiblioTech/Rewards/ChainEventsSender.cs @@ -0,0 +1,72 @@ +using Discord.WebSocket; +using Logging; + +namespace BiblioTech.Rewards +{ + public class ChainEventsSender + { + private readonly ILog log; + private readonly CustomReplacement replacement; + private readonly SocketTextChannel? eventsChannel; + + public ChainEventsSender(ILog log, CustomReplacement replacement, SocketTextChannel? eventsChannel) + { + this.log = log; + this.replacement = replacement; + this.eventsChannel = eventsChannel; + } + + public async Task ProcessChainEvents(string[] eventsOverview) + { + if (eventsChannel == null || eventsOverview == null || !eventsOverview.Any()) return; + try + { + await Task.Run(async () => + { + var users = Program.UserRepo.GetAllUserData(); + + foreach (var e in eventsOverview) + { + if (!string.IsNullOrEmpty(e)) + { + var @event = ApplyReplacements(users, e); + await eventsChannel.SendMessageAsync(@event); + await Task.Delay(3000); + } + } + }); + } + catch (Exception ex) + { + log.Error("Failed to process chain events: " + ex); + } + } + + private string ApplyReplacements(UserData[] users, string msg) + { + var result = ApplyUserAddressReplacements(users, msg); + result = ApplyCustomReplacements(result); + return result; + } + + private string ApplyUserAddressReplacements(UserData[] users, string msg) + { + foreach (var user in users) + { + if (user.CurrentAddress != null && + !string.IsNullOrEmpty(user.CurrentAddress.Address) && + !string.IsNullOrEmpty(user.Name)) + { + msg = msg.Replace(user.CurrentAddress.Address, user.Name); + } + } + + return msg; + } + + private string ApplyCustomReplacements(string result) + { + return replacement.Apply(result); + } + } +} diff --git a/Tools/BiblioTech/Rewards/CustomReplacement.cs b/Tools/BiblioTech/Rewards/CustomReplacement.cs new file mode 100644 index 00000000..359411a9 --- /dev/null +++ b/Tools/BiblioTech/Rewards/CustomReplacement.cs @@ -0,0 +1,34 @@ +namespace BiblioTech.Rewards +{ + public class CustomReplacement + { + private readonly Dictionary replacements = new Dictionary(); + + public void Add(string from, string to) + { + if (replacements.ContainsKey(from)) + { + replacements[from] = to; + } + else + { + replacements.Add(from, to); + } + } + + public void Remove(string from) + { + replacements.Remove(from); + } + + public string Apply(string msg) + { + var result = msg; + foreach (var pair in replacements) + { + result.Replace(pair.Key, pair.Value); + } + return result; + } + } +} diff --git a/Tools/BiblioTech/Rewards/RewardContext.cs b/Tools/BiblioTech/Rewards/RewardContext.cs new file mode 100644 index 00000000..d05d5f6d --- /dev/null +++ b/Tools/BiblioTech/Rewards/RewardContext.cs @@ -0,0 +1,102 @@ +using Discord.WebSocket; +using Discord; +using DiscordRewards; + +namespace BiblioTech.Rewards +{ + public class RewardContext + { + private readonly Dictionary users; + private readonly Dictionary roles; + private readonly SocketTextChannel? rewardsChannel; + + public RewardContext(Dictionary users, Dictionary roles, SocketTextChannel? rewardsChannel) + { + this.users = users; + this.roles = roles; + this.rewardsChannel = rewardsChannel; + } + + public async Task ProcessGiveRewardsCommand(UserReward[] rewards) + { + foreach (var rewardCommand in rewards) + { + if (roles.ContainsKey(rewardCommand.RewardCommand.RewardId)) + { + var role = roles[rewardCommand.RewardCommand.RewardId]; + await ProcessRewardCommand(role, rewardCommand); + } + else + { + Program.Log.Error($"RoleID not found on guild: {rewardCommand.RewardCommand.RewardId}"); + } + } + } + + private async Task ProcessRewardCommand(RoleReward role, UserReward reward) + { + foreach (var user in reward.Users) + { + await GiveReward(role, user); + } + } + + private async Task GiveReward(RoleReward role, UserData user) + { + if (!users.ContainsKey(user.DiscordId)) + { + Program.Log.Log($"User by id '{user.DiscordId}' not found."); + return; + } + + var guildUser = users[user.DiscordId]; + + var alreadyHas = guildUser.RoleIds.ToArray(); + var logMessage = $"Giving reward '{role.SocketRole.Id}' to user '{user.DiscordId}'({user.Name})[" + + $"alreadyHas:{string.Join(",", alreadyHas.Select(a => a.ToString()))}]: "; + + + if (alreadyHas.Any(r => r == role.Reward.RoleId)) + { + logMessage += "Already has role"; + Program.Log.Log(logMessage); + return; + } + + await GiveRole(guildUser, role.SocketRole); + await SendNotification(role, user, guildUser); + await Task.Delay(1000); + logMessage += "Role given. Notification sent."; + Program.Log.Log(logMessage); + } + + private async Task GiveRole(IGuildUser user, SocketRole role) + { + try + { + Program.Log.Log($"Giving role {role.Name}={role.Id} to user {user.DisplayName}"); + await user.AddRoleAsync(role); + } + catch (Exception ex) + { + Program.Log.Error($"Failed to give role '{role.Name}' to user '{user.DisplayName}': {ex}"); + } + } + + private async Task SendNotification(RoleReward reward, UserData userData, IGuildUser user) + { + try + { + if (userData.NotificationsEnabled && rewardsChannel != null) + { + var msg = reward.Reward.Message.Replace(RewardConfig.UsernameTag, $"<@{user.Id}>"); + await rewardsChannel.SendMessageAsync(msg); + } + } + catch (Exception ex) + { + Program.Log.Error($"Failed to notify user '{user.DisplayName}' about role '{reward.SocketRole.Name}': {ex}"); + } + } + } +} diff --git a/Tools/BiblioTech/Rewards/RewardController.cs b/Tools/BiblioTech/Rewards/RewardController.cs index c9a19dea..99f73ee5 100644 --- a/Tools/BiblioTech/Rewards/RewardController.cs +++ b/Tools/BiblioTech/Rewards/RewardController.cs @@ -23,6 +23,10 @@ namespace BiblioTech.Rewards { try { + if (cmd.Averages != null && cmd.Averages.Any()) + { + Program.Averages = cmd.Averages; + } await Program.RoleDriver.GiveRewards(cmd); } catch (Exception ex) diff --git a/Tools/BiblioTech/Rewards/RoleDriver.cs b/Tools/BiblioTech/Rewards/RoleDriver.cs index 2478f520..71019b78 100644 --- a/Tools/BiblioTech/Rewards/RoleDriver.cs +++ b/Tools/BiblioTech/Rewards/RoleDriver.cs @@ -1,40 +1,68 @@ using Discord; using Discord.WebSocket; using DiscordRewards; +using Logging; +using Newtonsoft.Json; namespace BiblioTech.Rewards { public class RoleDriver : IDiscordRoleDriver { private readonly DiscordSocketClient client; + private readonly ILog log; private readonly SocketTextChannel? rewardsChannel; + private readonly ChainEventsSender eventsSender; private readonly RewardRepo repo = new RewardRepo(); - public RoleDriver(DiscordSocketClient client) + public RoleDriver(DiscordSocketClient client, ILog log, CustomReplacement replacement) { this.client = client; - - if (!string.IsNullOrEmpty(Program.Config.RewardsChannelName)) - { - rewardsChannel = GetGuild().TextChannels.SingleOrDefault(c => c.Name == Program.Config.RewardsChannelName); - } + this.log = log; + rewardsChannel = GetChannel(Program.Config.RewardsChannelId); + eventsSender = new ChainEventsSender(log, replacement, GetChannel(Program.Config.ChainEventsChannelId)); } public async Task GiveRewards(GiveRewardsCommand rewards) { - var guild = GetGuild(); - // We load all role and user information first, - // so we don't ask the server for the same info multiple times. - var context = new RewardContext( - await LoadAllUsers(guild), - LookUpAllRoles(guild, rewards), - rewardsChannel); + log.Log($"Processing rewards command: '{JsonConvert.SerializeObject(rewards)}'"); - await context.ProcessGiveRewardsCommand(LookUpUsers(rewards)); + if (rewards.Rewards.Any()) + { + await ProcessRewards(rewards); + } + + await eventsSender.ProcessChainEvents(rewards.EventsOverview); + } + + private async Task ProcessRewards(GiveRewardsCommand rewards) + { + try + { + var guild = GetGuild(); + // We load all role and user information first, + // so we don't ask the server for the same info multiple times. + var context = new RewardContext( + await LoadAllUsers(guild), + LookUpAllRoles(guild, rewards), + rewardsChannel); + + await context.ProcessGiveRewardsCommand(LookUpUsers(rewards)); + } + catch (Exception ex) + { + log.Error("Failed to process rewards: " + ex); + } + } + + private SocketTextChannel? GetChannel(ulong id) + { + if (id == 0) return null; + return GetGuild().TextChannels.SingleOrDefault(c => c.Id == id); } private async Task> LoadAllUsers(SocketGuild guild) { + log.Log("Loading all users.."); var result = new Dictionary(); var users = guild.GetUsersAsync(); await foreach (var ulist in users) @@ -42,6 +70,8 @@ namespace BiblioTech.Rewards foreach (var u in ulist) { result.Add(u.Id, u); + //var roleIds = string.Join(",", u.RoleIds.Select(r => r.ToString()).ToArray()); + //log.Log($" > {u.Id}({u.DisplayName}) has [{roleIds}]"); } } return result; @@ -57,14 +87,14 @@ namespace BiblioTech.Rewards var rewardConfig = repo.Rewards.SingleOrDefault(rr => rr.RoleId == r.RewardId); if (rewardConfig == null) { - Program.Log.Log($"No Reward is configured for id '{r.RewardId}'."); + log.Log($"No Reward is configured for id '{r.RewardId}'."); } else { var socketRole = guild.GetRole(r.RewardId); if (socketRole == null) { - Program.Log.Log($"Guild Role by id '{r.RewardId}' not found."); + log.Log($"Guild Role by id '{r.RewardId}' not found."); } else { @@ -96,22 +126,25 @@ namespace BiblioTech.Rewards { try { - return Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(address)); + var userData = Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(address)); + if (userData != null) log.Log($"User '{userData.Name}' was looked up."); + else log.Log($"Lookup for user was unsuccessful. EthAddress: '{address}'"); + return userData; } catch (Exception ex) { - Program.Log.Error("Error during UserData lookup: " + ex); + log.Error("Error during UserData lookup: " + ex); return null; } } private SocketGuild GetGuild() { - var guild = client.Guilds.SingleOrDefault(g => g.Name == Program.Config.ServerName); + var guild = client.Guilds.SingleOrDefault(g => g.Id == Program.Config.ServerId); if (guild == null) { - throw new Exception($"Unable to find guild by name: '{Program.Config.ServerName}'. " + - $"Known guilds: [{string.Join(",", client.Guilds.Select(g => g.Name))}]"); + throw new Exception($"Unable to find guild by id: '{Program.Config.ServerId}'. " + + $"Known guilds: [{string.Join(",", client.Guilds.Select(g => g.Name + " (" + g.Id + ")"))}]"); } return guild; } @@ -140,85 +173,4 @@ namespace BiblioTech.Rewards public RewardUsersCommand RewardCommand { get; } public UserData[] Users { get; } } - - public class RewardContext - { - private readonly Dictionary users; - private readonly Dictionary roles; - private readonly SocketTextChannel? rewardsChannel; - - public RewardContext(Dictionary users, Dictionary roles, SocketTextChannel? rewardsChannel) - { - this.users = users; - this.roles = roles; - this.rewardsChannel = rewardsChannel; - } - - public async Task ProcessGiveRewardsCommand(UserReward[] rewards) - { - foreach (var rewardCommand in rewards) - { - if (roles.ContainsKey(rewardCommand.RewardCommand.RewardId)) - { - var role = roles[rewardCommand.RewardCommand.RewardId]; - await ProcessRewardCommand(role, rewardCommand); - } - } - } - - private async Task ProcessRewardCommand(RoleReward role, UserReward reward) - { - foreach (var user in reward.Users) - { - await GiveReward(role, user); - } - } - - private async Task GiveReward(RoleReward role, UserData user) - { - if (!users.ContainsKey(user.DiscordId)) - { - Program.Log.Log($"User by id '{user.DiscordId}' not found."); - return; - } - - var guildUser = users[user.DiscordId]; - - var alreadyHas = guildUser.RoleIds.ToArray(); - if (alreadyHas.Any(r => r == role.Reward.RoleId)) return; - - await GiveRole(guildUser, role.SocketRole); - await SendNotification(role, user, guildUser); - await Task.Delay(1000); - } - - private async Task GiveRole(IGuildUser user, SocketRole role) - { - try - { - Program.Log.Log($"Giving role {role.Name}={role.Id} to user {user.DisplayName}"); - await user.AddRoleAsync(role); - } - catch (Exception ex) - { - Program.Log.Error($"Failed to give role '{role.Name}' to user '{user.DisplayName}': {ex}"); - } - } - - private async Task SendNotification(RoleReward reward, UserData userData, IGuildUser user) - { - try - { - if (userData.NotificationsEnabled && rewardsChannel != null) - { - var msg = reward.Reward.Message.Replace(RewardConfig.UsernameTag, $"<@{user.Id}>"); - await rewardsChannel.SendMessageAsync(msg); - } - } - catch (Exception ex) - { - Program.Log.Error($"Failed to notify user '{user.DisplayName}' about role '{reward.SocketRole.Name}': {ex}"); - } - } - } } diff --git a/Tools/BiblioTech/UserData.cs b/Tools/BiblioTech/UserData.cs new file mode 100644 index 00000000..540c326a --- /dev/null +++ b/Tools/BiblioTech/UserData.cs @@ -0,0 +1,66 @@ +using CodexContractsPlugin; +using GethPlugin; + +namespace BiblioTech +{ + public class UserData + { + public UserData(ulong discordId, string name, DateTime createdUtc, EthAddress? currentAddress, List associateEvents, List mintEvents, bool notificationsEnabled) + { + DiscordId = discordId; + Name = name; + CreatedUtc = createdUtc; + CurrentAddress = currentAddress; + AssociateEvents = associateEvents; + MintEvents = mintEvents; + NotificationsEnabled = notificationsEnabled; + } + + public ulong DiscordId { get; } + public string Name { get; } + public DateTime CreatedUtc { get; } + public EthAddress? CurrentAddress { get; set; } + public List AssociateEvents { get; } + public List MintEvents { get; } + public bool NotificationsEnabled { get; set; } + + public string[] CreateOverview() + { + return new[] + { + $"name: '{Name}' - id:{DiscordId}", + $"joined: {CreatedUtc.ToString("o")}", + $"current address: {CurrentAddress}", + $"{AssociateEvents.Count + MintEvents.Count} total bot events." + }; + } + } + + public class UserAssociateAddressEvent + { + public UserAssociateAddressEvent(DateTime utc, EthAddress? newAddress) + { + Utc = utc; + NewAddress = newAddress; + } + + public DateTime Utc { get; } + public EthAddress? NewAddress { get; } + } + + public class UserMintEvent + { + public UserMintEvent(DateTime utc, EthAddress usedAddress, Transaction? ethReceived, Transaction? testTokensMinted) + { + Utc = utc; + UsedAddress = usedAddress; + EthReceived = ethReceived; + TestTokensMinted = testTokensMinted; + } + + public DateTime Utc { get; } + public EthAddress UsedAddress { get; } + public Transaction? EthReceived { get; } + public Transaction? TestTokensMinted { get; } + } +} diff --git a/Tools/BiblioTech/UserRepo.cs b/Tools/BiblioTech/UserRepo.cs index 01afbe90..7cd03fd5 100644 --- a/Tools/BiblioTech/UserRepo.cs +++ b/Tools/BiblioTech/UserRepo.cs @@ -8,6 +8,7 @@ namespace BiblioTech public class UserRepo { private readonly object repoLock = new object(); + private readonly Dictionary cache = new Dictionary(); public bool AssociateUserWithAddress(IUser user, EthAddress address) { @@ -33,6 +34,12 @@ namespace BiblioTech } } + public UserData[] GetAllUserData() + { + if (cache.Count == 0) LoadAllUserData(); + return cache.Values.ToArray(); + } + public void AddMintEventForUser(IUser user, EthAddress usedAddress, Transaction? eth, Transaction? tokens) { lock (repoLock) @@ -151,12 +158,19 @@ namespace BiblioTech private UserData? GetUserData(IUser user) { + if (cache.ContainsKey(user.Id)) + { + return cache[user.Id]; + } + var filename = GetFilename(user); if (!File.Exists(filename)) { return null; } - return JsonConvert.DeserializeObject(File.ReadAllText(filename))!; + var userData = JsonConvert.DeserializeObject(File.ReadAllText(filename))!; + cache.Add(userData.DiscordId, userData); + return userData; } private UserData GetOrCreate(IUser user) @@ -181,6 +195,15 @@ namespace BiblioTech var filename = GetFilename(userData); if (File.Exists(filename)) File.Delete(filename); File.WriteAllText(filename, JsonConvert.SerializeObject(userData)); + + if (cache.ContainsKey(userData.DiscordId)) + { + cache[userData.DiscordId] = userData; + } + else + { + cache.Add(userData.DiscordId, userData); + } } private static string GetFilename(IUser user) @@ -197,66 +220,29 @@ namespace BiblioTech { return Path.Combine(Program.Config.UserDataPath, discordId.ToString() + ".json"); } - } - public class UserData - { - public UserData(ulong discordId, string name, DateTime createdUtc, EthAddress? currentAddress, List associateEvents, List mintEvents, bool notificationsEnabled) + private void LoadAllUserData() { - DiscordId = discordId; - Name = name; - CreatedUtc = createdUtc; - CurrentAddress = currentAddress; - AssociateEvents = associateEvents; - MintEvents = mintEvents; - NotificationsEnabled = notificationsEnabled; - } - - public ulong DiscordId { get; } - public string Name { get; } - public DateTime CreatedUtc { get; } - public EthAddress? CurrentAddress { get; set; } - public List AssociateEvents { get; } - public List MintEvents { get; } - public bool NotificationsEnabled { get; set; } - - public string[] CreateOverview() - { - return new[] + try { - $"name: '{Name}' - id:{DiscordId}", - $"joined: {CreatedUtc.ToString("o")}", - $"current address: {CurrentAddress}", - $"{AssociateEvents.Count + MintEvents.Count} total bot events." - }; + var files = Directory.GetFiles(Program.Config.UserDataPath); + foreach (var file in files) + { + try + { + var userData = JsonConvert.DeserializeObject(File.ReadAllText(file))!; + if (userData != null && userData.DiscordId > 0) + { + cache.Add(userData.DiscordId, userData); + } + } + catch { } + } + } + catch (Exception ex) + { + Program.Log.Error("Exception while trying to load all user data: " + ex); + } } } - - public class UserAssociateAddressEvent - { - public UserAssociateAddressEvent(DateTime utc, EthAddress? newAddress) - { - Utc = utc; - NewAddress = newAddress; - } - - public DateTime Utc { get; } - public EthAddress? NewAddress { get; } - } - - public class UserMintEvent - { - public UserMintEvent(DateTime utc, EthAddress usedAddress, Transaction? ethReceived, Transaction? testTokensMinted) - { - Utc = utc; - UsedAddress = usedAddress; - EthReceived = ethReceived; - TestTokensMinted = testTokensMinted; - } - - public DateTime Utc { get; } - public EthAddress UsedAddress { get; } - public Transaction? EthReceived { get; } - public Transaction? TestTokensMinted { get; } - } } diff --git a/Tools/CodexNetDeployer/CodexNodeStarter.cs b/Tools/CodexNetDeployer/CodexNodeStarter.cs index 97dd4fd8..81df4e0a 100644 --- a/Tools/CodexNetDeployer/CodexNodeStarter.cs +++ b/Tools/CodexNetDeployer/CodexNodeStarter.cs @@ -43,7 +43,7 @@ namespace CodexNetDeployer { s.EnableMarketplace(gethNode, contracts, m => { - m.WithInitial(100.Eth(), config.InitialTestTokens.TestTokens()); + m.WithInitial(100.Eth(), config.InitialTestTokens.TstWei()); if (validatorsLeft > 0) m.AsValidator(); if (config.ShouldMakeStorageAvailable) m.AsStorageNode(); }); @@ -71,8 +71,8 @@ namespace CodexNetDeployer var availability = new StorageAvailability( totalSpace: config.StorageSell!.Value.MB(), maxDuration: TimeSpan.FromSeconds(config.MaxDuration), - minPriceForTotalSpace: config.MinPrice.TestTokens(), - maxCollateral: config.MaxCollateral.TestTokens() + minPriceForTotalSpace: config.MinPrice.TstWei(), + maxCollateral: config.MaxCollateral.TstWei() ); var response = codexNode.Marketplace.MakeStorageAvailable(availability); diff --git a/Tools/CodexNetDeployer/Deployer.cs b/Tools/CodexNetDeployer/Deployer.cs index 440d092a..9537096c 100644 --- a/Tools/CodexNetDeployer/Deployer.cs +++ b/Tools/CodexNetDeployer/Deployer.cs @@ -122,7 +122,7 @@ namespace CodexNetDeployer }); } - private RunningContainers? DeployDiscordBot(CoreInterface ci, GethDeployment gethDeployment, + private RunningPod? DeployDiscordBot(CoreInterface ci, GethDeployment gethDeployment, CodexContractsDeployment contractsDeployment) { if (!config.DeployDiscordBot) return null; @@ -155,7 +155,7 @@ namespace CodexNetDeployer return rc; } - private RunningContainers? StartMetricsService(CoreInterface ci, List startResults) + private RunningPod? StartMetricsService(CoreInterface ci, List startResults) { if (!config.MetricsScraper || !startResults.Any()) return null; @@ -180,7 +180,7 @@ namespace CodexNetDeployer private CodexInstance CreateCodexInstance(ICodexNode node) { - return new CodexInstance(node.Container.RunningContainers, node.GetDebugInfo()); + return new CodexInstance(node.Container.RunningPod, node.GetDebugInfo()); } private string? GetKubeConfig(string kubeConfigFile) @@ -255,9 +255,9 @@ namespace CodexNetDeployer return TimeSpan.FromSeconds(2); } - public int HttpMaxNumberOfRetries() + public TimeSpan HttpRetryTimeout() { - return 2; + return TimeSpan.FromSeconds(30); } public TimeSpan HttpCallTimeout() @@ -270,7 +270,7 @@ namespace CodexNetDeployer return TimeSpan.FromMinutes(10); } - public TimeSpan WaitForK8sServiceDelay() + public TimeSpan K8sOperationRetryDelay() { return TimeSpan.FromSeconds(30); } diff --git a/Tools/CodexNetDeployer/K8sHook.cs b/Tools/CodexNetDeployer/K8sHook.cs index b4c0c16a..b8c78b7c 100644 --- a/Tools/CodexNetDeployer/K8sHook.cs +++ b/Tools/CodexNetDeployer/K8sHook.cs @@ -18,11 +18,11 @@ namespace CodexNetDeployer this.metadata = metadata; } - public void OnContainersStarted(RunningContainers rc) + public void OnContainersStarted(RunningPod rc) { } - public void OnContainersStopped(RunningContainers rc) + public void OnContainersStopped(RunningPod rc) { } diff --git a/Tools/KeyMaker/Controllers/KeyController.cs b/Tools/KeyMaker/Controllers/KeyController.cs new file mode 100644 index 00000000..dd218893 --- /dev/null +++ b/Tools/KeyMaker/Controllers/KeyController.cs @@ -0,0 +1,23 @@ +using GethPlugin; +using Microsoft.AspNetCore.Mvc; + +namespace KeyMaker.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class KeyController : ControllerBase + { + [HttpGet] + public KeyResponse Get() + { + var account = EthAccount.GenerateNew(); + + return new KeyResponse + { + Public = account.EthAddress.Address, + Private = account.PrivateKey, + Secure = "Not Secure! For demo/development purposes only!" + }; + } + } +} diff --git a/Tools/KeyMaker/KeyMaker.csproj b/Tools/KeyMaker/KeyMaker.csproj new file mode 100644 index 00000000..9ee1ab24 --- /dev/null +++ b/Tools/KeyMaker/KeyMaker.csproj @@ -0,0 +1,18 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + diff --git a/Tools/KeyMaker/KeyMaker.csproj.user b/Tools/KeyMaker/KeyMaker.csproj.user new file mode 100644 index 00000000..9ff5820a --- /dev/null +++ b/Tools/KeyMaker/KeyMaker.csproj.user @@ -0,0 +1,6 @@ + + + + https + + \ No newline at end of file diff --git a/Tools/KeyMaker/KeyResponse.cs b/Tools/KeyMaker/KeyResponse.cs new file mode 100644 index 00000000..df8fb0b7 --- /dev/null +++ b/Tools/KeyMaker/KeyResponse.cs @@ -0,0 +1,9 @@ +namespace KeyMaker +{ + public class KeyResponse + { + public string Public { get; set; } = string.Empty; + public string Private { get; set; } = string.Empty; + public string Secure { get; set; } = string.Empty; + } +} diff --git a/Tools/KeyMaker/Program.cs b/Tools/KeyMaker/Program.cs new file mode 100644 index 00000000..9c24f8fc --- /dev/null +++ b/Tools/KeyMaker/Program.cs @@ -0,0 +1,31 @@ +var builder = WebApplication.CreateBuilder(args); + +var listenPort = Environment.GetEnvironmentVariable("APIPORT"); +if (string.IsNullOrEmpty(listenPort)) listenPort = "31090"; + +builder.WebHost.ConfigureKestrel((context, options) => +{ + options.ListenAnyIP(Convert.ToInt32(listenPort)); +}); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +Console.WriteLine("KeyMaker listening on port " + listenPort); + +app.Run(); diff --git a/Tools/KeyMaker/Properties/launchSettings.json b/Tools/KeyMaker/Properties/launchSettings.json new file mode 100644 index 00000000..7517b23f --- /dev/null +++ b/Tools/KeyMaker/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:17248", + "sslPort": 44396 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5069", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7056;http://localhost:5069", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/Tools/KeyMaker/appsettings.Development.json b/Tools/KeyMaker/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/Tools/KeyMaker/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/Tools/KeyMaker/appsettings.json b/Tools/KeyMaker/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/Tools/KeyMaker/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Tools/KeyMaker/docker/Dockerfile b/Tools/KeyMaker/docker/Dockerfile new file mode 100644 index 00000000..74d929bb --- /dev/null +++ b/Tools/KeyMaker/docker/Dockerfile @@ -0,0 +1,26 @@ +# Variables +ARG BUILDER=mcr.microsoft.com/dotnet/sdk:7.0 +ARG IMAGE=${BUILDER} +ARG APP_HOME=/app + + +# Build +FROM ${IMAGE} AS builder +ARG APP_HOME + +WORKDIR ${APP_HOME} +COPY ./Tools/KeyMaker ./Tools/KeyMaker +COPY ./Framework ./Framework +COPY ./ProjectPlugins ./ProjectPlugins +RUN dotnet restore Tools/KeyMaker +RUN dotnet publish Tools/KeyMaker -c Release -o out + + +# Create +FROM ${IMAGE} +ARG APP_HOME +ENV APP_HOME=${APP_HOME} + +WORKDIR ${APP_HOME} +COPY --from=builder ${APP_HOME}/out . +CMD dotnet ${APP_HOME}/KeyMaker.dll diff --git a/Tools/TestNetRewarder/BotClient.cs b/Tools/TestNetRewarder/BotClient.cs index c112af48..3ee1fb99 100644 --- a/Tools/TestNetRewarder/BotClient.cs +++ b/Tools/TestNetRewarder/BotClient.cs @@ -1,7 +1,5 @@ -using CodexContractsPlugin.Marketplace; -using DiscordRewards; +using DiscordRewards; using Logging; -using Newtonsoft.Json; using System.Net.Http.Json; namespace TestNetRewarder @@ -26,7 +24,7 @@ namespace TestNetRewarder public async Task SendRewards(GiveRewardsCommand command) { - if (command == null || command.Rewards == null || !command.Rewards.Any()) return false; + if (command == null) return false; var result = await HttpPostJson(command); log.Log("Reward response: " + result); return result == "OK"; diff --git a/Tools/TestNetRewarder/ChainChangeMux.cs b/Tools/TestNetRewarder/ChainChangeMux.cs new file mode 100644 index 00000000..f906a20f --- /dev/null +++ b/Tools/TestNetRewarder/ChainChangeMux.cs @@ -0,0 +1,46 @@ +using CodexContractsPlugin.ChainMonitor; +using GethPlugin; +using System.Numerics; + +namespace TestNetRewarder +{ + public class ChainChangeMux : IChainStateChangeHandler + { + private readonly IChainStateChangeHandler[] handlers; + + public ChainChangeMux(params IChainStateChangeHandler[] handlers) + { + this.handlers = handlers; + } + + public void OnNewRequest(RequestEvent requestEvent) + { + foreach (var handler in handlers) handler.OnNewRequest(requestEvent); + } + + public void OnRequestCancelled(RequestEvent requestEvent) + { + foreach (var handler in handlers) handler.OnRequestCancelled(requestEvent); + } + + public void OnRequestFinished(RequestEvent requestEvent) + { + foreach (var handler in handlers) handler.OnRequestFinished(requestEvent); + } + + public void OnRequestFulfilled(RequestEvent requestEvent) + { + foreach (var handler in handlers) handler.OnRequestFulfilled(requestEvent); + } + + public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex) + { + foreach (var handler in handlers) handler.OnSlotFilled(requestEvent, host, slotIndex); + } + + public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex) + { + foreach (var handler in handlers) handler.OnSlotFreed(requestEvent, slotIndex); + } + } +} diff --git a/Tools/TestNetRewarder/ChainState.cs b/Tools/TestNetRewarder/ChainState.cs deleted file mode 100644 index 40e4bf68..00000000 --- a/Tools/TestNetRewarder/ChainState.cs +++ /dev/null @@ -1,35 +0,0 @@ -using CodexContractsPlugin; -using CodexContractsPlugin.Marketplace; -using Utils; - -namespace TestNetRewarder -{ - public class ChainState - { - private readonly HistoricState historicState; - - public ChainState(HistoricState historicState, ICodexContracts contracts, BlockInterval blockRange) - { - NewRequests = contracts.GetStorageRequests(blockRange); - historicState.ProcessNewRequests(NewRequests); - historicState.UpdateStorageRequests(contracts); - - StartedRequests = historicState.StorageRequests.Where(r => r.RecentlyStarted).ToArray(); - FinishedRequests = historicState.StorageRequests.Where(r => r.RecentlyFininshed).ToArray(); - RequestFulfilledEvents = contracts.GetRequestFulfilledEvents(blockRange); - RequestCancelledEvents = contracts.GetRequestCancelledEvents(blockRange); - SlotFilledEvents = contracts.GetSlotFilledEvents(blockRange); - SlotFreedEvents = contracts.GetSlotFreedEvents(blockRange); - this.historicState = historicState; - } - - public Request[] NewRequests { get; } - public StorageRequest[] AllRequests => historicState.StorageRequests; - public StorageRequest[] StartedRequests { get; private set; } - public StorageRequest[] FinishedRequests { get; private set; } - public RequestFulfilledEventDTO[] RequestFulfilledEvents { get; } - public RequestCancelledEventDTO[] RequestCancelledEvents { get; } - public SlotFilledEventDTO[] SlotFilledEvents { get; } - public SlotFreedEventDTO[] SlotFreedEvents { get; } - } -} diff --git a/Tools/TestNetRewarder/Checks.cs b/Tools/TestNetRewarder/Checks.cs deleted file mode 100644 index 43102e06..00000000 --- a/Tools/TestNetRewarder/Checks.cs +++ /dev/null @@ -1,141 +0,0 @@ -using CodexContractsPlugin.Marketplace; -using GethPlugin; -using NethereumWorkflow; -using Utils; - -namespace TestNetRewarder -{ - public interface ICheck - { - EthAddress[] Check(ChainState state); - } - - public class FilledAnySlotCheck : ICheck - { - public EthAddress[] Check(ChainState state) - { - return state.SlotFilledEvents.Select(e => e.Host).ToArray(); - } - } - - public class FinishedSlotCheck : ICheck - { - private readonly ByteSize minSize; - private readonly TimeSpan minDuration; - - public FinishedSlotCheck(ByteSize minSize, TimeSpan minDuration) - { - this.minSize = minSize; - this.minDuration = minDuration; - } - - public EthAddress[] Check(ChainState state) - { - return state.FinishedRequests - .Where(r => - MeetsSizeRequirement(r) && - MeetsDurationRequirement(r)) - .SelectMany(r => r.Hosts) - .ToArray(); - } - - private bool MeetsSizeRequirement(StorageRequest r) - { - var slotSize = r.Request.Ask.SlotSize.ToDecimal(); - decimal min = minSize.SizeInBytes; - return slotSize >= min; - } - - private bool MeetsDurationRequirement(StorageRequest r) - { - var duration = TimeSpan.FromSeconds((double)r.Request.Ask.Duration); - return duration >= minDuration; - } - } - - public class PostedContractCheck : ICheck - { - private readonly ulong minNumberOfHosts; - private readonly ByteSize minSlotSize; - private readonly TimeSpan minDuration; - - public PostedContractCheck(ulong minNumberOfHosts, ByteSize minSlotSize, TimeSpan minDuration) - { - this.minNumberOfHosts = minNumberOfHosts; - this.minSlotSize = minSlotSize; - this.minDuration = minDuration; - } - - public EthAddress[] Check(ChainState state) - { - return state.NewRequests - .Where(r => - MeetsNumSlotsRequirement(r) && - MeetsSizeRequirement(r) && - MeetsDurationRequirement(r)) - .Select(r => r.ClientAddress) - .ToArray(); - } - - private bool MeetsNumSlotsRequirement(Request r) - { - return r.Ask.Slots >= minNumberOfHosts; - } - - private bool MeetsSizeRequirement(Request r) - { - var slotSize = r.Ask.SlotSize.ToDecimal(); - decimal min = minSlotSize.SizeInBytes; - return slotSize >= min; - } - - private bool MeetsDurationRequirement(Request r) - { - var duration = TimeSpan.FromSeconds((double)r.Ask.Duration); - return duration >= minDuration; - } - } - - public class StartedContractCheck : ICheck - { - private readonly ulong minNumberOfHosts; - private readonly ByteSize minSlotSize; - private readonly TimeSpan minDuration; - - public StartedContractCheck(ulong minNumberOfHosts, ByteSize minSlotSize, TimeSpan minDuration) - { - this.minNumberOfHosts = minNumberOfHosts; - this.minSlotSize = minSlotSize; - this.minDuration = minDuration; - } - - public EthAddress[] Check(ChainState state) - { - return state.StartedRequests - .Where(r => - MeetsNumSlotsRequirement(r) && - MeetsSizeRequirement(r) && - MeetsDurationRequirement(r)) - .Select(r => r.Request.ClientAddress) - .ToArray(); - } - - private bool MeetsNumSlotsRequirement(StorageRequest r) - { - return r.Request.Ask.Slots >= minNumberOfHosts; - } - - private bool MeetsSizeRequirement(StorageRequest r) - { - var slotSize = r.Request.Ask.SlotSize.ToDecimal(); - decimal min = minSlotSize.SizeInBytes; - return slotSize >= min; - } - - private bool MeetsDurationRequirement(StorageRequest r) - { - var duration = TimeSpan.FromSeconds((double)r.Request.Ask.Duration); - return duration >= minDuration; - } - } -} diff --git a/Tools/TestNetRewarder/Configuration.cs b/Tools/TestNetRewarder/Configuration.cs index d19edb30..5d6ae51d 100644 --- a/Tools/TestNetRewarder/Configuration.cs +++ b/Tools/TestNetRewarder/Configuration.cs @@ -4,21 +4,27 @@ namespace TestNetRewarder { public class Configuration { - [Uniform("datapath", "dp", "DATAPATH", false, "Root path where all data files will be saved.")] + [Uniform("datapath", "dp", "DATAPATH", true, "Root path where all data files will be saved.")] public string DataPath { get; set; } = "datapath"; [Uniform("discordbot-host", "dh", "DISCORDBOTHOST", true, "http address of the discord bot.")] public string DiscordHost { get; set; } = "host"; - [Uniform("discordbot-port", "dp", "DISCORDBOTPORT", true, "port number of the discord bot reward API. (31080 by default)")] + [Uniform("discordbot-port", "dp", "DISCORDBOTPORT", true, "port number of the discord bot reward API.")] public int DiscordPort { get; set; } = 31080; - [Uniform("interval-minutes", "im", "INTERVALMINUTES", false, "time in minutes between reward updates. (default 15)")] + [Uniform("interval-minutes", "im", "INTERVALMINUTES", true, "time in minutes between reward updates.")] public int IntervalMinutes { get; set; } = 15; [Uniform("check-history", "ch", "CHECKHISTORY", true, "Unix epoc timestamp of a moment in history on which processing begins. Required for hosting rewards. Should be 'launch of the testnet'.")] public int CheckHistoryTimestamp { get; set; } = 0; + [Uniform("market-insights", "mi", "MARKETINSIGHTS", false, "Semi-colon separated integers. Each represents a multiple of intervals, for which a market insights average will be generated.")] + public string MarketInsights { get; set; } = "1;96"; + + [Uniform("events-overview", "eo", "EVENTSOVERVIEW", false, "When greater than zero, chain event summary will be generated.")] + public int CreateChainEventsOverview { get; set; } = 1; + public string LogPath { get @@ -26,5 +32,22 @@ namespace TestNetRewarder return Path.Combine(DataPath, "logs"); } } + + public TimeSpan Interval + { + get + { + return TimeSpan.FromMinutes(IntervalMinutes); + } + } + + public DateTime HistoryStartUtc + { + get + { + if (CheckHistoryTimestamp == 0) throw new Exception("'check-history' unix timestamp is required. Set it to the start/launch moment of the testnet."); + return DateTimeOffset.FromUnixTimeSeconds(CheckHistoryTimestamp).UtcDateTime; + } + } } } diff --git a/Tools/TestNetRewarder/EventsFormatter.cs b/Tools/TestNetRewarder/EventsFormatter.cs new file mode 100644 index 00000000..048a3a55 --- /dev/null +++ b/Tools/TestNetRewarder/EventsFormatter.cs @@ -0,0 +1,123 @@ +using CodexContractsPlugin; +using CodexContractsPlugin.ChainMonitor; +using GethPlugin; +using System.Numerics; +using Utils; + +namespace TestNetRewarder +{ + public class EventsFormatter : IChainStateChangeHandler + { + private static readonly string nl = Environment.NewLine; + private readonly List events = new List(); + + public string[] GetEvents() + { + var result = events.ToArray(); + events.Clear(); + return result; + } + + public void AddError(string error) + { + AddBlock("📢 **Error**", error); + } + + public void OnNewRequest(RequestEvent requestEvent) + { + var request = requestEvent.Request; + AddRequestBlock(requestEvent, "New Request", + $"Client: {request.Client}", + $"Content: {request.Request.Content.Cid}", + $"Duration: {BigIntToDuration(request.Request.Ask.Duration)}", + $"Expiry: {BigIntToDuration(request.Request.Expiry)}", + $"Collateral: {BitIntToTestTokens(request.Request.Ask.Collateral)}", + $"Reward: {BitIntToTestTokens(request.Request.Ask.Reward)}", + $"Number of Slots: {request.Request.Ask.Slots}", + $"Slot Tolerance: {request.Request.Ask.MaxSlotLoss}", + $"Slot Size: {BigIntToByteSize(request.Request.Ask.SlotSize)}" + ); + } + + public void OnRequestCancelled(RequestEvent requestEvent) + { + AddRequestBlock(requestEvent, "Cancelled"); + } + + public void OnRequestFinished(RequestEvent requestEvent) + { + AddRequestBlock(requestEvent, "Finished"); + } + + public void OnRequestFulfilled(RequestEvent requestEvent) + { + AddRequestBlock(requestEvent, "Started"); + } + + public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex) + { + AddRequestBlock(requestEvent, "Slot Filled", + $"Host: {host}", + $"Slot Index: {slotIndex}" + ); + } + + public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex) + { + AddRequestBlock(requestEvent, "Slot Freed", + $"Slot Index: {slotIndex}" + ); + } + + private void AddRequestBlock(RequestEvent requestEvent, string eventName, params string[] content) + { + var blockNumber = $"[{requestEvent.Block.BlockNumber}]"; + var title = $"{blockNumber} **{eventName}** `{requestEvent.Request.Request.Id}`"; + AddBlock(title, content); + } + + private void AddBlock(string title, params string[] content) + { + events.Add(FormatBlock(title, content)); + } + + private string FormatBlock(string title, params string[] content) + { + if (content == null || !content.Any()) + { + return $"{title}{nl}{nl}"; + } + + return string.Join(nl, + new string[] + { + title, + "```" + } + .Concat(content) + .Concat(new string[] + { + "```" + }) + ) + nl + nl; + } + + private string BigIntToDuration(BigInteger big) + { + var span = TimeSpan.FromSeconds((int)big); + return Time.FormatDuration(span); + } + + private string BigIntToByteSize(BigInteger big) + { + var size = new ByteSize((long)big); + return size.ToString(); + } + + private string BitIntToTestTokens(BigInteger big) + { + var tt = new TestToken(big); + return tt.ToString(); + } + } +} diff --git a/Tools/TestNetRewarder/HistoricState.cs b/Tools/TestNetRewarder/HistoricState.cs index b1ac6996..8487aa06 100644 --- a/Tools/TestNetRewarder/HistoricState.cs +++ b/Tools/TestNetRewarder/HistoricState.cs @@ -1,6 +1,7 @@ using CodexContractsPlugin; using CodexContractsPlugin.Marketplace; using GethPlugin; +using Newtonsoft.Json; namespace TestNetRewarder { @@ -19,6 +20,29 @@ namespace TestNetRewarder { foreach (var r in storageRequests) r.Update(contracts); } + + public void CleanUpOldRequests() + { + storageRequests.RemoveAll(r => + r.State == RequestState.Cancelled || + r.State == RequestState.Finished || + r.State == RequestState.Failed + ); + } + + public string EntireString() + { + return JsonConvert.SerializeObject(StorageRequests); + } + + public HistoricState() + { + } + + public HistoricState(StorageRequest[] requests) + { + storageRequests.AddRange(requests); + } } public class StorageRequest @@ -32,12 +56,16 @@ namespace TestNetRewarder public Request Request { get; } public EthAddress[] Hosts { get; private set; } public RequestState State { get; private set; } + + [JsonIgnore] public bool RecentlyStarted { get; private set; } - public bool RecentlyFininshed { get; private set; } + + [JsonIgnore] + public bool RecentlyFinished { get; private set; } public void Update(ICodexContracts contracts) { - Hosts = GetHosts(contracts); + var newHosts = GetHosts(contracts); var newState = contracts.GetRequestState(Request); @@ -45,11 +73,12 @@ namespace TestNetRewarder State == RequestState.New && newState == RequestState.Started; - RecentlyFininshed = + RecentlyFinished = State == RequestState.Started && newState == RequestState.Finished; State = newState; + Hosts = newHosts; } private EthAddress[] GetHosts(ICodexContracts contracts) diff --git a/Tools/TestNetRewarder/MarketBuffer.cs b/Tools/TestNetRewarder/MarketBuffer.cs new file mode 100644 index 00000000..0fadf7e7 --- /dev/null +++ b/Tools/TestNetRewarder/MarketBuffer.cs @@ -0,0 +1,74 @@ +using CodexContractsPlugin.ChainMonitor; +using CodexContractsPlugin.Marketplace; +using DiscordRewards; +using System.Numerics; + +namespace TestNetRewarder +{ + public class MarketBuffer + { + private readonly List requestEvents = new List(); + private readonly TimeSpan bufferSpan; + + public MarketBuffer(TimeSpan bufferSpan) + { + this.bufferSpan = bufferSpan; + } + + public void Add(RequestEvent requestEvent) + { + requestEvents.Add(requestEvent); + } + + public void Update() + { + var now = DateTime.UtcNow; + requestEvents.RemoveAll(r => (now - r.Request.FinishedUtc) > bufferSpan); + } + + public MarketAverage? GetAverage() + { + if (requestEvents.Count == 0) return null; + + return new MarketAverage + { + NumberOfFinished = requestEvents.Count, + TimeRangeSeconds = (int)bufferSpan.TotalSeconds, + Price = Average(s => s.Request.Ask.Reward), + Duration = Average(s => s.Request.Ask.Duration), + Size = Average(s => GetTotalSize(s.Request.Ask)), + Collateral = Average(s => s.Request.Ask.Collateral), + ProofProbability = Average(s => s.Request.Ask.ProofProbability) + }; + } + + private float Average(Func getValue) + { + return Average(s => + { + var value = getValue(s); + return (int)value; + }); + } + + private float Average(Func getValue) + { + var sum = 0.0f; + float count = requestEvents.Count; + foreach (var r in requestEvents) + { + sum += getValue(r.Request); + } + + if (count < 1.0f) return 0.0f; + return sum / count; + } + + private int GetTotalSize(Ask ask) + { + var nSlots = Convert.ToInt32(ask.Slots); + var slotSize = (int)ask.SlotSize; + return nSlots * slotSize; + } + } +} diff --git a/Tools/TestNetRewarder/MarketTracker.cs b/Tools/TestNetRewarder/MarketTracker.cs new file mode 100644 index 00000000..981c51c5 --- /dev/null +++ b/Tools/TestNetRewarder/MarketTracker.cs @@ -0,0 +1,74 @@ +using CodexContractsPlugin.ChainMonitor; +using DiscordRewards; +using GethPlugin; +using Logging; +using System.Numerics; + +namespace TestNetRewarder +{ + public class MarketTracker : IChainStateChangeHandler + { + private readonly List buffers = new List(); + private readonly ILog log; + + public MarketTracker(Configuration config, ILog log) + { + var intervals = GetInsightCounts(config); + + foreach (var i in intervals) + { + buffers.Add(new MarketBuffer( + config.Interval * i + )); + } + + this.log = log; + } + + public MarketAverage[] GetAverages() + { + foreach (var b in buffers) b.Update(); + + return buffers.Select(b => b.GetAverage()).Where(a => a != null).Cast().ToArray(); + } + + public void OnNewRequest(RequestEvent requestEvent) + { + } + + public void OnRequestFinished(RequestEvent requestEvent) + { + foreach (var b in buffers) b.Add(requestEvent); + } + + public void OnRequestFulfilled(RequestEvent requestEvent) + { + } + + public void OnRequestCancelled(RequestEvent requestEvent) + { + } + + public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex) + { + } + + public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex) + { + } + + private int[] GetInsightCounts(Configuration config) + { + try + { + var tokens = config.MarketInsights.Split(';').ToArray(); + return tokens.Select(t => Convert.ToInt32(t)).ToArray(); + } + catch (Exception ex) + { + log.Error($"Exception when parsing MarketInsights config parameters: {ex}"); + } + return Array.Empty(); + } + } +} diff --git a/Tools/TestNetRewarder/Processor.cs b/Tools/TestNetRewarder/Processor.cs index 032b0ef7..642be072 100644 --- a/Tools/TestNetRewarder/Processor.cs +++ b/Tools/TestNetRewarder/Processor.cs @@ -1,127 +1,61 @@ -using DiscordRewards; -using GethPlugin; +using CodexContractsPlugin; +using CodexContractsPlugin.ChainMonitor; using Logging; -using Newtonsoft.Json; using Utils; namespace TestNetRewarder { - public class Processor + public class Processor : ITimeSegmentHandler { - private static readonly HistoricState historicState = new HistoricState(); - private static readonly RewardRepo rewardRepo = new RewardRepo(); + private readonly RequestBuilder builder; + private readonly RewardChecker rewardChecker; + private readonly MarketTracker marketTracker; + private readonly EventsFormatter eventsFormatter; + private readonly ChainState chainState; + private readonly BotClient client; private readonly ILog log; - private BlockInterval? lastBlockRange; - public Processor(ILog log) + public Processor(Configuration config, BotClient client, ICodexContracts contracts, ILog log) { + this.client = client; this.log = log; + + builder = new RequestBuilder(); + rewardChecker = new RewardChecker(builder); + marketTracker = new MarketTracker(config, log); + eventsFormatter = new EventsFormatter(); + + var handler = new ChainChangeMux( + rewardChecker.Handler, + marketTracker, + eventsFormatter + ); + + chainState = new ChainState(log, contracts, handler, config.HistoryStartUtc); } - public async Task ProcessTimeSegment(TimeRange timeRange) + public async Task OnNewSegment(TimeRange timeRange) { - var connector = GethConnector.GethConnector.Initialize(log); - if (connector == null) throw new Exception("Invalid Geth information"); - try { - var blockRange = connector.GethNode.ConvertTimeRangeToBlockRange(timeRange); - if (!IsNewBlockRange(blockRange)) - { - log.Log($"Block range {blockRange} was previously processed. Skipping..."); - return; - } + chainState.Update(timeRange.To); - var chainState = new ChainState(historicState, connector.CodexContracts, blockRange); - await ProcessChainState(chainState); + var averages = marketTracker.GetAverages(); + var events = eventsFormatter.GetEvents(); + + var request = builder.Build(averages, events); + if (request.HasAny()) + { + await client.SendRewards(request); + } } catch (Exception ex) { - log.Error("Exception processing time segment: " + ex); + var msg = "Exception processing time segment: " + ex; + log.Error(msg); + eventsFormatter.AddError(msg); throw; } } - - private bool IsNewBlockRange(BlockInterval blockRange) - { - if (lastBlockRange == null || - lastBlockRange.From != blockRange.From || - lastBlockRange.To != blockRange.To) - { - lastBlockRange = blockRange; - return true; - } - - return false; - } - - private async Task ProcessChainState(ChainState chainState) - { - var outgoingRewards = new List(); - foreach (var reward in rewardRepo.Rewards) - { - ProcessReward(outgoingRewards, reward, chainState); - } - - log.Log($"Found {outgoingRewards.Count} rewards to send."); - if (outgoingRewards.Any()) - { - if (!await SendRewardsCommand(outgoingRewards)) - { - log.Error("Failed to send reward command."); - } - } - } - - private async Task SendRewardsCommand(List outgoingRewards) - { - var cmd = new GiveRewardsCommand - { - Rewards = outgoingRewards.ToArray() - }; - - log.Debug("Sending rewards: " + JsonConvert.SerializeObject(cmd)); - return await Program.BotClient.SendRewards(cmd); - } - - private void ProcessReward(List outgoingRewards, RewardConfig reward, ChainState chainState) - { - var winningAddresses = PerformCheck(reward, chainState); - foreach (var win in winningAddresses) - { - log.Log($"Address '{win.Address}' wins '{reward.Message}'"); - } - if (winningAddresses.Any()) - { - outgoingRewards.Add(new RewardUsersCommand - { - RewardId = reward.RoleId, - UserAddresses = winningAddresses.Select(a => a.Address).ToArray() - }); - } - } - - private EthAddress[] PerformCheck(RewardConfig reward, ChainState chainState) - { - var check = GetCheck(reward.CheckConfig); - return check.Check(chainState); - } - - private ICheck GetCheck(CheckConfig config) - { - switch (config.Type) - { - case CheckType.FilledSlot: - return new FilledAnySlotCheck(); - case CheckType.FinishedSlot: - return new FinishedSlotCheck(config.MinSlotSize, config.MinDuration); - case CheckType.PostedContract: - return new PostedContractCheck(config.MinNumberOfHosts, config.MinSlotSize, config.MinDuration); - case CheckType.StartedContract: - return new StartedContractCheck(config.MinNumberOfHosts, config.MinSlotSize, config.MinDuration); - } - - throw new Exception("Unknown check type: " + config.Type); - } } } diff --git a/Tools/TestNetRewarder/Program.cs b/Tools/TestNetRewarder/Program.cs index 0aaa41c7..0056ffec 100644 --- a/Tools/TestNetRewarder/Program.cs +++ b/Tools/TestNetRewarder/Program.cs @@ -6,10 +6,10 @@ namespace TestNetRewarder { public class Program { - public static Configuration Config { get; private set; } = null!; - public static ILog Log { get; private set; } = null!; - public static CancellationToken CancellationToken { get; private set; } - public static BotClient BotClient { get; private set; } = null!; + public static CancellationToken CancellationToken; + private static Configuration Config = null!; + private static ILog Log = null!; + private static BotClient BotClient = null!; private static Processor processor = null!; private static DateTime lastCheck = DateTime.MinValue; @@ -27,8 +27,11 @@ namespace TestNetRewarder new ConsoleLog() ); + var connector = GethConnector.GethConnector.Initialize(Log); + if (connector == null) throw new Exception("Invalid Geth information"); + BotClient = new BotClient(Config, Log); - processor = new Processor(Log); + processor = new Processor(Config, BotClient, connector.CodexContracts, Log); EnsurePath(Config.DataPath); EnsurePath(Config.LogPath); @@ -41,12 +44,12 @@ namespace TestNetRewarder EnsureGethOnline(); Log.Log("Starting TestNet Rewarder..."); - var segmenter = new TimeSegmenter(Log, Config); + var segmenter = new TimeSegmenter(Log, Config, processor); while (!CancellationToken.IsCancellationRequested) { await EnsureBotOnline(); - await segmenter.WaitForNextSegment(processor.ProcessTimeSegment); + await segmenter.ProcessNextSegment(); await Task.Delay(100, CancellationToken); } } diff --git a/Tools/TestNetRewarder/RequestBuilder.cs b/Tools/TestNetRewarder/RequestBuilder.cs new file mode 100644 index 00000000..ea06eaac --- /dev/null +++ b/Tools/TestNetRewarder/RequestBuilder.cs @@ -0,0 +1,40 @@ +using DiscordRewards; +using GethPlugin; + +namespace TestNetRewarder +{ + public class RequestBuilder : IRewardGiver + { + private readonly Dictionary> rewards = new Dictionary>(); + + public void Give(RewardConfig reward, EthAddress receiver) + { + if (rewards.ContainsKey(reward.RoleId)) + { + rewards[reward.RoleId].Add(receiver); + } + else + { + rewards.Add(reward.RoleId, new List { receiver }); + } + } + + public GiveRewardsCommand Build(MarketAverage[] marketAverages, string[] lines) + { + var result = new GiveRewardsCommand + { + Rewards = rewards.Select(p => new RewardUsersCommand + { + RewardId = p.Key, + UserAddresses = p.Value.Select(v => v.Address).ToArray() + }).ToArray(), + Averages = marketAverages, + EventsOverview = lines + }; + + rewards.Clear(); + + return result; + } + } +} diff --git a/Tools/TestNetRewarder/RewardCheck.cs b/Tools/TestNetRewarder/RewardCheck.cs new file mode 100644 index 00000000..c01e0835 --- /dev/null +++ b/Tools/TestNetRewarder/RewardCheck.cs @@ -0,0 +1,97 @@ +using CodexContractsPlugin.ChainMonitor; +using DiscordRewards; +using GethPlugin; +using NethereumWorkflow; +using System.Numerics; + +namespace TestNetRewarder +{ + public interface IRewardGiver + { + void Give(RewardConfig reward, EthAddress receiver); + } + + public class RewardCheck : IChainStateChangeHandler + { + private readonly RewardConfig reward; + private readonly IRewardGiver giver; + + public RewardCheck(RewardConfig reward, IRewardGiver giver) + { + this.reward = reward; + this.giver = giver; + } + + public void OnNewRequest(RequestEvent requestEvent) + { + if (MeetsRequirements(CheckType.ClientPostedContract, requestEvent)) + { + GiveReward(reward, requestEvent.Request.Client); + } + } + + public void OnRequestCancelled(RequestEvent requestEvent) + { + } + + public void OnRequestFinished(RequestEvent requestEvent) + { + if (MeetsRequirements(CheckType.HostFinishedSlot, requestEvent)) + { + foreach (var host in requestEvent.Request.Hosts.GetHosts()) + { + GiveReward(reward, host); + } + } + } + + public void OnRequestFulfilled(RequestEvent requestEvent) + { + if (MeetsRequirements(CheckType.ClientStartedContract, requestEvent)) + { + GiveReward(reward, requestEvent.Request.Client); + } + } + + public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex) + { + if (MeetsRequirements(CheckType.HostFilledSlot, requestEvent)) + { + if (host != null) + { + GiveReward(reward, host); + } + } + } + + public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex) + { + } + + private void GiveReward(RewardConfig reward, EthAddress receiver) + { + giver.Give(reward, receiver); + } + + private bool MeetsRequirements(CheckType type, RequestEvent requestEvent) + { + return + reward.CheckConfig.Type == type && + MeetsDurationRequirement(requestEvent.Request) && + MeetsSizeRequirement(requestEvent.Request); + } + + private bool MeetsSizeRequirement(IChainStateRequest r) + { + var slotSize = r.Request.Ask.SlotSize.ToDecimal(); + decimal min = reward.CheckConfig.MinSlotSize.SizeInBytes; + return slotSize >= min; + } + + private bool MeetsDurationRequirement(IChainStateRequest r) + { + var duration = TimeSpan.FromSeconds((double)r.Request.Ask.Duration); + return duration >= reward.CheckConfig.MinDuration; + } + } +} diff --git a/Tools/TestNetRewarder/RewardChecker.cs b/Tools/TestNetRewarder/RewardChecker.cs new file mode 100644 index 00000000..0d22baf4 --- /dev/null +++ b/Tools/TestNetRewarder/RewardChecker.cs @@ -0,0 +1,17 @@ +using CodexContractsPlugin.ChainMonitor; +using DiscordRewards; + +namespace TestNetRewarder +{ + public class RewardChecker + { + public RewardChecker(IRewardGiver giver) + { + var repo = new RewardRepo(); + var checks = repo.Rewards.Select(r => new RewardCheck(r, giver)).ToArray(); + Handler = new ChainChangeMux(checks); + } + + public IChainStateChangeHandler Handler { get; } + } +} diff --git a/Tools/TestNetRewarder/TimeSegmenter.cs b/Tools/TestNetRewarder/TimeSegmenter.cs index d14bcc73..24899ed1 100644 --- a/Tools/TestNetRewarder/TimeSegmenter.cs +++ b/Tools/TestNetRewarder/TimeSegmenter.cs @@ -3,51 +3,60 @@ using Utils; namespace TestNetRewarder { + public interface ITimeSegmentHandler + { + Task OnNewSegment(TimeRange timeRange); + } + public class TimeSegmenter { private readonly ILog log; + private readonly ITimeSegmentHandler handler; private readonly TimeSpan segmentSize; - private DateTime start; + private DateTime latest; - public TimeSegmenter(ILog log, Configuration configuration) + public TimeSegmenter(ILog log, Configuration configuration, ITimeSegmentHandler handler) { this.log = log; + this.handler = handler; + if (configuration.IntervalMinutes < 0) configuration.IntervalMinutes = 1; - if (configuration.IntervalMinutes < 0) configuration.IntervalMinutes = 15; - if (configuration.CheckHistoryTimestamp == 0) throw new Exception("'check-history' unix timestamp is required. Set it to the start/launch moment of the testnet."); + segmentSize = configuration.Interval; + latest = configuration.HistoryStartUtc; - segmentSize = TimeSpan.FromMinutes(configuration.IntervalMinutes); - start = DateTimeOffset.FromUnixTimeSeconds(configuration.CheckHistoryTimestamp).UtcDateTime; - - log.Log("Starting time segments at " + start); + log.Log("Starting time segments at " + latest); log.Log("Segment size: " + Time.FormatDuration(segmentSize)); } - public async Task WaitForNextSegment(Func onSegment) + public async Task ProcessNextSegment() { - var now = DateTime.UtcNow; - var end = start + segmentSize; - var waited = false; - if (end > now) - { - // Wait for the entire time segment to be in the past. - var delay = end - now; - waited = true; - log.Log($"Waiting till time segment is in the past... {Time.FormatDuration(delay)}"); - await Task.Delay(delay, Program.CancellationToken); - } - await Task.Delay(TimeSpan.FromSeconds(3), Program.CancellationToken); + var end = latest + segmentSize; + var waited = await WaitUntilTimeSegmentInPast(end); if (Program.CancellationToken.IsCancellationRequested) return; var postfix = "(Catching up...)"; if (waited) postfix = "(Real-time)"; + log.Log($"Time segment [{latest} to {end}] {postfix}"); + + var range = new TimeRange(latest, end); + latest = end; - log.Log($"Time segment [{start} to {end}] {postfix}"); - var range = new TimeRange(start, end); - start = end; + await handler.OnNewSegment(range); + } - await onSegment(range); + private async Task WaitUntilTimeSegmentInPast(DateTime end) + { + await Task.Delay(TimeSpan.FromSeconds(3), Program.CancellationToken); + + var now = DateTime.UtcNow; + while (end > now) + { + var delay = (end - now) + TimeSpan.FromSeconds(3); + await Task.Delay(delay, Program.CancellationToken); + return true; + } + return false; } } } diff --git a/cs-codex-dist-testing.sln b/cs-codex-dist-testing.sln index eb7de476..8c95ef09 100644 --- a/cs-codex-dist-testing.sln +++ b/cs-codex-dist-testing.sln @@ -68,6 +68,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoClient", "Tools\AutoClient\AutoClient.csproj", "{73599F9C-98BB-4C6A-9D7D-7C50FBF2993B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KeyMaker", "Tools\KeyMaker\KeyMaker.csproj", "{B57A4789-D8EF-42E0-8D20-581C4057FFD3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -178,6 +180,10 @@ Global {73599F9C-98BB-4C6A-9D7D-7C50FBF2993B}.Debug|Any CPU.Build.0 = Debug|Any CPU {73599F9C-98BB-4C6A-9D7D-7C50FBF2993B}.Release|Any CPU.ActiveCfg = Release|Any CPU {73599F9C-98BB-4C6A-9D7D-7C50FBF2993B}.Release|Any CPU.Build.0 = Release|Any CPU + {B57A4789-D8EF-42E0-8D20-581C4057FFD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B57A4789-D8EF-42E0-8D20-581C4057FFD3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B57A4789-D8EF-42E0-8D20-581C4057FFD3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B57A4789-D8EF-42E0-8D20-581C4057FFD3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -209,6 +215,7 @@ Global {B07820C4-309F-4454-BCC1-1D4902C9C67B} = {81AE04BC-CBFA-4E6F-B039-8208E9AFAAE7} {88C212E9-308A-46A4-BAAD-468E8EBD8EDF} = {8F1F1C2A-E313-4E0C-BE40-58FB0BA91124} {73599F9C-98BB-4C6A-9D7D-7C50FBF2993B} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} + {B57A4789-D8EF-42E0-8D20-581C4057FFD3} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C}