Compare commits

..
Author SHA1 Message Date
benbierens 7eed6160eb working as intended 2024-06-28 09:16:12 +02:00
benbierens c57dc4daa1 concurrent purchases 2024-06-28 08:47:09 +02:00
Ben 4c75cebcd6 Implements autoclient with image generator 2024-06-27 15:38:13 +02:00
Ben a820788c7d Merge branch 'master' into feature/auto-client
# Conflicts:
#	cs-codex-dist-testing.sln
2024-06-27 13:42:54 +02:00
Ben 52a02abd3f Fixes incorrect block numbers in eventFormatter 2024-06-27 11:43:25 +02:00
Ben a92455b2a5 bump codex image to v0.1.2 + prover 2024-06-27 11:16:50 +02:00
Ben 933d5e7d4d Merge branch 'better-chain-events' 2024-06-27 11:16:34 +02:00
Ben ba43fd90c6 Adds admin option to set custom replacements 2024-06-27 11:16:17 +02:00
Ben 01ee514c73 Implements known username replacement 2024-06-27 10:44:37 +02:00
Ben 1eb30329c6 Pulls out chain events sender 2024-06-27 10:14:23 +02:00
Ben 0ef55abdf4 Adds events formatter 2024-06-27 10:07:10 +02:00
Ben 8341807d92 Disables role rewards (not configured in real server) 2024-06-26 11:27:19 +02:00
Ben c2712daccb latest images 2024-06-25 13:02:05 +02:00
benbierens 8033da1176 Updates to new Codex image. Fixes tests 2024-06-21 11:29:05 +02:00
benbierens 5afab577a7 Merge branch 'chainstate-update' 2024-06-21 10:19:32 +02:00
benbierens 4be9b9df9a Finished! 2024-06-21 10:19:09 +02:00
benbierens a905f0ce53 fixes crash in market buffer again 2024-06-21 09:59:24 +02:00
benbierens 0c4d3be912 Fixes crash in marketBuffer 2024-06-21 09:33:52 +02:00
benbierens 2b7ba61543 Cleanup 2024-06-21 09:18:00 +02:00
benbierens f9c7e18985 Merge branch 'master' into chainstate-update 2024-06-21 08:56:43 +02:00
benbierens 32e7028029 wip 2024-06-21 08:56:20 +02:00
Ben cc3eddf02d wip 2024-06-20 12:15:59 +02:00
Ben d73ad6db15 Merge branch 'feature/keymaker' 2024-06-19 14:06:20 +02:00
Ben 5280f0732c Sets up docker image for creating keys 2024-06-19 14:04:27 +02:00
Ben 5ca135646a Container crash detection during start-up 2024-06-19 10:39:14 +02:00
Ben e1da38b6e8 Adds test for making sure node does not crash after mysterious CID is downloaded 2024-06-18 11:04:04 +02:00
Ben f7c45d17d7 wip 2024-06-18 09:57:38 +02:00
Ben cb4cdfe69a better logging 2024-06-17 16:00:51 +02:00
Ben bed57dd35b Finish replace of old chainstate object in rewarder bot 2024-06-17 15:34:08 +02:00
benbierens c38a2242ba wip 2024-06-14 11:09:51 +02:00
benbierens d3488dc907 Adds block numbers to chainstate event logging 2024-06-14 10:22:30 +02:00
benbierens 1fa7787d3b Merge branch 'master' into chainstate-update
# Conflicts:
#	ProjectPlugins/CodexPlugin/CodexNode.cs
#	ProjectPlugins/CodexPlugin/MarketplaceAccess.cs
2024-06-14 09:28:34 +02:00
benbierens 54f053cfcc Improves crash detection 2024-06-14 09:05:56 +02:00
Ben 7b5e802efb Applies delete in scalability tests 2024-06-13 08:51:52 +02:00
Ben 390b9de54a Replaces delete with disk info log. 2024-06-12 15:28:08 +02:00
Ben 4fc9835f43 Attempt to log disk space before and after uploads and downloads 2024-06-12 10:48:52 +02:00
Ben 9a46e20b4a Fixes exception in CodexDistTests 2024-06-11 11:33:02 +02:00
Ben 74ca512f92 Sets up test for downloading content by contract CID 2024-06-10 15:15:27 +02:00
Ben 1c856f7615 Fixes log 2024-06-10 14:17:19 +02:00
Ben cc2513bd2f Better chain state representation 2024-06-10 14:04:25 +02:00
Ben 22527a5d93 Deletes block repostore folder in container when Codex node stops. 2024-06-10 10:58:50 +02:00
benbierens 25663b59a0 sets trace level for scalability tests 2024-06-08 10:50:30 +02:00
benbierens aa416d50b3 ensuring enough mounted disk space 2024-06-08 10:36:23 +02:00
benbierens 16bf5ce5ab I think I may be on to something 2024-06-07 18:16:19 +02:00
benbierens 52ae671234 focussing on the interesting numbers for now 2024-06-07 17:08:14 +02:00
benbierens 5ffff1ed07 Disables metrics in scalability test. Downloads container log when retry attempt failed too quickly to be a timeout. 2024-06-07 17:07:35 +02:00
benbierens 18a02b1717 Implements mapping of debug/info routing table nodes 2024-06-07 16:37:31 +02:00
benbierens 3a61fc89c6 Adds WaitForCleanup test attribute to allow tests to wait for resources to be cleaned up 2024-06-06 15:09:52 +02:00
Ben 38c2d1749a Links up status method for checking. 2024-06-06 10:31:31 +02:00
Ben 9900db8d25 simply scalability test failure handling 2024-06-06 10:12:11 +02:00
Ben e53a644718 Updates to new codex image 2024-06-06 10:11:51 +02:00
Ben 62b56e198b Adds logging of node status to base codexdistTest 2024-06-06 09:54:50 +02:00
Ben f4d1dae478 Disables large network scalability test for now 2024-06-05 10:00:50 +02:00
Ben bf75c25152 Cannot assume retry uploads will require as much space as file is large 2024-06-05 09:59:03 +02:00
Ben b888999b61 Adds space check for upload call 2024-06-05 09:34:43 +02:00
Ben e9555cf99e Upload/download failure now automatically investigates node 2024-06-05 09:20:00 +02:00
Ben 11b866986d wip retry upgrade 2024-06-04 11:06:19 +02:00
benbierens 67fc2183b7 Better logging for slot-filled event checking 2024-06-03 12:56:19 +02:00
benbierens 02baa72c1e DebugInfo log 2024-06-03 11:42:52 +02:00
benbierens 383102b988 Adds container name to crash messge. 2024-06-03 11:36:48 +02:00
benbierens 3da36725e3 Updates codex image 2024-06-03 10:58:04 +02:00
benbierens ac7d323201 wip 2024-06-03 10:38:48 +02:00
benbierens a846d51c0c Implements ChainState in CodexContracts plugin 2024-05-31 11:19:50 +02:00
benbierens f5da80dc9c Sets up ChainEvents object 2024-05-31 10:16:57 +02:00
benbierens f94a67adb4 Extracts event methods from CodexContractsAccess 2024-05-31 10:06:34 +02:00
benbierens 362040bd3c Sets up docker image and CI 2024-04-01 21:29:02 +02:00
benbierens 40393c3a5b Using raw version of purchase-status call. API doesn't line up 2024-04-01 21:09:24 +02:00
benbierens 7f972bac85 Mvoes autoclient 2024-04-01 20:47:56 +02:00
benbierens d532d9505a Sets up autoclient. 2024-04-01 20:40:03 +02:00
102 changed files with 2900 additions and 1332 deletions
+26
View File
@@ -0,0 +1,26 @@
name: Docker - AutoClient
on:
push:
branches:
- master
tags:
- 'v*.*.*'
paths:
- 'Tools/AutoClient/**'
- '!Tools/AutoClient/docker/docker-compose.yaml'
- 'Framework/**'
- 'ProjectPlugins/**'
- .github/workflows/docker-autoclient.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/AutoClient/docker/Dockerfile
docker_repo: codexstorage/codex-autoclient
secrets: inherit
+27
View File
@@ -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
+2 -1
View File
@@ -1,4 +1,5 @@
.vs
obj
bin
.vscode
.vscode
Tools/AutoClient/datapath
+4 -4
View File
@@ -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)
+4 -3
View File
@@ -1,4 +1,5 @@
using Logging;
using KubernetesWorkflow;
using Logging;
namespace Core
{
@@ -14,9 +15,9 @@ namespace Core
{
private readonly LogFile logFile;
internal DownloadedLog(LogFile logFile)
internal DownloadedLog(WriteToFileLogHandler logHandler)
{
this.logFile = logFile;
logFile = logHandler.LogFile;
}
public void IterateLines(Action<string> action)
+7 -3
View File
@@ -38,10 +38,14 @@ namespace Core
return new CoreInterface(this);
}
public void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles)
/// <summary>
/// Deletes kubernetes and tracked file resources.
/// when `waitTillDone` is true, this function will block until resources are deleted.
/// </summary>
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<T>() where T : IProjectPlugin
+10 -3
View File
@@ -7,6 +7,7 @@ namespace Core
{
T OnClient<T>(Func<HttpClient, T> action);
T OnClient<T>(Func<HttpClient, T> action, string description);
T OnClient<T>(Func<HttpClient, T> action, Retry retry);
IEndpoint CreateEndpoint(Address address, string baseUrl, string? logAlias = null);
}
@@ -35,13 +36,19 @@ namespace Core
}
public T OnClient<T>(Func<HttpClient, T> action, string description)
{
var retry = new Retry(description, timeSet.HttpRetryTimeout(), timeSet.HttpCallRetryDelay(), f => { });
return OnClient(action, retry);
}
public T OnClient<T>(Func<HttpClient, T> 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<T>(Func<T> operation, string description)
private T LockRetry<T>(Func<T> operation, Retry retry)
{
lock (httpLock)
{
return Time.Retry(operation, timeSet.HttpRetryTimeout(), timeSet.HttpCallRetryDelay(), description);
return retry.Run(operation);
}
}
-28
View File
@@ -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);
}
}
}
+2 -2
View File
@@ -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);
}
}
+14 -7
View File
@@ -6,7 +6,13 @@ namespace Core
{
public interface IPluginTools : IWorkflowTool, ILogTool, IHttpFactoryTool, IFileTool
{
void Decommission(bool deleteKubernetesResources, bool deleteTrackedFiles);
ITimeSet TimeSet { get; }
/// <summary>
/// Deletes kubernetes and tracked file resources.
/// when `waitTillDone` is true, this function will block until resources are deleted.
/// </summary>
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<HttpClient> onClientCreated)
{
return CreateHttp(onClientCreated, timeSet);
return CreateHttp(onClientCreated, TimeSet);
}
public IHttp CreateHttp(Action<HttpClient> 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();
}
+4 -4
View File
@@ -13,9 +13,9 @@ namespace DiscordRewards
public enum CheckType
{
Uninitialized,
FilledSlot,
FinishedSlot,
PostedContract,
StartedContract,
HostFilledSlot,
HostFinishedSlot,
ClientPostedContract,
ClientStartedContract,
}
}
@@ -5,6 +5,11 @@
public RewardUsersCommand[] Rewards { get; set; } = Array.Empty<RewardUsersCommand>();
public MarketAverage[] Averages { get; set; } = Array.Empty<MarketAverage>();
public string[] EventsOverview { get; set; } = Array.Empty<string>();
public bool HasAny()
{
return Rewards.Any() || Averages.Any() || EventsOverview.Any();
}
}
public class RewardUsersCommand
+41 -41
View File
@@ -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),
// })
//};
}
}
+16 -5
View File
@@ -57,16 +57,27 @@ namespace FileUtils
public void ScopedFiles(Action action)
{
PushFileSet();
action();
PopFileSet();
try
{
action();
}
finally
{
PopFileSet();
}
}
public T ScopedFiles<T>(Func<T> action)
{
PushFileSet();
var result = action();
PopFileSet();
return result;
try
{
return action();
}
finally
{
PopFileSet();
}
}
private void PushFileSet()
@@ -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<long, string>
{
{ 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;
}
}
}
+9 -7
View File
@@ -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);
}
}
}
@@ -16,6 +16,7 @@ namespace KubernetesWorkflow
{
var config = GetConfig();
UpdateHostAddress(config);
config.SkipTlsVerify = true; // Required for operation on Wings cluster.
return config;
}
+42 -10
View File
@@ -45,7 +45,7 @@ namespace KubernetesWorkflow
public void WaitUntilOnline(RunningContainer container)
{
WaitUntilDeploymentOnline(container.Recipe.Name);
WaitUntilDeploymentOnline(container);
}
public PodInfo GetPodInfo(RunningDeployment deployment)
@@ -64,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);
}
@@ -115,7 +115,7 @@ namespace KubernetesWorkflow
});
}
public void DeleteAllNamespacesStartingWith(string prefix)
public void DeleteAllNamespacesStartingWith(string prefix, bool wait)
{
log.Debug();
@@ -124,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);
}
}
@@ -532,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;
}
@@ -871,15 +874,44 @@ namespace KubernetesWorkflow
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)
{
WaitUntil(() =>
+24 -1
View File
@@ -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);
}
}
}
@@ -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)
+1 -4
View File
@@ -1,8 +1,5 @@
using k8s;
using k8s.Models;
using KubernetesWorkflow.Recipe;
using KubernetesWorkflow.Recipe;
using KubernetesWorkflow.Types;
using Newtonsoft.Json;
namespace KubernetesWorkflow
{
@@ -15,10 +15,10 @@ namespace KubernetesWorkflow
PodInfo GetPodInfo(RunningPod pod);
CrashWatcher CreateCrashWatcher(RunningContainer container);
void Stop(RunningPod pod, bool waitTillStopped);
void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null);
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
@@ -106,11 +106,11 @@ namespace KubernetesWorkflow
});
}
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);
});
}
@@ -122,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);
});
}
@@ -117,6 +117,7 @@ namespace NethereumWorkflow
}
return new BlockInterval(
timeRange: timeRange,
from: fromBlock.Value,
to: toBlock.Value
);
+4 -1
View File
@@ -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()
{
+131
View File
@@ -0,0 +1,131 @@
namespace Utils
{
public class Retry
{
private readonly string description;
private readonly TimeSpan maxTimeout;
private readonly TimeSpan sleepAfterFail;
private readonly Action<Failure> onFail;
public Retry(string description, TimeSpan maxTimeout, TimeSpan sleepAfterFail, Action<Failure> 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<T>(Func<T> 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<Failure> onFail;
private readonly DateTime start = DateTime.UtcNow;
private readonly List<Failure> failures = new List<Failure>();
private int tryNumber;
private DateTime tryStart;
public RetryRun(string description, Action task, TimeSpan maxTimeout, TimeSpan sleepAfterFail, Action<Failure> 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}'";
}
}
}
+13 -69
View File
@@ -1,6 +1,4 @@
using System.Diagnostics;
namespace Utils
namespace Utils
{
public static class Time
{
@@ -111,78 +109,24 @@ namespace Utils
public static void Retry(Action action, TimeSpan maxTimeout, TimeSpan retryTime, string description)
{
var start = DateTime.UtcNow;
var tries = 1;
var tryInfo = new List<(Exception, TimeSpan)>();
while (true)
{
var duration = DateTime.UtcNow - start;
if (duration > maxTimeout)
{
var info = FormatTryInfos(tryInfo);
throw new TimeoutException($"Retry '{description}' timed out after {tries} tries over {FormatDuration(duration)}.{Environment.NewLine}{info}");
}
var sw = Stopwatch.StartNew();
try
{
action();
return;
}
catch (Exception ex)
{
tryInfo.Add((ex, sw.Elapsed));
tries++;
}
Sleep(retryTime);
}
}
private static string FormatTryInfos(List<(Exception, TimeSpan)> tryInfo)
{
return string.Join(Environment.NewLine, tryInfo.Select(FormatTryInfo).ToArray());
}
private static string FormatTryInfo((Exception, TimeSpan) info, int index)
{
return $"Attempt {index} took {FormatDuration(info.Item2)} and failed with exception {info.Item1}.";
}
private static Action<int> failedCallback = i => { };
public static void SetRetryFailedCallback(Action<int> onRetryFailed)
{
failedCallback = onRetryFailed;
Retry(action, maxTimeout, retryTime, description, f => { });
}
public static T Retry<T>(Func<T> action, TimeSpan maxTimeout, TimeSpan retryTime, string description)
{
var start = DateTime.UtcNow;
var tries = 1;
var exceptions = new List<Exception>();
return Retry(action, maxTimeout, retryTime, description, f => { });
}
while (true)
{
var duration = DateTime.UtcNow - start;
if (duration > maxTimeout)
{
throw new TimeoutException($"Retry '{description}' timed out after {tries} tries over {FormatDuration(duration)}.", new AggregateException(exceptions));
}
public static void Retry(Action action, TimeSpan maxTimeout, TimeSpan retryTime, string description, Action<Failure> onFail)
{
var r = new Retry(description, maxTimeout, retryTime, onFail);
r.Run(action);
}
try
{
return action();
}
catch (Exception ex)
{
exceptions.Add(ex);
failedCallback(tries);
tries++;
}
Sleep(retryTime);
}
public static T Retry<T>(Func<T> action, TimeSpan maxTimeout, TimeSpan retryTime, string description, Action<Failure> onFail)
{
var r = new Retry(description, maxTimeout, retryTime, onFail);
return r.Run(action);
}
}
}
@@ -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<IHasBlock>();
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()
);
}
}
}
@@ -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<ChainStateRequest> requests = new List<ChainStateRequest>();
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);
}
}
}
@@ -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<int, EthAddress> hosts = new Dictionary<int, EthAddress>();
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();
}
}
}
@@ -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)
{
}
}
}
@@ -2,10 +2,8 @@
using GethPlugin;
using Logging;
using Nethereum.ABI;
using Nethereum.Hex.HexTypes;
using Nethereum.Util;
using NethereumWorkflow;
using NethereumWorkflow.BlockUtils;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Utils;
@@ -22,13 +20,10 @@ 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))]
@@ -81,65 +76,14 @@ namespace CodexContractsPlugin
return balance.TstWei();
}
public Request[] GetStorageRequests(BlockInterval blockRange)
public ICodexContractsEvents GetEvents(TimeRange timeRange)
{
var events = gethNode.GetEvents<StorageRequestedEventDTO>(Deployment.MarketplaceAddress, blockRange);
var i = StartInteraction();
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();
return GetEvents(gethNode.ConvertTimeRangeToBlockRange(timeRange));
}
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange)
public ICodexContractsEvents GetEvents(BlockInterval blockInterval)
{
var events = gethNode.GetEvents<RequestFulfilledEventDTO>(Deployment.MarketplaceAddress, blockRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
public RequestCancelledEventDTO[] GetRequestCancelledEvents(BlockInterval blockRange)
{
var events = gethNode.GetEvents<RequestCancelledEventDTO>(Deployment.MarketplaceAddress, blockRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
public SlotFilledEventDTO[] GetSlotFilledEvents(BlockInterval blockRange)
{
var events = gethNode.GetEvents<SlotFilledEventDTO>(Deployment.MarketplaceAddress, blockRange);
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(BlockInterval blockRange)
{
var events = gethNode.GetEvents<SlotFreedEventDTO>(Deployment.MarketplaceAddress, blockRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
return new CodexContractsEvents(log, gethNode, Deployment, blockInterval);
}
public EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex)
@@ -170,17 +114,6 @@ namespace CodexContractsPlugin
return gethNode.Call<RequestStateFunction, RequestState>(Deployment.MarketplaceAddress, func);
}
private BlockTimeEntry GetBlock(ulong number)
{
return gethNode.GetBlockForNumber(number);
}
private EthAddress GetEthAddressFromTransaction(string transactionHash)
{
var transaction = gethNode.GetTransaction(transactionHash);
return new EthAddress(transaction.From);
}
private ContractInteractions StartInteraction()
{
return new ContractInteractions(log, gethNode);
@@ -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<StorageRequestedEventDTO>(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<RequestFulfilledEventDTO>(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<RequestCancelledEventDTO>(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<SlotFilledEventDTO>(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<SlotFreedEventDTO>(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);
}
}
}
@@ -5,35 +5,49 @@ using Newtonsoft.Json;
namespace CodexContractsPlugin.Marketplace
{
public partial class Request : RequestBase
public interface IHasBlock
{
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
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
}
public partial class RequestCancelledEventDTO
public partial class RequestCancelledEventDTO : IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
}
public partial class SlotFilledEventDTO
public partial class SlotFilledEventDTO : IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
public EthAddress Host { get; set; }
}
public partial class SlotFreedEventDTO
public partial class SlotFreedEventDTO : IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
@@ -50,6 +50,7 @@ namespace CodexDiscordBotPlugin
startupConfig.Add(config);
var pod = workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig).WaitForOnline();
WaitForStartupMessage(workflow, pod);
workflow.CreateCrashWatcher(pod.Containers.Single()).Start();
return pod;
}
@@ -58,7 +59,9 @@ namespace CodexDiscordBotPlugin
var startupConfig = new StartupConfig();
startupConfig.NameOverride = config.Name;
startupConfig.Add(config);
return workflow.Start(1, new RewarderBotContainerRecipe(), startupConfig).WaitForOnline();
var pod = workflow.Start(1, new RewarderBotContainerRecipe(), startupConfig).WaitForOnline();
workflow.CreateCrashWatcher(pod.Containers.Single()).Start();
return pod;
}
private void WaitForStartupMessage(IStartupWorkflow workflow, RunningPod pod)
@@ -7,7 +7,7 @@ namespace CodexDiscordBotPlugin
public class DiscordBotContainerRecipe : ContainerRecipeFactory
{
public override string AppName => "discordbot-bibliotech";
public override string Image => "codexstorage/codex-discordbot:sha-22cf82b";
public override string Image => "codexstorage/codex-discordbot:sha-8033da1";
public static string RewardsPort = "bot_rewards_port";
@@ -7,7 +7,7 @@ namespace CodexDiscordBotPlugin
public class RewarderBotContainerRecipe : ContainerRecipeFactory
{
public override string AppName => "discordbot-rewarder";
public override string Image => "codexstorage/codex-rewarderbot:sha-12dc7ef";
public override string Image => "codexstorage/codex-rewarderbot:sha-8033da1";
protected override void Initialize(StartupConfig startupConfig)
{
+1 -1
View File
@@ -9,7 +9,7 @@ namespace CodexPlugin
public class ApiChecker
{
// <INSERT-OPENAPI-YAML-HASH>
private const string OpenApiYamlHash = "0F-C8-02-1E-2C-2C-15-F6-91-6A-01-31-11-49-95-06-79-26-25-BF-27-3C-A8-2E-5F-7F-34-FD-C0-57-A0-9A";
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";
+150 -48
View File
@@ -2,25 +2,26 @@
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, RunningPod container, CrashWatcher crashWatcher)
{
this.tools = tools;
log = tools.GetLog();
Container = container;
CrashWatcher = crashWatcher;
hasContainerCrashed = false;
CrashWatcher.Start(this);
CrashWatcher.Start();
}
public RunningPod Container { get; }
@@ -34,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<DebugPeer>(str);
result.IsPeerFound = true;
return result;
if (str.ToLowerInvariant() == "unable to find peer!")
{
return new DebugPeer
{
IsPeerFound = false
};
}
var result = endpoint.Deserialize<DebugPeer>(str);
result.IsPeerFound = true;
return result;
});
}
public void ConnectToPeer(string peerId, string[] peerMultiAddresses)
@@ -59,14 +63,19 @@ namespace CodexPlugin
});
}
public string UploadFile(FileStream fileStream)
public string UploadFile(FileStream fileStream, Action<Failure> 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<Failure> 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;
}
@@ -89,15 +98,24 @@ namespace CodexPlugin
return OnCodex<string>(api => api.CreateStorageRequestAsync(request.ContentId.Id, body));
}
public CodexSpace Space()
{
var space = OnCodex<Space>(api => api.SpaceAsync());
return mapper.Map(space);
}
public StoragePurchase GetPurchaseStatus(string purchaseId)
{
var endpoint = GetEndpoint();
return Time.Retry(() =>
return CrashCheck(() =>
{
var str = endpoint.HttpGetString($"storage/purchases/{purchaseId}");
if (string.IsNullOrEmpty(str)) throw new Exception("Empty response.");
return JsonConvert.DeserializeObject<StoragePurchase>(str)!;
}, nameof(GetPurchaseStatus));
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<StoragePurchase>(str)!;
}, nameof(GetPurchaseStatus));
});
// TODO: current getpurchase api does not line up with its openapi spec.
// return mapper.Map(OnCodex(api => api.GetPurchaseAsync(purchaseId)));
@@ -114,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<T>(Func<CodexApi, Task<T>> 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<T>(Func<CodexApi, Task<T>> action, Retry retry)
{
var result = tools.CreateHttp(CheckContainerCrashed).OnClient(client => CallCodex(client, action), retry);
return result;
}
private T CallCodex<T>(HttpClient client, Func<CodexApi, Task<T>> 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<T>(Func<T> action)
{
try
{
return action();
}
finally
{
CrashWatcher.HasContainerCrashed();
}
}
private IEndpoint GetEndpoint()
{
return tools
@@ -136,31 +202,67 @@ namespace CodexPlugin
private Address GetAddress()
{
return Container.Containers.Single().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<Failure> 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}");
}
}
}
@@ -7,7 +7,7 @@ namespace CodexPlugin
{
public class CodexContainerRecipe : ContainerRecipeFactory
{
private const string DefaultDockerImage = "codexstorage/nim-codex:sha-267266a-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";
@@ -120,7 +120,7 @@ namespace CodexPlugin
}
}
if(!string.IsNullOrEmpty(config.NameOverride))
if (!string.IsNullOrEmpty(config.NameOverride))
{
AddEnvVar("CODEX_NODENAME", config.NameOverride);
}
@@ -160,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);
}
+47 -10
View File
@@ -15,8 +15,11 @@ namespace CodexPlugin
DebugInfo GetDebugInfo();
DebugPeer GetDebugPeer(string peerId);
ContentId UploadFile(TrackedFile file);
ContentId UploadFile(TrackedFile file, Action<Failure> onFailure);
TrackedFile? DownloadContent(ContentId contentId, string fileLabel = "");
TrackedFile? DownloadContent(ContentId contentId, Action<Failure> onFailure, string fileLabel = "");
LocalDatasetList LocalFiles();
CodexSpace Space();
void ConnectToPeer(ICodexNode node);
DebugInfoVersion Version { get; }
IMarketplaceAccess Marketplace { get; }
@@ -24,6 +27,12 @@ namespace CodexPlugin
PodInfo GetPodInfo();
ITransferSpeeds TransferSpeeds { get; }
EthAccount EthAccount { get; }
/// <summary>
/// Warning! The node is not usable after this.
/// TODO: Replace with delete-blocks debug call once available in Codex.
/// </summary>
void DeleteRepoFolder();
void Stop(bool waitTillStopped);
}
@@ -90,7 +99,7 @@ namespace CodexPlugin
{
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;
}
@@ -101,13 +110,20 @@ namespace CodexPlugin
public ContentId UploadFile(TrackedFile file)
{
return UploadFile(file, DoNothing);
}
public ContentId UploadFile(TrackedFile file, Action<Failure> 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;
@@ -117,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<Failure> 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;
@@ -136,6 +159,11 @@ namespace CodexPlugin
return CodexAccess.LocalFiles();
}
public CodexSpace Space()
{
return CodexAccess.Space();
}
public void ConnectToPeer(ICodexNode node)
{
var peer = (CodexNode)node;
@@ -152,15 +180,16 @@ namespace CodexPlugin
return CodexAccess.GetPodInfo();
}
public void DeleteRepoFolder()
{
CodexAccess.DeleteRepoFolder();
}
public void Stop(bool waitTillStopped)
{
Log("Stopping...");
CrashWatcher.Stop();
Group.Stop(this, 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);
}
public void EnsureOnlineGetVersionResponse()
@@ -192,12 +221,14 @@ namespace CodexPlugin
.ToArray();
}
private void DownloadToFile(string contentId, TrackedFile file)
private void DownloadToFile(string contentId, TrackedFile file, Action<Failure> 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
@@ -205,6 +236,8 @@ namespace CodexPlugin
Log($"Failed to download file '{contentId}'.");
throw;
}
CodexAccess.LogDiskSpace("After download");
}
private void EnsureMarketplace()
@@ -216,5 +249,9 @@ namespace CodexPlugin
{
tools.GetLog().Log($"{GetName()}: {msg}");
}
private void DoNothing(Failure failure)
{
}
}
}
+1 -1
View File
@@ -33,7 +33,7 @@ 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}"));
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();
+14
View File
@@ -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);
}
}
}
+64 -9
View File
@@ -1,4 +1,5 @@
using CodexContractsPlugin;
using CodexOpenApi;
using Newtonsoft.Json.Linq;
using System.Numerics;
using Utils;
@@ -62,14 +63,47 @@ namespace CodexPlugin
};
}
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)
{
@@ -84,6 +118,17 @@ namespace CodexPlugin
};
}
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
@@ -1,5 +1,7 @@
using CodexContractsPlugin;
using CodexOpenApi;
using Logging;
using System.Data;
using Utils;
namespace CodexPlugin
@@ -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
@@ -6,6 +6,9 @@ namespace CodexPlugin
{
public interface IStoragePurchaseContract
{
string PurchaseId { get; }
StoragePurchaseRequest Purchase { get; }
ContentId ContentId { get; }
void WaitForStorageContractSubmitted();
void WaitForStorageContractStarted();
void WaitForStorageContractFinished();
@@ -27,10 +30,13 @@ namespace CodexPlugin
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;
+5
View File
@@ -289,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
@@ -303,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:
@@ -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<string>();
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<string> lines, Action<List<string>> values)
{
var list = new List<string>();
values(list);
lines.Add(string.Join(",", list));
}
}
public class MetricsSet
@@ -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.");
}
}
+1 -1
View File
@@ -64,7 +64,7 @@ namespace ContinuousTests
}
finally
{
entryPoint.Tools.CreateWorkflow().DeleteNamespace();
entryPoint.Tools.CreateWorkflow().DeleteNamespace(wait: false);
}
}
+2 -1
View File
@@ -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();
}
@@ -50,8 +50,6 @@ namespace CodexLongTests.BasicTests
var node = StartCodex(s => s.WithStorageQuota((size + 10).MB()));
Time.SetRetryFailedCallback(i => OnFailed(i, node));
var uploadStart = DateTime.UtcNow;
var cid = node.UploadFile(expectedFile);
var downloadStart = DateTime.UtcNow;
@@ -62,17 +60,6 @@ namespace CodexLongTests.BasicTests
AssertTimeConstraint(uploadStart, downloadStart, downloadFinished, size);
}
private void OnFailed(int tries, ICodexNode node)
{
if (tries < 5) return;
if (tries % 10 == 0)
{
Log($"After try {tries}, downloading node log.");
Ci.DownloadLog(node);
}
}
private void AssertTimeConstraint(DateTime uploadStart, DateTime downloadStart, DateTime downloadFinished, long size)
{
float sizeInMB = size;
@@ -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++) StartCodex();
var nodes = StartCodex(numberOfNodes);
CreatePeerDownloadTestHelpers().AssertFullDownloadInterconnectivity(GetAllOnlineCodexNodes(), sizeMBs.MB());
CreatePeerDownloadTestHelpers().AssertFullDownloadInterconnectivity(nodes, sizeMBs.MB());
}
}
}
@@ -17,12 +17,13 @@ public class ScalabilityTests : CodexDistTest
[Combinatorial]
[UseLongTimeouts]
[DontDownloadLogs]
[WaitForCleanup]
public void ShouldMaintainFileInNetwork(
[Values(10, 40, 80, 100)] int numberOfNodes,
[Values(100, 1000, 5000, 10000)] int fileSizeInMb
[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.Info;
var logLevel = CodexLogLevel.Trace;
var bootstrap = StartCodex(s => s.WithLogLevel(logLevel));
var nodes = StartCodex(numberOfNodes - 1, s => s
@@ -35,17 +36,27 @@ public class ScalabilityTests : CodexDistTest
var downloader = nodes.PickOneRandom();
var testFile = GenerateTestFile(fileSizeInMb.MB());
var contentId = uploader.UploadFile(testFile);
var downloadedFile = downloader.DownloadContent(contentId);
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();
}
/// <summary>
@@ -57,6 +68,7 @@ public class ScalabilityTests : CodexDistTest
[Combinatorial]
[UseLongTimeouts]
[DontDownloadLogs]
[WaitForCleanup]
public void EveryoneGetsAFile(
[Values(10, 40, 80, 100)] int numberOfNodes,
[Values(100, 1000, 5000, 10000)] int fileSizeInMb
@@ -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]
@@ -33,10 +33,9 @@ namespace CodexTests.BasicTests
.AsStorageNode()
.AsValidator()));
var expectedHostBalance = (numberOfHosts * hostInitialBalance.TstWei).TstWei();
foreach (var host in hosts)
{
AssertBalance(contracts, host, Is.EqualTo(expectedHostBalance));
AssertBalance(contracts, host, Is.EqualTo(hostInitialBalance));
var availability = new StorageAvailability(
totalSpace: 10.GB(),
@@ -85,16 +84,56 @@ 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();
Debug($"SlotFilledEvents: {slotFilledEvents.Length} - NumSlots: {purchase.MinRequiredNumberOfNodes}");
if (slotFilledEvents.Length != purchase.MinRequiredNumberOfNodes) throw new Exception();
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");
}
@@ -107,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();
}
+1 -11
View File
@@ -13,18 +13,8 @@ namespace CodexTests.BasicTests
var primary = StartCodex();
PerformOneClientTest(primary);
}
[Test]
public void RestartTest()
{
var primary = StartCodex();
primary.Stop(waitTillStopped: true);
primary = StartCodex();
PerformOneClientTest(primary);
LogNodeStatus(primary);
}
private void PerformOneClientTest(ICodexNode primary)
+31 -1
View File
@@ -1,4 +1,5 @@
using NUnit.Framework;
using CodexPlugin;
using NUnit.Framework;
using Utils;
namespace CodexTests.BasicTests
@@ -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));
}
}
}
}
+24 -27
View File
@@ -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<TestLifecycle, List<ICodexNode>> onlineCodexNodes = new Dictionary<TestLifecycle, List<ICodexNode>>();
public CodexDistTest()
{
ProjectPlugin.Load<CodexPlugin.CodexPlugin>();
@@ -29,16 +29,6 @@ namespace CodexTests
localBuilder.Build();
}
protected override void LifecycleStart(TestLifecycle lifecycle)
{
onlineCodexNodes.Add(lifecycle, new List<ICodexNode>());
}
protected override void LifecycleStop(TestLifecycle lifecycle)
{
onlineCodexNodes.Remove(lifecycle);
}
public ICodexNode StartCodex()
{
return StartCodex(s => { });
@@ -61,7 +51,7 @@ namespace CodexTests
setup(s);
OnCodexSetup(s);
});
onlineCodexNodes[Get()].AddRange(group);
return group;
}
@@ -75,11 +65,6 @@ namespace CodexTests
return new PeerDownloadTestHelpers(GetTestLog(), Get().GetFileManager());
}
public IEnumerable<ICodexNode> 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<string, string> 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());
}
}
}
@@ -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()
{
StartCodex(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);
StartCodex(2, s => s.EnableMarketplace(geth, contracts, m => m
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)
{
StartCodex(numberOfNodes);
var nodes = StartCodex(numberOfNodes);
AssertAllNodesConnected(sizeMBs);
AssertAllNodesConnected(nodes, sizeMBs);
}
private void AssertAllNodesConnected(int sizeMBs = 10)
private void AssertAllNodesConnected(IEnumerable<ICodexNode> nodes, int sizeMBs = 10)
{
CreatePeerDownloadTestHelpers().AssertFullDownloadInterconnectivity(GetAllOnlineCodexNodes(), sizeMBs.MB());
CreatePeerDownloadTestHelpers().AssertFullDownloadInterconnectivity(nodes, sizeMBs.MB());
}
}
}
@@ -1,4 +1,5 @@
using NUnit.Framework;
using CodexPlugin;
using NUnit.Framework;
namespace CodexTests.PeerDiscoveryTests
{
@@ -13,7 +14,7 @@ namespace CodexTests.PeerDiscoveryTests
var l1Node = StartCodex(s => s.WithBootstrapNode(root));
var l2Target = StartCodex(s => s.WithBootstrapNode(l1Node));
AssertAllNodesConnected();
AssertAllNodesConnected(root, l1Source, l1Node, l2Target);
}
[Test]
@@ -25,7 +26,7 @@ namespace CodexTests.PeerDiscoveryTests
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 nodes = new List<ICodexNode>();
var node = StartCodex();
nodes.Add(node);
for (var i = 1; i < chainLength; i++)
{
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);
}
}
}
@@ -21,9 +21,9 @@ namespace CodexTests.PeerDiscoveryTests
[Test]
public void MetricsDoesNotInterfereWithPeerDiscovery()
{
StartCodex(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);
StartCodex(2, s => s.EnableMarketplace(geth, contracts, m => m
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)
{
StartCodex(number);
var nodes = StartCodex(number);
AssertAllNodesConnected();
AssertAllNodesConnected(nodes);
}
private void AssertAllNodesConnected()
private void AssertAllNodesConnected(IEnumerable<ICodexNode> nodes)
{
var allNodes = GetAllOnlineCodexNodes();
CreatePeerConnectionTestHelpers().AssertFullyConnected(allNodes);
CheckRoutingTable(allNodes);
nodes = nodes.Concat(new[] { BootstrapNode }).ToArray()!;
CreatePeerConnectionTestHelpers().AssertFullyConnected(nodes);
CheckRoutingTable(nodes);
}
private void CheckRoutingTable(IEnumerable<ICodexNode> allNodes)
+76 -152
View File
@@ -1,15 +1,14 @@
using CodexContractsPlugin;
using CodexContractsPlugin.Marketplace;
using CodexDiscordBotPlugin;
using CodexPlugin;
using Core;
using DiscordRewards;
using DistTestCore;
using GethPlugin;
using KubernetesWorkflow.Types;
using Logging;
using Newtonsoft.Json;
using NUnit.Framework;
using TestNetRewarder;
using Utils;
namespace CodexTests.UtilityTests
@@ -24,62 +23,90 @@ namespace CodexTests.UtilityTests
private readonly List<EthAccount> hostAccounts = new List<EthAccount>();
private readonly List<ulong> rewardsSeen = new List<ulong>();
private readonly TimeSpan rewarderInterval = TimeSpan.FromMinutes(1);
private readonly List<string> receivedEvents = new List<string>();
private readonly List<MarketAverage> receivedAverages = new List<MarketAverage>();
[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 monitor = new ChainMonitor(contracts, geth, GetTestLog());
monitor.Start();
var botContainer = StartDiscordBot(gethInfo);
StartHosts(geth, contracts);
var rewarderContainer = StartRewarderBot(gethInfo, botContainer);
StartHosts(geth, contracts);
var client = StartClient(geth, contracts);
var apiCalls = new RewardApiCalls(Ci, botContainer);
var apiCalls = new RewardApiCalls(GetTestLog(), Ci, botContainer);
apiCalls.Start(OnCommand);
var rewarderLog = new RewarderLogMonitor(Ci, rewarderContainer.Containers.Single());
rewarderLog.Start(l => Log("Rewarder ChainState: " + l));
var purchaseContract = ClientPurchasesStorage(client);
purchaseContract.WaitForStorageContractStarted();
purchaseContract.WaitForStorageContractFinished();
Thread.Sleep(rewarderInterval * 3);
rewarderLog.Stop();
apiCalls.Stop();
monitor.Stop();
Log("Done!");
AssertEventOccurance("Created as New.", 1);
AssertEventOccurance("SlotFilled", Convert.ToInt32(GetNumberOfRequiredHosts()));
AssertEventOccurance("Transit: New -> Started", 1);
AssertEventOccurance("Transit: Started -> Finished", 1);
Thread.Sleep(rewarderInterval * 2);
Log("Seen:");
foreach (var seen in rewardsSeen)
{
Log(seen.ToString());
}
Log("");
AssertMarketAverage();
foreach (var r in repo.Rewards)
{
var seen = rewardsSeen.Any(s => r.RoleId == s);
Log($"{r.RoleId} = {seen}");
Log($"{Lookup(r.RoleId)} = {seen}");
}
Assert.That(repo.Rewards.All(r => rewardsSeen.Contains(r.RoleId)));
}
private void OnCommand(GiveRewardsCommand call)
private string Lookup(ulong rewardId)
{
if (call.Averages.Any()) Log($"{call.Averages.Length} average.");
if (call.EventsOverview.Any()) Log($"{call.EventsOverview.Length} events.");
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($"<API call {timestamp}>");
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);
@@ -87,9 +114,10 @@ namespace CodexTests.UtilityTests
foreach (var address in r.UserAddresses)
{
var user = IdentifyAccount(address);
Log(user + ": " + reward.Message);
Log("\tReward: " + user + ": " + reward.Message);
}
}
Log($"</API call>");
}
private IStoragePurchaseContract ClientPurchasesStorage(ICodexNode client)
@@ -103,8 +131,8 @@ namespace CodexTests.UtilityTests
MinRequiredNumberOfNodes = GetNumberOfRequiredHosts(),
NodeFailureTolerance = 2,
ProofProbability = 5,
Duration = TimeSpan.FromMinutes(6),
Expiry = TimeSpan.FromMinutes(5)
Duration = GetMinRequiredRequestDuration(),
Expiry = GetMinRequiredRequestDuration() - TimeSpan.FromMinutes(1)
};
return client.Marketplace.RequestStorage(purchase);
@@ -226,6 +254,11 @@ namespace CodexTests.UtilityTests
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";
@@ -243,14 +276,13 @@ namespace CodexTests.UtilityTests
public class RewardApiCalls
{
private readonly ContainerFileMonitor monitor;
private readonly Dictionary<string, GiveRewardsCommand> commands = new Dictionary<string, GiveRewardsCommand>();
public RewardApiCalls(CoreInterface ci, RunningContainer botContainer)
public RewardApiCalls(ILog log, CoreInterface ci, RunningContainer botContainer)
{
monitor = new ContainerFileMonitor(ci, botContainer, "/app/datapath/logs/discordbot.log");
monitor = new ContainerFileMonitor(log, ci, botContainer, "/app/datapath/logs/discordbot.log");
}
public void Start(Action<GiveRewardsCommand> onCommand)
public void Start(Action<string, GiveRewardsCommand> onCommand)
{
monitor.Start(line => ParseLine(line, onCommand));
}
@@ -260,19 +292,17 @@ namespace CodexTests.UtilityTests
monitor.Stop();
}
private void ParseLine(string line, Action<GiveRewardsCommand> onCommand)
private void ParseLine(string line, Action<string, GiveRewardsCommand> onCommand)
{
try
{
var timestamp = line.Substring(0, 30);
if (commands.ContainsKey(timestamp)) return;
var json = line.Substring(31);
var cmd = JsonConvert.DeserializeObject<GiveRewardsCommand>(json);
if (cmd != null)
{
commands.Add(timestamp, cmd);
onCommand(cmd);
onCommand(timestamp, cmd);
}
}
catch
@@ -281,57 +311,9 @@ namespace CodexTests.UtilityTests
}
}
public class RewarderLogMonitor
{
private readonly ContainerFileMonitor monitor;
private readonly Dictionary<string, GiveRewardsCommand> commands = new Dictionary<string, GiveRewardsCommand>();
public RewarderLogMonitor(CoreInterface ci, RunningContainer botContainer)
{
monitor = new ContainerFileMonitor(ci, botContainer, "/app/datapath/logs/testnetrewarder.log");
}
public void Start(Action<string> onCommand)
{
monitor.Start(l => ProcessLine(l, onCommand));
}
public void Stop()
{
monitor.Stop();
}
private void ProcessLine(string line, Action<string> log)
{
//$"ChainState=[{JsonConvert.SerializeObject(this)}]" +
//$"HistoricState=[{historicState.EntireString()}]";
var stateOpenTag = "ChainState=[";
var historicOpenTag = "]HistoricState=[";
if (!line.Contains(stateOpenTag)) return;
if (!line.Contains(historicOpenTag)) return;
var stateStr = Between(line, stateOpenTag, historicOpenTag);
var historicStr = Between(line, historicOpenTag, "]");
var chainState = JsonConvert.DeserializeObject<ChainState>(stateStr);
var historicState = JsonConvert.DeserializeObject<TestNetRewarder.StorageRequest[]>(historicStr)!;
chainState!.Set(new HistoricState(historicState));
log(string.Join(",", chainState!.GenerateOverview()));
}
private string Between(string s, string open, string close)
{
var start = s.IndexOf(open) + open.Length;
var end = s.LastIndexOf(close);
return s.Substring(start, end - start);
}
}
public class ContainerFileMonitor
{
private readonly ILog log;
private readonly CoreInterface ci;
private readonly RunningContainer botContainer;
private readonly string filePath;
@@ -340,8 +322,9 @@ namespace CodexTests.UtilityTests
private Task worker = Task.CompletedTask;
private Action<string> onNewLine = c => { };
public ContainerFileMonitor(CoreInterface ci, RunningContainer botContainer, string filePath)
public ContainerFileMonitor(ILog log, CoreInterface ci, RunningContainer botContainer, string filePath)
{
this.log = log;
this.ci = ci;
this.botContainer = botContainer;
this.filePath = filePath;
@@ -359,6 +342,9 @@ namespace CodexTests.UtilityTests
worker.Wait();
}
// did any container crash? that's why it repeats?
private void Worker()
{
while (!cts.IsCancellationRequested)
@@ -376,6 +362,8 @@ namespace CodexTests.UtilityTests
var lines = botLog.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
// log.Log("line: " + line);
if (!seenLines.Contains(line))
{
seenLines.Add(line);
@@ -384,69 +372,5 @@ namespace CodexTests.UtilityTests
}
}
}
public class ChainMonitor
{
private readonly ICodexContracts contracts;
private readonly IGethNode geth;
private readonly ILog log;
private readonly CancellationTokenSource cts = new CancellationTokenSource();
private Task worker = Task.CompletedTask;
private DateTime last = DateTime.UtcNow;
public ChainMonitor(ICodexContracts contracts, IGethNode geth, ILog log)
{
this.contracts = contracts;
this.geth = geth;
this.log = log;
}
public void Start()
{
last = DateTime.UtcNow;
worker = Task.Run(Worker);
}
public void Stop()
{
cts.Cancel();
worker.Wait();
}
private void Worker()
{
while (!cts.IsCancellationRequested)
{
Thread.Sleep(TimeSpan.FromSeconds(10));
if (cts.IsCancellationRequested) return;
Update();
}
}
private void Update()
{
var start = last;
var stop = DateTime.UtcNow;
last = stop;
var range = geth.ConvertTimeRangeToBlockRange(new TimeRange(start, stop));
LogEvents(nameof(contracts.GetStorageRequests), contracts.GetStorageRequests, range);
LogEvents(nameof(contracts.GetRequestFulfilledEvents), contracts.GetRequestFulfilledEvents, range);
LogEvents(nameof(contracts.GetRequestCancelledEvents), contracts.GetRequestCancelledEvents, range);
LogEvents(nameof(contracts.GetSlotFilledEvents), contracts.GetSlotFilledEvents, range);
LogEvents(nameof(contracts.GetSlotFreedEvents), contracts.GetSlotFreedEvents, range);
}
private void LogEvents(string n, Func<BlockInterval, object> f, BlockInterval r)
{
var a = (object[])f(r);
a.ToList().ForEach(request => log.Log(n + " - " + JsonConvert.SerializeObject(request)));
}
}
}
}
+16 -4
View File
@@ -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
);
}
@@ -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,6 +242,11 @@ namespace DistTestCore
return new DefaultTimeSet();
}
private bool ShouldWaitForCleanup()
{
return CurrentTestMethodHasAttribute<WaitForCleanupAttribute>();
}
private bool ShouldUseLongTimeouts()
{
return CurrentTestMethodHasAttribute<UseLongTimeoutsAttribute>();
+5 -3
View File
@@ -16,7 +16,7 @@ namespace DistTestCore
private readonly List<RunningPod> 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
);
}
@@ -0,0 +1,15 @@
using NUnit.Framework;
namespace DistTestCore
{
/// <summary>
/// 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.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class WaitForCleanupAttribute : PropertyAttribute
{
}
}
+16
View File
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\ArgsUniform\ArgsUniform.csproj" />
<ProjectReference Include="..\..\Framework\Logging\Logging.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexPlugin\CodexPlugin.csproj" />
</ItemGroup>
</Project>
+45
View File
@@ -0,0 +1,45 @@
using ArgsUniform;
namespace AutoClient
{
public class Configuration
{
[Uniform("codex-host", "ch", "CODEXHOST", false, "Codex Host address. (default 'http://localhost')")]
public string CodexHost { get; set; } = "http://localhost";
[Uniform("codex-port", "cp", "CODEXPORT", false, "port number of Codex API. (8080 by default)")]
public int CodexPort { get; set; } = 8080;
[Uniform("datapath", "dp", "DATAPATH", false, "Root path where all data files will be saved.")]
public string DataPath { get; set; } = "datapath";
[Uniform("purchases", "np", "PURCHASES", false, "Number of concurrent purchases.")]
public int NumConcurrentPurchases { get; set; } = 10;
[Uniform("contract-duration", "cd", "CONTRACTDURATION", false, "contract duration in minutes. (default 30)")]
public int ContractDurationMinutes { get; set; } = 30;
[Uniform("contract-expiry", "ce", "CONTRACTEXPIRY", false, "contract expiry in minutes. (default 15)")]
public int ContractExpiryMinutes { get; set; } = 15;
[Uniform("num-hosts", "nh", "NUMHOSTS", false, "Number of hosts for contract. (default 5)")]
public int NumHosts { get; set; } = 5;
[Uniform("num-hosts-tolerance", "nt", "NUMTOL", false, "Number of host tolerance for contract. (default 2)")]
public int HostTolerance { get; set; } = 2;
[Uniform("price","p", "PRICE", false, "Price of contract. (default 10)")]
public int Price { get; set; } = 10;
[Uniform("collateral", "c", "COLLATERAL", false, "Required collateral. (default 1)")]
public int RequiredCollateral { get; set; } = 1;
public string LogPath
{
get
{
return Path.Combine(DataPath, "logs");
}
}
}
}
+17
View File
@@ -0,0 +1,17 @@
namespace AutoClient
{
public class ImageGenerator
{
public async Task<string> GenerateImage()
{
var httpClient = new HttpClient();
var thing = await httpClient.GetStreamAsync("https://picsum.photos/3840/2160");
var filename = $"{Guid.NewGuid().ToString().ToLowerInvariant()}.jpg";
using var file = File.OpenWrite(filename);
await thing.CopyToAsync(file);
return filename;
}
}
}
+80
View File
@@ -0,0 +1,80 @@
using ArgsUniform;
using AutoClient;
using CodexOpenApi;
using Core;
using Logging;
public static class Program
{
public static async Task Main(string[] args)
{
var cts = new CancellationTokenSource();
var cancellationToken = cts.Token;
Console.CancelKeyPress += (sender, args) => cts.Cancel();
var uniformArgs = new ArgsUniform<Configuration>(PrintHelp, args);
var config = uniformArgs.Parse(true);
if (config.NumConcurrentPurchases < 1)
{
throw new Exception("Number of concurrent purchases must be > 0");
}
var log = new LogSplitter(
new FileLog(Path.Combine(config.LogPath, "autoclient")),
new ConsoleLog()
);
var address = new Utils.Address(
host: config.CodexHost,
port: config.CodexPort
);
log.Log($"Start. Address: {address}");
var imgGenerator = new ImageGenerator();
var client = new HttpClient();
var codex = new CodexApi(client);
codex.BaseUrl = $"{address.Host}:{address.Port}/api/codex/v1";
await CheckCodex(codex, log);
var purchasers = new List<Purchaser>();
for (var i = 0; i < config.NumConcurrentPurchases; i++)
{
purchasers.Add(
new Purchaser(new LogPrefixer(log, $"({i}) "), client, address, codex, cancellationToken, config, imgGenerator)
);
}
var delayPerPurchaser = TimeSpan.FromMinutes(config.ContractDurationMinutes) / config.NumConcurrentPurchases;
foreach (var purchaser in purchasers)
{
purchaser.Start();
await Task.Delay(delayPerPurchaser);
}
log.Log("Done.");
}
private static async Task CheckCodex(CodexApi codex, ILog log)
{
log.Log("Checking Codex...");
try
{
var info = await codex.GetDebugInfoAsync();
if (string.IsNullOrEmpty(info.Id)) throw new Exception("Failed to fetch Codex node id");
}
catch (Exception ex)
{
log.Log($"Codex not OK: {ex}");
throw;
}
}
private static void PrintHelp()
{
Console.WriteLine("Generates fake data and creates Codex storage contracts for it.");
}
}
+166
View File
@@ -0,0 +1,166 @@
using CodexOpenApi;
using CodexPlugin;
using Logging;
using Newtonsoft.Json;
using Utils;
namespace AutoClient
{
public class Purchaser
{
private readonly ILog log;
private readonly HttpClient client;
private readonly Address address;
private readonly CodexApi codex;
private readonly CancellationToken ct;
private readonly Configuration config;
private readonly ImageGenerator generator;
public Purchaser(ILog log, HttpClient client, Address address, CodexApi codex, CancellationToken ct, Configuration config, ImageGenerator generator)
{
this.log = log;
this.client = client;
this.address = address;
this.codex = codex;
this.ct = ct;
this.config = config;
this.generator = generator;
}
public void Start()
{
Task.Run(Worker);
}
private async Task Worker()
{
while (!ct.IsCancellationRequested)
{
var pid = await StartNewPurchase();
await WaitTillFinished(pid);
}
}
private async Task<string> StartNewPurchase()
{
var file = await CreateFile();
var cid = await UploadFile(file);
return await RequestStorage(cid);
}
private async Task<string> CreateFile()
{
return await generator.GenerateImage();
}
private async Task<ContentId> UploadFile(string filename)
{
// Copied from CodexNode :/
using var fileStream = File.OpenRead(filename);
log.Log($"Uploading file {filename}...");
var response = await codex.UploadAsync(fileStream, ct);
if (string.IsNullOrEmpty(response)) FrameworkAssert.Fail("Received empty response.");
if (response.StartsWith("Unable to store block")) FrameworkAssert.Fail("Node failed to store block.");
log.Log($"Uploaded file. Received contentId: '{response}'.");
return new ContentId(response);
}
private async Task<string> RequestStorage(ContentId cid)
{
log.Log("Requesting storage for " + cid.Id);
var result = await codex.CreateStorageRequestAsync(cid.Id, new StorageRequestCreation()
{
Collateral = config.RequiredCollateral.ToString(),
Duration = (config.ContractDurationMinutes * 60).ToString(),
Expiry = (config.ContractExpiryMinutes * 60).ToString(),
Nodes = config.NumHosts,
Reward = config.Price.ToString(),
ProofProbability = "15",
Tolerance = config.HostTolerance
}, ct);
log.Log("Purchase ID: " + result);
return result;
}
private async Task<string?> GetPurchaseState(string pid)
{
try
{
// openapi still don't match code.
var str = await client.GetStringAsync($"{address.Host}:{address.Port}/api/codex/v1/storage/purchases/{pid}");
if (string.IsNullOrEmpty(str)) return null;
var sp = JsonConvert.DeserializeObject<StoragePurchase>(str)!;
log.Log($"Purchase {pid} is {sp.State}");
if (!string.IsNullOrEmpty(sp.Error)) log.Log($"Purchase {pid} error is {sp.Error}");
return sp.State;
}
catch
{
return null;
}
}
private async Task WaitTillFinished(string pid)
{
log.Log("Waiting...");
try
{
var emptyResponseTolerance = 10;
while (true)
{
var status = (await GetPurchaseState(pid))?.ToLowerInvariant();
if (string.IsNullOrEmpty(status))
{
emptyResponseTolerance--;
if (emptyResponseTolerance == 0)
{
log.Log("Received 10 empty responses. Stop tracking this purchase.");
await ExpiryTimeDelay();
return;
}
}
else
{
if (status.Contains("cancel") ||
status.Contains("error") ||
status.Contains("finished"))
{
return;
}
if (status.Contains("started"))
{
await FixedDurationDelay();
}
}
await FixedShortDelay();
}
}
catch (Exception ex)
{
log.Log($"Wait failed with exception: {ex}. Assume contract will expire: Wait expiry time.");
await ExpiryTimeDelay();
}
}
private async Task FixedDurationDelay()
{
await Task.Delay(config.ContractDurationMinutes * 60 * 1000, ct);
}
private async Task ExpiryTimeDelay()
{
await Task.Delay(config.ContractExpiryMinutes * 60 * 1000, ct);
}
private async Task FixedShortDelay()
{
await Task.Delay(15 * 1000, ct);
}
}
}
+24
View File
@@ -0,0 +1,24 @@
# 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/AutoClient ./Tools/AutoClient
COPY ./Framework ./Framework
COPY ./ProjectPlugins ./ProjectPlugins
RUN dotnet restore Tools/AutoClient
RUN dotnet publish Tools/AutoClient -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}/AutoClient.dll
+10 -6
View File
@@ -3,19 +3,23 @@ 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;
}
@@ -24,12 +28,12 @@ namespace BiblioTech
{
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);
}
}
+59 -3
View File
@@ -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}'.");
}
}
}
}
}
+3 -2
View File
@@ -11,6 +11,7 @@ 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();
@@ -73,13 +74,13 @@ namespace BiblioTech
var notifyCommand = new NotifyCommand();
var associateCommand = new UserAssociateCommand(notifyCommand);
var sprCommand = new SprCommand();
var handler = new CommandHandler(client,
var handler = new CommandHandler(Log, client, replacement,
new GetBalanceCommand(associateCommand),
new MintCommand(associateCommand),
sprCommand,
associateCommand,
notifyCommand,
new AdminCommand(sprCommand),
new AdminCommand(sprCommand, replacement),
new MarketCommand()
);
@@ -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);
}
}
}
@@ -0,0 +1,34 @@
namespace BiblioTech.Rewards
{
public class CustomReplacement
{
private readonly Dictionary<string, string> replacements = new Dictionary<string, string>();
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;
}
}
}
+31 -38
View File
@@ -1,6 +1,7 @@
using Discord;
using Discord.WebSocket;
using DiscordRewards;
using Logging;
using Newtonsoft.Json;
namespace BiblioTech.Rewards
@@ -8,41 +9,49 @@ namespace BiblioTech.Rewards
public class RoleDriver : IDiscordRoleDriver
{
private readonly DiscordSocketClient client;
private readonly ILog log;
private readonly SocketTextChannel? rewardsChannel;
private readonly SocketTextChannel? eventsChannel;
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;
this.log = log;
rewardsChannel = GetChannel(Program.Config.RewardsChannelId);
eventsChannel = GetChannel(Program.Config.ChainEventsChannelId);
eventsSender = new ChainEventsSender(log, replacement, GetChannel(Program.Config.ChainEventsChannelId));
}
public async Task GiveRewards(GiveRewardsCommand rewards)
{
Program.Log.Log($"Processing rewards command: '{JsonConvert.SerializeObject(rewards)}'");
log.Log($"Processing rewards command: '{JsonConvert.SerializeObject(rewards)}'");
if (rewards.Rewards.Any())
{
await ProcessRewards(rewards);
}
await ProcessChainEvents(rewards.EventsOverview);
await eventsSender.ProcessChainEvents(rewards.EventsOverview);
}
private async Task ProcessRewards(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);
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));
await context.ProcessGiveRewardsCommand(LookUpUsers(rewards));
}
catch (Exception ex)
{
log.Error("Failed to process rewards: " + ex);
}
}
private SocketTextChannel? GetChannel(ulong id)
@@ -51,25 +60,9 @@ namespace BiblioTech.Rewards
return GetGuild().TextChannels.SingleOrDefault(c => c.Id == id);
}
private async Task ProcessChainEvents(string[] eventsOverview)
{
if (eventsChannel == null || eventsOverview == null || !eventsOverview.Any()) return;
await Task.Run(async () =>
{
foreach (var e in eventsOverview)
{
if (!string.IsNullOrEmpty(e))
{
await eventsChannel.SendMessageAsync(e);
await Task.Delay(3000);
}
}
});
}
private async Task<Dictionary<ulong, IGuildUser>> LoadAllUsers(SocketGuild guild)
{
Program.Log.Log("Loading all users:");
log.Log("Loading all users..");
var result = new Dictionary<ulong, IGuildUser>();
var users = guild.GetUsersAsync();
await foreach (var ulist in users)
@@ -77,8 +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());
Program.Log.Log($" > {u.Id}({u.DisplayName}) has [{roleIds}]");
//var roleIds = string.Join(",", u.RoleIds.Select(r => r.ToString()).ToArray());
//log.Log($" > {u.Id}({u.DisplayName}) has [{roleIds}]");
}
}
return result;
@@ -94,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
{
@@ -134,13 +127,13 @@ namespace BiblioTech.Rewards
try
{
var userData = Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(address));
if (userData != null) Program.Log.Log($"User '{userData.Name}' was looked up.");
else Program.Log.Log($"Lookup for user was unsuccessful. 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;
}
}
+66
View File
@@ -0,0 +1,66 @@
using CodexContractsPlugin;
using GethPlugin;
namespace BiblioTech
{
public class UserData
{
public UserData(ulong discordId, string name, DateTime createdUtc, EthAddress? currentAddress, List<UserAssociateAddressEvent> associateEvents, List<UserMintEvent> 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<UserAssociateAddressEvent> AssociateEvents { get; }
public List<UserMintEvent> 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<Ether>? ethReceived, Transaction<TestToken>? testTokensMinted)
{
Utc = utc;
UsedAddress = usedAddress;
EthReceived = ethReceived;
TestTokensMinted = testTokensMinted;
}
public DateTime Utc { get; }
public EthAddress UsedAddress { get; }
public Transaction<Ether>? EthReceived { get; }
public Transaction<TestToken>? TestTokensMinted { get; }
}
}
+44 -58
View File
@@ -8,6 +8,7 @@ namespace BiblioTech
public class UserRepo
{
private readonly object repoLock = new object();
private readonly Dictionary<ulong, UserData> cache = new Dictionary<ulong, UserData>();
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<Ether>? eth, Transaction<TestToken>? 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<UserData>(File.ReadAllText(filename))!;
var userData = JsonConvert.DeserializeObject<UserData>(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<UserAssociateAddressEvent> associateEvents, List<UserMintEvent> 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<UserAssociateAddressEvent> AssociateEvents { get; }
public List<UserMintEvent> 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<UserData>(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<Ether>? ethReceived, Transaction<TestToken>? testTokensMinted)
{
Utc = utc;
UsedAddress = usedAddress;
EthReceived = ethReceived;
TestTokensMinted = testTokensMinted;
}
public DateTime Utc { get; }
public EthAddress UsedAddress { get; }
public Transaction<Ether>? EthReceived { get; }
public Transaction<TestToken>? TestTokensMinted { get; }
}
}
@@ -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!"
};
}
}
}
+18
View File
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.15" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ProjectPlugins\GethPlugin\GethPlugin.csproj" />
</ItemGroup>
</Project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ActiveDebugProfile>https</ActiveDebugProfile>
</PropertyGroup>
</Project>
+9
View File
@@ -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;
}
}
+31
View File
@@ -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();
@@ -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"
}
}
}
}
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
+26
View File
@@ -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
+46
View File
@@ -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);
}
}
}
-184
View File
@@ -1,184 +0,0 @@
using CodexContractsPlugin;
using CodexContractsPlugin.Marketplace;
using NethereumWorkflow.BlockUtils;
using Newtonsoft.Json;
using Utils;
namespace TestNetRewarder
{
public class ChainState
{
private HistoricState historicState;
private readonly string[] colorIcons = new[]
{
"🔴",
"🟠",
"🟡",
"🟢",
"🔵",
"🟣",
"🟤",
"⚫",
"⚪",
"🟥",
"🟧",
"🟨",
"🟩",
"🟦",
"🟪",
"🟫",
"⬛",
"⬜",
"🔶",
"🔷"
};
public ChainState(HistoricState historicState, ICodexContracts contracts, BlockInterval blockRange)
{
this.historicState = historicState;
NewRequests = contracts.GetStorageRequests(blockRange);
historicState.CleanUpOldRequests();
historicState.ProcessNewRequests(NewRequests);
historicState.UpdateStorageRequests(contracts);
StartedRequests = historicState.StorageRequests.Where(r => r.RecentlyStarted).ToArray();
FinishedRequests = historicState.StorageRequests.Where(r => r.RecentlyFinished).ToArray();
RequestFulfilledEvents = contracts.GetRequestFulfilledEvents(blockRange);
RequestCancelledEvents = contracts.GetRequestCancelledEvents(blockRange);
SlotFilledEvents = contracts.GetSlotFilledEvents(blockRange);
SlotFreedEvents = contracts.GetSlotFreedEvents(blockRange);
}
public ChainState(
Request[] newRequests,
RequestFulfilledEventDTO[] requestFulfilledEvents,
RequestCancelledEventDTO[] requestCancelledEvents,
SlotFilledEventDTO[] slotFilledEvents,
SlotFreedEventDTO[] slotFreedEvents)
{
NewRequests = newRequests;
RequestFulfilledEvents = requestFulfilledEvents;
RequestCancelledEvents = requestCancelledEvents;
SlotFilledEvents = slotFilledEvents;
SlotFreedEvents = slotFreedEvents;
historicState = new HistoricState();
StartedRequests = Array.Empty<StorageRequest>();
FinishedRequests = Array.Empty<StorageRequest>();
}
public Request[] NewRequests { get; }
[JsonIgnore]
public StorageRequest[] AllRequests => historicState.StorageRequests;
[JsonIgnore]
public StorageRequest[] StartedRequests { get; private set; }
[JsonIgnore]
public StorageRequest[] FinishedRequests { get; private set; }
public RequestFulfilledEventDTO[] RequestFulfilledEvents { get; }
public RequestCancelledEventDTO[] RequestCancelledEvents { get; }
public SlotFilledEventDTO[] SlotFilledEvents { get; }
public SlotFreedEventDTO[] SlotFreedEvents { get; }
public string EntireString()
{
return
$"ChainState=[{JsonConvert.SerializeObject(this)}]" +
$"HistoricState=[{historicState.EntireString()}]";
}
public void Set(HistoricState h)
{
historicState = h;
}
public string[] GenerateOverview()
{
var entries = new List<StringBlockNumberPair>();
entries.AddRange(NewRequests.Select(ToPair));
entries.AddRange(RequestFulfilledEvents.Select(ToPair));
entries.AddRange(RequestCancelledEvents.Select(ToPair));
entries.AddRange(SlotFilledEvents.Select(ToPair));
entries.AddRange(SlotFreedEvents.Select(ToPair));
entries.AddRange(FinishedRequests.Select(ToPair));
entries.Sort(new StringUtcComparer());
return entries.Select(ToLine).ToArray();
}
private StringBlockNumberPair ToPair(Request r)
{
return new StringBlockNumberPair("NewRequest", JsonConvert.SerializeObject(r), r.Block, r.RequestId);
}
public StringBlockNumberPair ToPair(StorageRequest r)
{
return new StringBlockNumberPair("FinishedRequest", JsonConvert.SerializeObject(r), r.Request.Block, r.Request.RequestId);
}
private StringBlockNumberPair ToPair(RequestFulfilledEventDTO r)
{
return new StringBlockNumberPair("Fulfilled", JsonConvert.SerializeObject(r), r.Block, r.RequestId);
}
private StringBlockNumberPair ToPair(RequestCancelledEventDTO r)
{
return new StringBlockNumberPair("Cancelled", JsonConvert.SerializeObject(r), r.Block, r.RequestId);
}
private StringBlockNumberPair ToPair(SlotFilledEventDTO r)
{
return new StringBlockNumberPair("SlotFilled", JsonConvert.SerializeObject(r), r.Block, r.RequestId);
}
private StringBlockNumberPair ToPair(SlotFreedEventDTO r)
{
return new StringBlockNumberPair("SlotFreed", JsonConvert.SerializeObject(r), r.Block, r.RequestId);
}
private string ToLine(StringBlockNumberPair pair)
{
var nl = Environment.NewLine;
var colorIcon = GetColorIcon(pair.RequestId);
return $"{colorIcon} {pair.Block} ({pair.Name}){nl}" +
$"```json{nl}" +
$"{pair.Str}{nl}" +
$"```";
}
private string GetColorIcon(byte[] requestId)
{
var index = requestId[0] % colorIcons.Length;
return colorIcons[index];
}
public class StringBlockNumberPair
{
public StringBlockNumberPair(string name, string str, BlockTimeEntry block, byte[] requestId)
{
Name = name;
Str = str;
Block = block;
RequestId = requestId;
}
public string Name { get; }
public string Str { get; }
public BlockTimeEntry Block { get; }
public byte[] RequestId { get; }
}
public class StringUtcComparer : IComparer<StringBlockNumberPair>
{
public int Compare(StringBlockNumberPair? x, StringBlockNumberPair? y)
{
if (x == null && y == null) return 0;
if (x == null) return 1;
if (y == null) return -1;
return x.Block.BlockNumber.CompareTo(y.Block.BlockNumber);
}
}
}
}
-141
View File
@@ -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;
}
}
}
+9
View File
@@ -40,5 +40,14 @@ namespace TestNetRewarder
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;
}
}
}
}
+123
View File
@@ -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<string> events = new List<string>();
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();
}
}
}
+74
View File
@@ -0,0 +1,74 @@
using CodexContractsPlugin.ChainMonitor;
using CodexContractsPlugin.Marketplace;
using DiscordRewards;
using System.Numerics;
namespace TestNetRewarder
{
public class MarketBuffer
{
private readonly List<RequestEvent> requestEvents = new List<RequestEvent>();
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<IChainStateRequest, BigInteger> getValue)
{
return Average(s =>
{
var value = getValue(s);
return (int)value;
});
}
private float Average(Func<IChainStateRequest, int> 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;
}
}
}
+44 -94
View File
@@ -1,124 +1,74 @@
using CodexContractsPlugin.Marketplace;
using CodexContractsPlugin.ChainMonitor;
using DiscordRewards;
using GethPlugin;
using Logging;
using System.Numerics;
namespace TestNetRewarder
{
public class MarketTracker
public class MarketTracker : IChainStateChangeHandler
{
private readonly List<ChainState> buffer = new List<ChainState>();
private readonly List<MarketBuffer> buffers = new List<MarketBuffer>();
private readonly ILog log;
public MarketAverage[] ProcessChainState(ChainState chainState)
public MarketTracker(Configuration config, ILog log)
{
var intervalCounts = GetInsightCounts();
if (!intervalCounts.Any()) return Array.Empty<MarketAverage>();
var intervals = GetInsightCounts(config);
UpdateBuffer(chainState, intervalCounts.Max());
var result = intervalCounts
.Select(GenerateMarketAverage)
.Where(a => a != null)
.Cast<MarketAverage>()
.ToArray();
if (!result.Any()) result = Array.Empty<MarketAverage>();
return result;
}
private void UpdateBuffer(ChainState chainState, int maxNumberOfIntervals)
{
buffer.Add(chainState);
while (buffer.Count > maxNumberOfIntervals)
foreach (var i in intervals)
{
buffer.RemoveAt(0);
buffers.Add(new MarketBuffer(
config.Interval * i
));
}
this.log = log;
}
private MarketAverage? GenerateMarketAverage(int numberOfIntervals)
public MarketAverage[] GetAverages()
{
var states = SelectStates(numberOfIntervals);
return CreateAverage(states);
foreach (var b in buffers) b.Update();
return buffers.Select(b => b.GetAverage()).Where(a => a != null).Cast<MarketAverage>().ToArray();
}
private ChainState[] SelectStates(int numberOfIntervals)
public void OnNewRequest(RequestEvent requestEvent)
{
if (numberOfIntervals < 1) return Array.Empty<ChainState>();
if (numberOfIntervals > buffer.Count) return Array.Empty<ChainState>();
return buffer.TakeLast(numberOfIntervals).ToArray();
}
private MarketAverage? CreateAverage(ChainState[] states)
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
{
return new MarketAverage
{
NumberOfFinished = CountNumberOfFinishedRequests(states),
TimeRangeSeconds = GetTotalTimeRange(states),
Price = Average(states, s => s.Request.Ask.Reward),
Duration = Average(states, s => s.Request.Ask.Duration),
Size = Average(states, s => GetTotalSize(s.Request.Ask)),
Collateral = Average(states, s => s.Request.Ask.Collateral),
ProofProbability = Average(states, s => s.Request.Ask.ProofProbability)
};
}
catch (Exception ex)
{
Program.Log.Error($"Exception in CreateAverage: {ex}");
return null;
}
}
private int GetTotalSize(Ask ask)
{
var nSlots = Convert.ToInt32(ask.Slots);
var slotSize = Convert.ToInt32(ask.SlotSize);
return nSlots * slotSize;
}
private float Average(ChainState[] states, Func<StorageRequest, BigInteger> getValue)
{
return Average(states, s => Convert.ToInt32(getValue(s)));
}
private float Average(ChainState[] states, Func<StorageRequest, int> getValue)
{
var sum = 0.0f;
var count = 0.0f;
foreach (var state in states)
{
foreach (var finishedRequest in state.FinishedRequests)
{
sum += getValue(finishedRequest);
count++;
}
}
if (count < 1.0f) return 0.0f;
return sum / count;
}
private int GetTotalTimeRange(ChainState[] states)
{
return Convert.ToInt32((Program.Config.Interval * states.Length).TotalSeconds);
}
private int CountNumberOfFinishedRequests(ChainState[] states)
{
return states.Sum(s => s.FinishedRequests.Length);
}
private int[] GetInsightCounts()
{
try
{
var tokens = Program.Config.MarketInsights.Split(';').ToArray();
var tokens = config.MarketInsights.Split(';').ToArray();
return tokens.Select(t => Convert.ToInt32(t)).ToArray();
}
catch (Exception ex)
{
Program.Log.Error($"Exception when parsing MarketInsights config parameters: {ex}");
log.Error($"Exception when parsing MarketInsights config parameters: {ex}");
}
return Array.Empty<int>();
return Array.Empty<int>();
}
}
}
+37 -124
View File
@@ -1,148 +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 static readonly MarketTracker marketTracker = new MarketTracker();
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)
{
log.Log(chainState.EntireString());
var outgoingRewards = new List<RewardUsersCommand>();
foreach (var reward in rewardRepo.Rewards)
{
ProcessReward(outgoingRewards, reward, chainState);
}
var marketAverages = GetMarketAverages(chainState);
var eventsOverview = GenerateEventsOverview(chainState);
log.Log($"Found {outgoingRewards.Count} rewards. " +
$"Found {marketAverages.Length} market averages. " +
$"Found {eventsOverview.Length} events.");
if (outgoingRewards.Any() || marketAverages.Any() || eventsOverview.Any())
{
if (!await SendRewardsCommand(outgoingRewards, marketAverages, eventsOverview))
{
log.Error("Failed to send reward command.");
}
}
}
private string[] GenerateEventsOverview(ChainState chainState)
{
return chainState.GenerateOverview();
}
private MarketAverage[] GetMarketAverages(ChainState chainState)
{
return marketTracker.ProcessChainState(chainState);
}
private async Task<bool> SendRewardsCommand(List<RewardUsersCommand> outgoingRewards, MarketAverage[] marketAverages, string[] eventsOverview)
{
var cmd = new GiveRewardsCommand
{
Rewards = outgoingRewards.ToArray(),
Averages = marketAverages.ToArray(),
EventsOverview = eventsOverview
};
log.Debug("Sending rewards: " + JsonConvert.SerializeObject(cmd));
return await Program.BotClient.SendRewards(cmd);
}
private void ProcessReward(List<RewardUsersCommand> 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).Distinct().ToArray();
}
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);
}
}
}
+10 -7
View File
@@ -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);
}
}
+40
View File
@@ -0,0 +1,40 @@
using DiscordRewards;
using GethPlugin;
namespace TestNetRewarder
{
public class RequestBuilder : IRewardGiver
{
private readonly Dictionary<ulong, List<EthAddress>> rewards = new Dictionary<ulong, List<EthAddress>>();
public void Give(RewardConfig reward, EthAddress receiver)
{
if (rewards.ContainsKey(reward.RoleId))
{
rewards[reward.RoleId].Add(receiver);
}
else
{
rewards.Add(reward.RoleId, new List<EthAddress> { 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;
}
}
}
+97
View File
@@ -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;
}
}
}
+17
View File
@@ -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; }
}
}

Some files were not shown because too many files have changed in this diff Show More