Merge branch 'master' into feature/multi-codex-folder-saver

This commit is contained in:
ThatBen 2025-05-02 08:51:42 +02:00
commit c26fb65967
No known key found for this signature in database
GPG Key ID: E020A7DDCD52E1AB
100 changed files with 2101 additions and 1900 deletions

View File

@ -1,6 +1,6 @@
namespace Core
{
internal class PluginManager
internal class PluginManager : IPluginAccess
{
private readonly List<PluginToolsPair> pairs = new List<PluginToolsPair>();
@ -14,6 +14,7 @@
ApplyLogPrefix(plugin, tools);
}
AwakePlugins();
}
internal void AnnouncePlugins()
@ -43,7 +44,7 @@
}
}
internal T GetPlugin<T>() where T : IProjectPlugin
public T GetPlugin<T>() where T : IProjectPlugin
{
return (T)pairs.Single(p => p.Plugin.GetType() == typeof(T)).Plugin;
}
@ -55,6 +56,14 @@
return plugin;
}
private void AwakePlugins()
{
foreach (var p in pairs)
{
p.Plugin.Awake(this);
}
}
private void ApplyLogPrefix(IProjectPlugin plugin, PluginTools tools)
{
if (plugin is IHasLogPrefix hasLogPrefix)

View File

@ -4,6 +4,7 @@ namespace Core
{
public interface IProjectPlugin
{
void Awake(IPluginAccess access);
void Announce();
void Decommission();
}
@ -18,6 +19,11 @@ namespace Core
void AddMetadata(IAddMetadata metadata);
}
public interface IPluginAccess
{
T GetPlugin<T>() where T : IProjectPlugin;
}
public static class ProjectPlugin
{
/// <summary>

View File

@ -1,21 +0,0 @@
using Utils;
namespace DiscordRewards
{
public class CheckConfig
{
public CheckType Type { get; set; }
public ulong MinNumberOfHosts { get; set; }
public ByteSize MinSlotSize { get; set; } = 0.Bytes();
public TimeSpan MinDuration { get; set; } = TimeSpan.Zero;
}
public enum CheckType
{
Uninitialized,
HostFilledSlot,
HostFinishedSlot,
ClientPostedContract,
ClientStartedContract,
}
}

View File

@ -0,0 +1,34 @@
namespace DiscordRewards
{
public class EventsAndErrors
{
public ChainEventMessage[] EventsOverview { get; set; } = Array.Empty<ChainEventMessage>();
public string[] Errors { get; set; } = Array.Empty<string>();
public ActiveChainAddresses ActiveChainAddresses { get; set; } = new ActiveChainAddresses();
public bool HasAny()
{
return
Errors.Length > 0 ||
EventsOverview.Length > 0 ||
ActiveChainAddresses.HasAny();
}
}
public class ChainEventMessage
{
public ulong BlockNumber { get; set; }
public string Message { get; set; } = string.Empty;
}
public class ActiveChainAddresses
{
public string[] Hosts { get; set; } = Array.Empty<string>();
public string[] Clients { get; set; } = Array.Empty<string>();
public bool HasAny()
{
return Hosts.Length > 0 || Clients.Length > 0;
}
}
}

View File

@ -1,26 +0,0 @@
namespace DiscordRewards
{
public class GiveRewardsCommand
{
public RewardUsersCommand[] Rewards { get; set; } = Array.Empty<RewardUsersCommand>();
public ChainEventMessage[] EventsOverview { get; set; } = Array.Empty<ChainEventMessage>();
public string[] Errors { get; set; } = Array.Empty<string>();
public bool HasAny()
{
return Rewards.Any() || EventsOverview.Any();
}
}
public class RewardUsersCommand
{
public ulong RewardId { get; set; }
public string[] UserAddresses { get; set; } = Array.Empty<string>();
}
public class ChainEventMessage
{
public ulong BlockNumber { get; set; }
public string Message { get; set; } = string.Empty;
}
}

View File

@ -1,18 +0,0 @@
namespace DiscordRewards
{
public class RewardConfig
{
public const string UsernameTag = "<USER>";
public RewardConfig(ulong roleId, string message, CheckConfig checkConfig)
{
RoleId = roleId;
Message = message;
CheckConfig = checkConfig;
}
public ulong RoleId { get; }
public string Message { get; }
public CheckConfig CheckConfig { get; }
}
}

View File

@ -1,53 +0,0 @@
namespace DiscordRewards
{
public class RewardRepo
{
private static string Tag => RewardConfig.UsernameTag;
public RewardConfig[] Rewards { get; } = new RewardConfig[0];
// Example configuration, from test server:
//{
// // Filled any slot
// new RewardConfig(1187039439558541498, $"{Tag} successfully filled their first slot!", new CheckConfig
// {
// Type = CheckType.HostFilledSlot
// }),
// // Finished any slot
// new RewardConfig(1202286165630390339, $"{Tag} successfully finished their first slot!", new CheckConfig
// {
// Type = CheckType.HostFinishedSlot
// }),
// // 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),
// }),
// // Posted any contract
// new RewardConfig(1202286258370383913, $"{Tag} posted their first contract!", new CheckConfig
// {
// Type = CheckType.ClientPostedContract
// }),
// // 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),
// })
//};
}
}

View File

@ -7,8 +7,10 @@ namespace Logging
void Log(string message);
void Debug(string message = "", int skipFrames = 0);
void Error(string message);
void Raw(string message);
void AddStringReplace(string from, string to);
LogFile CreateSubfile(string addName, string ext = "log");
string GetFullName();
}
public abstract class BaseLog : ILog
@ -28,7 +30,8 @@ namespace Logging
}
protected bool IsDebug { get; private set; }
protected abstract string GetFullName();
public abstract string GetFullName();
public LogFile LogFile
{
@ -60,6 +63,11 @@ namespace Logging
Log(msg);
}
public void Raw(string message)
{
LogFile.WriteRaw(message);
}
public virtual void AddStringReplace(string from, string to)
{
if (string.IsNullOrWhiteSpace(from)) return;

View File

@ -2,7 +2,7 @@
{
public class ConsoleLog : BaseLog
{
protected override string GetFullName()
public override string GetFullName()
{
return "CONSOLE";
}

View File

@ -9,7 +9,7 @@
public string FullFilename { get; }
protected override string GetFullName()
public override string GetFullName()
{
return FullFilename;
}

View File

@ -17,7 +17,6 @@
public string Prefix { get; set; } = string.Empty;
public LogFile CreateSubfile(string addName, string ext = "log")
{
return backingLog.CreateSubfile(addName, ext);
@ -42,5 +41,15 @@
{
backingLog.AddStringReplace(from, to);
}
public void Raw(string message)
{
backingLog.Raw(message);
}
public string GetFullName()
{
return backingLog.GetFullName();
}
}
}

View File

@ -29,11 +29,21 @@
OnAll(l => l.Error(message));
}
public string GetFullName()
{
return targetLogs.First().GetFullName();
}
public void Log(string message)
{
OnAll(l => l.Log(message));
}
public void Raw(string message)
{
OnAll(l => l.Raw(message));
}
private void OnAll(Action<ILog> action)
{
foreach (var t in targetLogs) action(t);

View File

@ -2,11 +2,9 @@
{
public class NullLog : BaseLog
{
public string FullFilename { get; set; } = "NULL";
protected override string GetFullName()
public override string GetFullName()
{
return FullFilename;
return "NULL";
}
public override void Log(string message)

View File

@ -34,10 +34,28 @@
var source = items.ToList();
while (source.Any())
{
result.Add(RandomUtils.PickOneRandom(source));
result.Add(PickOneRandom(source));
}
return result.ToArray();
}
}
public static string GenerateRandomString(long requiredLength)
{
lock (@lock)
{
var result = "";
while (result.Length < requiredLength)
{
var remaining = requiredLength - result.Length;
var len = Math.Min(1024, remaining);
var bytes = new byte[len];
random.NextBytes(bytes);
result += string.Join("", bytes.Select(b => b.ToString()));
}
return result.Substring(0, Convert.ToInt32(requiredLength));
}
}
}
}

View File

@ -4,8 +4,11 @@
{
public static string Between(string input, string open, string close)
{
var openIndex = input.IndexOf(open) + open.Length;
var openI = input.IndexOf(open);
if (openI == -1) return input;
var openIndex = openI + open.Length;
var closeIndex = input.LastIndexOf(close);
if (closeIndex == -1) return input;
return input.Substring(openIndex, closeIndex - openIndex);
}

View File

@ -20,11 +20,6 @@ namespace WebUtils
private readonly Action<HttpClient> onClientCreated;
private readonly string id;
internal Http(string id, ILog log, IWebCallTimeSet timeSet)
: this(id, log, timeSet, DoNothing)
{
}
internal Http(string id, ILog log, IWebCallTimeSet timeSet, Action<HttpClient> onClientCreated)
{
this.id = id;
@ -89,9 +84,5 @@ namespace WebUtils
onClientCreated(client);
return client;
}
private static void DoNothing(HttpClient client)
{
}
}
}

View File

@ -13,16 +13,28 @@ namespace WebUtils
{
private readonly ILog log;
private readonly IWebCallTimeSet defaultTimeSet;
private readonly Action<HttpClient> factoryOnClientCreated;
public HttpFactory(ILog log)
: this (log, new DefaultWebCallTimeSet())
{
}
public HttpFactory(ILog log, Action<HttpClient> onClientCreated)
: this(log, new DefaultWebCallTimeSet(), onClientCreated)
{
}
public HttpFactory(ILog log, IWebCallTimeSet defaultTimeSet)
: this(log, defaultTimeSet, DoNothing)
{
}
public HttpFactory(ILog log, IWebCallTimeSet defaultTimeSet, Action<HttpClient> onClientCreated)
{
this.log = log;
this.defaultTimeSet = defaultTimeSet;
this.factoryOnClientCreated = onClientCreated;
}
public IHttp CreateHttp(string id, Action<HttpClient> onClientCreated)
@ -32,12 +44,20 @@ namespace WebUtils
public IHttp CreateHttp(string id, Action<HttpClient> onClientCreated, IWebCallTimeSet ts)
{
return new Http(id, log, ts, onClientCreated);
return new Http(id, log, ts, (c) =>
{
factoryOnClientCreated(c);
onClientCreated(c);
});
}
public IHttp CreateHttp(string id)
{
return new Http(id, log, defaultTimeSet);
return new Http(id, log, defaultTimeSet, factoryOnClientCreated);
}
private static void DoNothing(HttpClient client)
{
}
}
}

View File

@ -17,6 +17,7 @@ namespace CodexClient
{
public string Version { get; set; } = string.Empty;
public string Revision { get; set; } = string.Empty;
public string Contracts { get; set; } = string.Empty;
public bool IsValid()
{

View File

@ -168,7 +168,8 @@ namespace CodexClient
return new DebugInfoVersion
{
Version = obj.Version,
Revision = obj.Revision
Revision = obj.Revision,
Contracts = obj.Contracts
};
}

View File

@ -124,6 +124,9 @@ components:
revision:
type: string
example: 0c647d8
contracts:
type: string
example: 0b537c7
PeersTable:
type: object

View File

@ -1,4 +1,5 @@
using GethPlugin;
using CodexClient;
using GethPlugin;
using KubernetesWorkflow;
using KubernetesWorkflow.Recipe;
@ -6,13 +7,17 @@ namespace CodexContractsPlugin
{
public class CodexContractsContainerRecipe : ContainerRecipeFactory
{
public static string DockerImage { get; } = "codexstorage/codex-contracts-eth:latest-dist-tests";
public const string MarketplaceAddressFilename = "/hardhat/deployments/codexdisttestnetwork/Marketplace.json";
public const string MarketplaceArtifactFilename = "/hardhat/artifacts/contracts/Marketplace.sol/Marketplace.json";
private readonly DebugInfoVersion versionInfo;
public override string AppName => "codex-contracts";
public override string Image => DockerImage;
public override string Image => GetContractsDockerImage();
public CodexContractsContainerRecipe(DebugInfoVersion versionInfo)
{
this.versionInfo = versionInfo;
}
protected override void Initialize(StartupConfig startupConfig)
{
@ -26,5 +31,10 @@ namespace CodexContractsPlugin
AddEnvVar("HARDHAT_NETWORK", "codexdisttestnetwork");
AddEnvVar("KEEP_ALIVE", "1");
}
private string GetContractsDockerImage()
{
return $"codexstorage/codex-contracts-eth:sha-{versionInfo.Contracts}-dist-tests";
}
}
}

View File

@ -16,6 +16,10 @@ namespace CodexContractsPlugin
public string LogPrefix => "(CodexContracts) ";
public void Awake(IPluginAccess access)
{
}
public void Announce()
{
tools.GetLog().Log($"Loaded Codex-Marketplace SmartContracts");
@ -23,16 +27,16 @@ namespace CodexContractsPlugin
public void AddMetadata(IAddMetadata metadata)
{
metadata.Add("codexcontractsid", CodexContractsContainerRecipe.DockerImage);
metadata.Add("codexcontractsid", "dynamic");
}
public void Decommission()
{
}
public CodexContractsDeployment DeployContracts(CoreInterface ci, IGethNode gethNode)
public CodexContractsDeployment DeployContracts(CoreInterface ci, IGethNode gethNode, CodexClient.DebugInfoVersion versionInfo)
{
return starter.Deploy(ci, gethNode);
return starter.Deploy(ci, gethNode, versionInfo);
}
public ICodexContracts WrapDeploy(IGethNode gethNode, CodexContractsDeployment deployment)

View File

@ -13,6 +13,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Framework\Core\Core.csproj" />
<ProjectReference Include="..\CodexClient\CodexClient.csproj" />
<ProjectReference Include="..\GethPlugin\GethPlugin.csproj" />
</ItemGroup>

View File

@ -1,4 +1,5 @@
using CodexContractsPlugin.Marketplace;
using CodexClient;
using CodexContractsPlugin.Marketplace;
using Core;
using GethPlugin;
using KubernetesWorkflow;
@ -18,7 +19,7 @@ namespace CodexContractsPlugin
this.tools = tools;
}
public CodexContractsDeployment Deploy(CoreInterface ci, IGethNode gethNode)
public CodexContractsDeployment Deploy(CoreInterface ci, IGethNode gethNode, DebugInfoVersion versionInfo)
{
Log("Starting Codex SmartContracts container...");
@ -26,7 +27,10 @@ namespace CodexContractsPlugin
var startupConfig = CreateStartupConfig(gethNode);
startupConfig.NameOverride = "codex-contracts";
var containers = workflow.Start(1, new CodexContractsContainerRecipe(), startupConfig).WaitForOnline();
var recipe = new CodexContractsContainerRecipe(versionInfo);
Log($"Using image: {recipe.Image}");
var containers = workflow.Start(1, recipe, startupConfig).WaitForOnline();
if (containers.Containers.Length != 1) throw new InvalidOperationException("Expected 1 Codex contracts container to be created. Test infra failure.");
var container = containers.Containers[0];

View File

@ -1,13 +1,14 @@
using Core;
using CodexClient;
using Core;
using GethPlugin;
namespace CodexContractsPlugin
{
public static class CoreInterfaceExtensions
{
public static CodexContractsDeployment DeployCodexContracts(this CoreInterface ci, IGethNode gethNode)
public static CodexContractsDeployment DeployCodexContracts(this CoreInterface ci, IGethNode gethNode, DebugInfoVersion versionInfo)
{
return Plugin(ci).DeployContracts(ci, gethNode);
return Plugin(ci).DeployContracts(ci, gethNode, versionInfo);
}
public static ICodexContracts WrapCodexContractsDeployment(this CoreInterface ci, IGethNode gethNode, CodexContractsDeployment deployment)
@ -15,9 +16,9 @@ namespace CodexContractsPlugin
return Plugin(ci).WrapDeploy(gethNode, deployment);
}
public static ICodexContracts StartCodexContracts(this CoreInterface ci, IGethNode gethNode)
public static ICodexContracts StartCodexContracts(this CoreInterface ci, IGethNode gethNode, DebugInfoVersion versionInfo)
{
var deployment = DeployCodexContracts(ci, gethNode);
var deployment = DeployCodexContracts(ci, gethNode, versionInfo);
return WrapCodexContractsDeployment(ci, gethNode, deployment);
}

View File

@ -17,6 +17,10 @@ namespace CodexDiscordBotPlugin
public string LogPrefix => "(DiscordBot) ";
public void Awake(IPluginAccess access)
{
}
public void Announce()
{
tools.GetLog().Log($"Codex DiscordBot (BiblioTech) loaded.");

View File

@ -10,7 +10,7 @@ namespace CodexPlugin
public class ApiChecker
{
// <INSERT-OPENAPI-YAML-HASH>
private const string OpenApiYamlHash = "1A-F7-DF-C3-E1-C6-98-FF-32-20-21-9B-26-40-B0-51-08-35-C2-E7-DB-41-49-93-60-A9-CE-47-B5-AD-3D-A3";
private const string OpenApiYamlHash = "06-B9-41-E8-C8-6C-DE-01-86-83-F3-9A-E4-AC-E7-30-D9-E6-64-60-E0-21-81-9E-4E-C5-93-77-2C-71-79-14";
private const string OpenApiFilePath = "/codex/openapi.yaml";
private const string DisableEnvironmentVariable = "CODEXPLUGIN_DISABLE_APICHECK";

View File

@ -7,7 +7,6 @@ namespace CodexPlugin
{
public class CodexContainerRecipe : ContainerRecipeFactory
{
private const string DefaultDockerImage = "codexstorage/nim-codex:latest-dist-tests";
public const string ApiPortTag = "codex_api_port";
public const string ListenPortTag = "codex_listen_port";
public const string MetricsPortTag = "codex_metrics_port";
@ -16,11 +15,15 @@ namespace CodexPlugin
// Used by tests for time-constraint assertions.
public static readonly TimeSpan MaxUploadTimePerMegabyte = TimeSpan.FromSeconds(2.0);
public static readonly TimeSpan MaxDownloadTimePerMegabyte = TimeSpan.FromSeconds(2.0);
private readonly CodexDockerImage codexDockerImage;
public override string AppName => "codex";
public override string Image => GetDockerImage();
public override string Image => codexDockerImage.GetCodexDockerImage();
public static string DockerImageOverride { get; set; } = string.Empty;
public CodexContainerRecipe(CodexDockerImage codexDockerImage)
{
this.codexDockerImage = codexDockerImage;
}
protected override void Initialize(StartupConfig startupConfig)
{
@ -163,13 +166,5 @@ namespace CodexPlugin
// Default Codex quota: 8 Gb, using +20% to be safe.
return 8.GB().Multiply(1.2);
}
private string GetDockerImage()
{
var image = Environment.GetEnvironmentVariable("CODEXDOCKERIMAGE");
if (!string.IsNullOrEmpty(image)) return image;
if (!string.IsNullOrEmpty(DockerImageOverride)) return DockerImageOverride;
return DefaultDockerImage;
}
}
}

View File

@ -0,0 +1,17 @@
namespace CodexPlugin
{
public class CodexDockerImage
{
private const string DefaultDockerImage = "codexstorage/nim-codex:latest-dist-tests";
public static string Override { get; set; } = string.Empty;
public string GetCodexDockerImage()
{
var image = Environment.GetEnvironmentVariable("CODEXDOCKERIMAGE");
if (!string.IsNullOrEmpty(image)) return image;
if (!string.IsNullOrEmpty(Override)) return Override;
return DefaultDockerImage;
}
}
}

View File

@ -13,12 +13,15 @@ namespace CodexPlugin
private readonly CodexLogLevel defaultLogLevel = CodexLogLevel.Trace;
private readonly CodexHooksFactory hooksFactory = new CodexHooksFactory();
private readonly ProcessControlMap processControlMap = new ProcessControlMap();
private readonly CodexDockerImage codexDockerImage = new CodexDockerImage();
private readonly CodexContainerRecipe recipe;
private readonly CodexWrapper codexWrapper;
public CodexPlugin(IPluginTools tools)
{
this.tools = tools;
recipe = new CodexContainerRecipe(codexDockerImage);
codexStarter = CreateCodexStarter();
codexWrapper = new CodexWrapper(tools, processControlMap, hooksFactory);
}
@ -28,7 +31,7 @@ namespace CodexPlugin
if (UseContainers)
{
Log("Using Containerized Codex instances");
return new ContainerCodexStarter(tools, processControlMap);
return new ContainerCodexStarter(tools, recipe, processControlMap);
}
Log("Using Binary Codex instances");
@ -37,8 +40,14 @@ namespace CodexPlugin
public string LogPrefix => "(Codex) ";
public void Awake(IPluginAccess access)
{
}
public void Announce()
{
// give codex docker image to contracts plugin.
Log($"Loaded with Codex ID: '{codexWrapper.GetCodexId()}' - Revision: {codexWrapper.GetCodexRevision()}");
}

View File

@ -10,12 +10,13 @@ namespace CodexPlugin
{
private readonly IPluginTools pluginTools;
private readonly ProcessControlMap processControlMap;
private readonly CodexContainerRecipe recipe = new CodexContainerRecipe();
private readonly CodexContainerRecipe recipe;
private readonly ApiChecker apiChecker;
public ContainerCodexStarter(IPluginTools pluginTools, ProcessControlMap processControlMap)
public ContainerCodexStarter(IPluginTools pluginTools, CodexContainerRecipe recipe, ProcessControlMap processControlMap)
{
this.pluginTools = pluginTools;
this.recipe = recipe;
this.processControlMap = processControlMap;
apiChecker = new ApiChecker(pluginTools);
}

View File

@ -40,7 +40,7 @@ namespace CodexNetDeployer
Log($"Codex docker image will be built in path '{repoPath}'.");
Log("Please note this can take several minutes. If you're not trying to use a Codex image with local code changes,");
Log("Consider using the default test image or consider setting the 'CODEXDOCKERIMAGE' environment variable to use an already built image.");
CodexContainerRecipe.DockerImageOverride = $"Using docker image locally built in path '{repoPath}'.";
CodexDockerImage.Override = $"Using docker image locally built in path '{repoPath}'.";
}
public void Build()
@ -62,7 +62,7 @@ namespace CodexNetDeployer
Docker("push", customImage);
CodexContainerRecipe.DockerImageOverride = customImage;
CodexDockerImage.Override = customImage;
Log("Image pushed. Good to go!");
}

View File

@ -23,16 +23,27 @@ namespace CodexPlugin.OverwatchSupport
converter = new CodexLogConverter(writer, config, identityMap);
}
public void Finalize(string outputFilepath)
public void FinalizeWriter()
{
log.Log("Finalizing Codex transcript...");
writer.AddHeader(CodexHeaderKey, CreateCodexHeader());
writer.Write(outputFilepath);
writer.Write(GetOutputFullPath());
log.Log("Done");
}
private string GetOutputFullPath()
{
var outputPath = Path.GetDirectoryName(log.GetFullName());
if (outputPath == null) throw new Exception("Logfile path is null");
var filename = Path.GetFileNameWithoutExtension(log.GetFullName());
if (string.IsNullOrEmpty(filename)) throw new Exception("Logfile name is null or empty");
var outputFile = Path.Combine(outputPath, filename + "_" + config.OutputPath);
if (!outputFile.EndsWith(".owts")) outputFile += ".owts";
return outputFile;
}
public ICodexNodeHooks CreateHooks(string nodeName)
{
nodeName = Str.Between(nodeName, "'", "'");

View File

@ -2,11 +2,13 @@
{
public class CodexTranscriptWriterConfig
{
public CodexTranscriptWriterConfig(bool includeBlockReceivedEvents)
public CodexTranscriptWriterConfig(string outputPath, bool includeBlockReceivedEvents)
{
OutputPath = outputPath;
IncludeBlockReceivedEvents = includeBlockReceivedEvents;
}
public string OutputPath { get; }
public bool IncludeBlockReceivedEvents { get; }
}
}

View File

@ -13,6 +13,10 @@ namespace DeployAndRunPlugin
this.tools = tools;
}
public void Awake(IPluginAccess access)
{
}
public void Announce()
{
tools.GetLog().Log("Deploy-and-Run plugin loaded.");

View File

@ -16,6 +16,10 @@ namespace GethPlugin
public string LogPrefix => "(Geth) ";
public void Awake(IPluginAccess access)
{
}
public void Announce()
{
tools.GetLog().Log($"Loaded Geth plugin.");

View File

@ -18,6 +18,10 @@ namespace MetricsPlugin
public string LogPrefix => "(Metrics) ";
public void Awake(IPluginAccess access)
{
}
public void Announce()
{
tools.GetLog().Log($"Prometheus plugin loaded with '{starter.GetPrometheusId()}'.");

View File

@ -27,7 +27,7 @@ namespace ContinuousTests
var startTime = DateTime.UtcNow;
var overviewLog = new LogSplitter(
new FixtureLog(logConfig, startTime, config.CodexDeployment.Id, "Overview"),
FixtureLog.Create(logConfig, startTime, config.CodexDeployment.Id, "Overview"),
new ConsoleLog()
);
var statusLog = new StatusLog(logConfig, startTime, "continuous-tests", config.CodexDeployment.Id,

View File

@ -33,7 +33,7 @@ namespace ContinuousTests
var entryPoint = CreateEntryPoint();
// We have to be sure that the transient node we start is using the same image as whatever's already in the deployed network.
// Therefore, we use the image of the bootstrap node.
CodexContainerRecipe.DockerImageOverride = bootstrapNode.GetImageName();
CodexDockerImage.Override = bootstrapNode.GetImageName();
try
{

View File

@ -37,7 +37,7 @@ namespace ContinuousTests
this.handle = handle;
this.cancelToken = cancelToken;
testName = handle.Test.GetType().Name;
fixtureLog = new FixtureLog(new LogConfig(config.LogPath), DateTime.UtcNow, deployId, testName);
fixtureLog = FixtureLog.Create(new LogConfig(config.LogPath), DateTime.UtcNow, deployId, testName);
entryPoint = entryPointFactory.CreateEntryPoint(config.KubeConfigFile, config.DataPath,
config.CodexDeployment.Metadata.KubeNamespace, fixtureLog);
ApplyLogReplacements(fixtureLog, startupChecker);
@ -81,17 +81,11 @@ namespace ContinuousTests
OverviewLog($" > Test passed. ({Time.FormatDuration(duration)})");
UpdateStatusLogPassed(testStart, duration);
if (!config.KeepPassedTestLogs)
{
fixtureLog.Delete();
}
resultHandler(true);
}
catch (Exception ex)
{
fixtureLog.Error("Test run failed with exception: " + ex);
fixtureLog.MarkAsFailed();
UpdateStatusLogFailed(testStart, duration, ex.ToString());
DownloadContainerLogs(testStart);

View File

@ -22,7 +22,7 @@ namespace ContinuousTests
public void Check()
{
var log = new FixtureLog(new LogConfig(config.LogPath), DateTime.UtcNow, config.CodexDeployment.Id,
var log = FixtureLog.Create(new LogConfig(config.LogPath), DateTime.UtcNow, config.CodexDeployment.Id,
"StartupChecks");
log.Log("Starting continuous test run...");
IncludeDeploymentConfiguration(log);
@ -90,7 +90,7 @@ namespace ContinuousTests
}
}
private void CheckCodexNodes(BaseLog log, Configuration config)
private void CheckCodexNodes(ILog log, Configuration config)
{
throw new NotImplementedException();

View File

@ -50,8 +50,9 @@ namespace CodexReleaseTests.DataTests
var blockTtl = TimeSpan.FromMinutes(1.0);
var interval = TimeSpan.FromSeconds(10.0);
var bootstrapNode = StartCodex();
var geth = StartGethNode(s => s.IsMiner());
var contracts = Ci.StartCodexContracts(geth);
var contracts = Ci.StartCodexContracts(geth, bootstrapNode.Version);
var node = StartCodex(s => s
.EnableMarketplace(geth, contracts, m => m.WithInitial(100.Eth(), 100.Tst()))
.WithBlockTTL(blockTtl)

View File

@ -3,7 +3,6 @@ using CodexContractsPlugin;
using CodexContractsPlugin.Marketplace;
using CodexPlugin;
using CodexTests;
using DistTestCore;
using GethPlugin;
using Nethereum.Hex.HexConvertors.Extensions;
using NUnit.Framework;
@ -13,32 +12,26 @@ namespace CodexReleaseTests.MarketTests
{
public abstract class MarketplaceAutoBootstrapDistTest : AutoBootstrapDistTest
{
private readonly Dictionary<TestLifecycle, MarketplaceHandle> handles = new Dictionary<TestLifecycle, MarketplaceHandle>();
private MarketplaceHandle handle = null!;
protected const int StartingBalanceTST = 1000;
protected const int StartingBalanceEth = 10;
protected override void LifecycleStart(TestLifecycle lifecycle)
[SetUp]
public void SetupMarketplace()
{
base.LifecycleStart(lifecycle);
var geth = StartGethNode(s => s.IsMiner());
var contracts = Ci.StartCodexContracts(geth);
handles.Add(lifecycle, new MarketplaceHandle(geth, contracts));
}
protected override void LifecycleStop(TestLifecycle lifecycle, DistTestResult result)
{
base.LifecycleStop(lifecycle, result);
handles.Remove(lifecycle);
var contracts = Ci.StartCodexContracts(geth, BootstrapNode.Version);
handle = new MarketplaceHandle(geth, contracts);
}
protected IGethNode GetGeth()
{
return handles[Get()].Geth;
return handle.Geth;
}
protected ICodexContracts GetContracts()
{
return handles[Get()].Contracts;
return handle.Contracts;
}
protected TimeSpan GetPeriodDuration()
@ -195,6 +188,8 @@ namespace CodexReleaseTests.MarketTests
var expectedBalance = StartingBalanceTST.Tst() - GetContractFinalCost(pricePerBytePerSecond, contract, hosts);
AssertTstBalance(client, expectedBalance, "Client balance incorrect.");
Log($"Client has paid for contract. Balance: {expectedBalance}");
}
protected void AssertHostsWerePaidForContract(TestToken pricePerBytePerSecond, IStoragePurchaseContract contract, ICodexNodeGroup hosts)
@ -214,7 +209,9 @@ namespace CodexReleaseTests.MarketTests
foreach (var pair in expectedBalances)
{
AssertTstBalance(pair.Key, pair.Value, "Host was not paid for storage.");
AssertTstBalance(pair.Key, pair.Value, $"Host {pair.Key} was not paid for storage.");
Log($"Host {pair.Key} was paid for storage. Balance: {pair.Value}");
}
}

View File

@ -1,11 +1,5 @@
using CodexPlugin;
using CodexTests;
using CodexTests;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Utils;
namespace CodexReleaseTests.NodeTests

View File

@ -1,6 +1,6 @@
using NUnit.Framework;
[assembly: LevelOfParallelism(1)]
namespace CodexReleaseTests.DataTests
[assembly: LevelOfParallelism(10)]
namespace CodexReleaseTests
{
}

View File

@ -12,70 +12,45 @@ using Assert = NUnit.Framework.Assert;
namespace DistTestCore
{
[Parallelizable(ParallelScope.All)]
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
public abstract class DistTest
{
private const string TestNamespacePrefix = "cdx-";
private readonly Configuration configuration = new Configuration();
private readonly Assembly[] testAssemblies;
private static readonly Global global = new Global();
private readonly FixtureLog fixtureLog;
private readonly StatusLog statusLog;
private readonly object lifecycleLock = new object();
private readonly EntryPoint globalEntryPoint;
private readonly Dictionary<string, TestLifecycle> lifecycles = new Dictionary<string, TestLifecycle>();
private readonly string deployId;
private readonly TestLifecycle lifecycle;
private readonly string deployId = NameUtils.MakeDeployId();
public DistTest()
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
testAssemblies = assemblies.Where(a => a.FullName!.ToLowerInvariant().Contains("test")).ToArray();
deployId = NameUtils.MakeDeployId();
var logConfig = configuration.GetLogConfig();
var logConfig = global.Configuration.GetLogConfig();
var startTime = DateTime.UtcNow;
fixtureLog = new FixtureLog(logConfig, startTime, deployId);
fixtureLog = FixtureLog.Create(logConfig, startTime, deployId);
statusLog = new StatusLog(logConfig, startTime, "dist-tests", deployId);
globalEntryPoint = new EntryPoint(fixtureLog, configuration.GetK8sConfiguration(new DefaultK8sTimeSet(), TestNamespacePrefix), configuration.GetFileManagerFolder());
fixtureLog.Log("Test framework revision: " + GitInfo.GetStatus());
lifecycle = new TestLifecycle(fixtureLog.CreateTestLog(startTime), global.Configuration,
GetWebCallTimeSet(),
GetK8sTimeSet(),
Global.TestNamespacePrefix + Guid.NewGuid().ToString(),
deployId,
ShouldWaitForCleanup()
);
Initialize(fixtureLog);
}
[OneTimeSetUp]
public void GlobalSetup()
public static void GlobalSetup()
{
fixtureLog.Log($"Distributed Tests are starting...");
globalEntryPoint.Announce();
// Previous test run may have been interrupted.
// Begin by cleaning everything up.
try
{
Stopwatch.Measure(fixtureLog, "Global setup", () =>
{
globalEntryPoint.Tools.CreateWorkflow().DeleteNamespacesStartingWith(TestNamespacePrefix, wait: true);
});
}
catch (Exception ex)
{
GlobalTestFailure.HasFailed = true;
fixtureLog.Error($"Global setup cleanup failed with: {ex}");
throw;
}
fixtureLog.Log("Test framework revision: " + GitInfo.GetStatus());
fixtureLog.Log("Global setup cleanup successful");
global.Setup();
}
[OneTimeTearDown]
public void GlobalTearDown()
public static void GlobalTearDown()
{
globalEntryPoint.Decommission(
// There shouldn't be any of either, but clean everything up regardless.
deleteKubernetesResources: true,
deleteTrackedFiles: true,
waitTillDone: true
);
global.TearDown();
}
[SetUp]
@ -85,10 +60,6 @@ namespace DistTestCore
{
Assert.Inconclusive("Skip test: Previous test failed during clean up.");
}
else
{
CreateNewTestLifecycle();
}
}
[TearDown]
@ -109,18 +80,18 @@ namespace DistTestCore
{
get
{
return Get().CoreInterface;
return lifecycle.CoreInterface;
}
}
public TrackedFile GenerateTestFile(ByteSize size, string label = "")
{
return Get().GenerateTestFile(size, label);
return lifecycle.GenerateTestFile(size, label);
}
public TrackedFile GenerateTestFile(Action<IGenerateOption> options, string label = "")
{
return Get().GenerateTestFile(options, label);
return lifecycle.GenerateTestFile(options, label);
}
/// <summary>
@ -129,12 +100,22 @@ namespace DistTestCore
/// </summary>
public void ScopedTestFiles(Action action)
{
Get().GetFileManager().ScopedFiles(action);
lifecycle.GetFileManager().ScopedFiles(action);
}
public ILog GetTestLog()
{
return Get().Log;
return lifecycle.Log;
}
public IFileManager GetFileManager()
{
return lifecycle.GetFileManager();
}
public string GetTestNamespace()
{
return lifecycle.TestNamespace;
}
public void Log(string msg)
@ -151,64 +132,24 @@ namespace DistTestCore
public void Measure(string name, Action action)
{
Stopwatch.Measure(Get().Log, name, action);
Stopwatch.Measure(lifecycle.Log, name, action);
}
protected TimeRange GetTestRunTimeRange()
{
return new TimeRange(Get().TestStart, DateTime.UtcNow);
return new TimeRange(lifecycle.TestStart, DateTime.UtcNow);
}
protected virtual void Initialize(FixtureLog fixtureLog)
{
}
protected virtual void LifecycleStart(TestLifecycle lifecycle)
{
}
protected virtual void LifecycleStop(TestLifecycle lifecycle, DistTestResult testResult)
{
}
protected virtual void CollectStatusLogData(TestLifecycle lifecycle, Dictionary<string, string> data)
{
}
protected TestLifecycle Get()
{
lock (lifecycleLock)
{
return lifecycles[GetCurrentTestName()];
}
}
private void CreateNewTestLifecycle()
{
var testName = GetCurrentTestName();
fixtureLog.WriteLogTag();
Stopwatch.Measure(fixtureLog, $"Setup for {testName}", () =>
{
lock (lifecycleLock)
{
var testNamespace = TestNamespacePrefix + Guid.NewGuid().ToString();
var lifecycle = new TestLifecycle(
fixtureLog.CreateTestLog(),
configuration,
GetWebCallTimeSet(),
GetK8sTimeSet(),
testNamespace,
deployId,
ShouldWaitForCleanup());
lifecycles.Add(testName, lifecycle);
LifecycleStart(lifecycle);
}
});
}
private void DisposeTestLifecycle()
{
var lifecycle = Get();
var testResult = GetTestResult();
var testDuration = lifecycle.GetTestDuration();
var data = lifecycle.GetPluginMetadata();
@ -220,9 +161,7 @@ namespace DistTestCore
WriteEndTestLog(lifecycle.Log);
IncludeLogsOnTestFailure(lifecycle);
LifecycleStop(lifecycle, testResult);
lifecycle.DeleteAllResources();
lifecycles.Remove(GetCurrentTestName());
});
}
@ -236,11 +175,6 @@ namespace DistTestCore
Log(result.Message);
Log($"{result.StackTrace}");
}
if (result.Outcome.Status == TestStatus.Failed)
{
log.MarkAsFailed();
}
}
private IWebCallTimeSet GetWebCallTimeSet()
@ -284,7 +218,7 @@ namespace DistTestCore
var className = currentTest.ClassName;
var methodName = currentTest.MethodName;
var testClasses = testAssemblies.SelectMany(a => a.GetTypes()).Where(c => c.FullName == className).ToArray();
var testClasses = global.TestAssemblies.SelectMany(a => a.GetTypes()).Where(c => c.FullName == className).ToArray();
var testMethods = testClasses.SelectMany(c => c.GetMethods()).Where(m => m.Name == methodName).ToArray();
return testMethods.Select(m => m.GetCustomAttribute<T>())
@ -293,24 +227,24 @@ namespace DistTestCore
.ToArray();
}
protected IDownloadedLog[] DownloadAllLogs()
{
return lifecycle.DownloadAllLogs();
}
private void IncludeLogsOnTestFailure(TestLifecycle lifecycle)
{
var testStatus = TestContext.CurrentContext.Result.Outcome.Status;
if (testStatus == TestStatus.Failed)
{
fixtureLog.MarkAsFailed();
}
if (ShouldDownloadAllLogs(testStatus))
{
lifecycle.Log.Log("Downloading all container logs...");
lifecycle.DownloadAllLogs();
DownloadAllLogs();
}
}
private bool ShouldDownloadAllLogs(TestStatus testStatus)
{
if (configuration.AlwaysDownloadContainerLogs) return true;
if (global.Configuration.AlwaysDownloadContainerLogs) return true;
if (!IsDownloadingLogsEnabled()) return false;
if (testStatus == TestStatus.Failed)
{
@ -325,7 +259,7 @@ namespace DistTestCore
return $"[{TestContext.CurrentContext.Test.Name}]";
}
private DistTestResult GetTestResult()
public DistTestResult GetTestResult()
{
var success = TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Passed;
var status = TestContext.CurrentContext.Result.Outcome.Status.ToString();

View File

@ -0,0 +1,60 @@
using System.Reflection;
using Core;
using Logging;
namespace DistTestCore
{
public class Global
{
public const string TestNamespacePrefix = "cdx-";
public Configuration Configuration { get; } = new Configuration();
public Assembly[] TestAssemblies { get; }
private readonly EntryPoint globalEntryPoint;
private readonly ILog log;
public Global()
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
TestAssemblies = assemblies.Where(a => a.FullName!.ToLowerInvariant().Contains("test")).ToArray();
log = new ConsoleLog();
globalEntryPoint = new EntryPoint(
log,
Configuration.GetK8sConfiguration(
new DefaultK8sTimeSet(),
TestNamespacePrefix
),
Configuration.GetFileManagerFolder()
);
}
public void Setup()
{
try
{
Stopwatch.Measure(log, "Global setup", () =>
{
globalEntryPoint.Announce();
globalEntryPoint.Tools.CreateWorkflow().DeleteNamespacesStartingWith(TestNamespacePrefix, wait: true);
});
}
catch (Exception ex)
{
GlobalTestFailure.HasFailed = true;
log.Error($"Global setup cleanup failed with: {ex}");
throw;
}
}
public void TearDown()
{
globalEntryPoint.Decommission(
// There shouldn't be any of either, but clean everything up regardless.
deleteKubernetesResources: true,
deleteTrackedFiles: true,
waitTillDone: true
);
}
}
}

View File

@ -2,27 +2,83 @@
namespace DistTestCore.Logs
{
public abstract class BaseTestLog : BaseLog
public abstract class BaseTestLog : ILog
{
private bool hasFailed;
private readonly string deployId;
private readonly ILog backingLog;
protected BaseTestLog(string deployId)
protected BaseTestLog(ILog backingLog, string deployId)
{
this.deployId = deployId;
this.backingLog = backingLog;
DeployId = deployId;
}
public string DeployId { get; }
public void AddStringReplace(string from, string to)
{
backingLog.AddStringReplace(from, to);
}
public LogFile CreateSubfile(string addName, string ext = "log")
{
return backingLog.CreateSubfile(addName, ext);
}
public void Debug(string message = "", int skipFrames = 0)
{
backingLog.Debug(message, skipFrames);
}
public void Error(string message)
{
backingLog.Error(message);
}
public string GetFullName()
{
return backingLog.GetFullName();
}
public void Log(string message)
{
backingLog.Log(message);
}
public void Raw(string message)
{
backingLog.Raw(message);
}
public void WriteLogTag()
{
var category = NameUtils.GetCategoryName();
var name = NameUtils.GetTestMethodName();
LogFile.WriteRaw($"{deployId} {category} {name}");
backingLog.Raw($"{DeployId} {category} {name}");
}
public void MarkAsFailed()
protected static ILog CreateMainLog(string fullName, string name)
{
if (hasFailed) return;
hasFailed = true;
ILog log = new FileLog(fullName);
log = ApplyConsoleOutput(log);
return log;
}
private static ILog ApplyConsoleOutput(ILog log)
{
// If we're running as a release test, we'll split the log output
// to the console as well.
var testType = Environment.GetEnvironmentVariable("TEST_TYPE");
if (string.IsNullOrEmpty(testType) || testType.ToLowerInvariant() != "release-tests")
{
return log;
}
return new LogSplitter(
log,
new ConsoleLog()
);
}
}
}

View File

@ -4,28 +4,21 @@ namespace DistTestCore.Logs
{
public class FixtureLog : BaseTestLog
{
private readonly string fullName;
private readonly string deployId;
public FixtureLog(LogConfig config, DateTime start, string deployId, string name = "") : base(deployId)
public FixtureLog(ILog backingLog, string deployId)
: base(backingLog, deployId)
{
this.deployId = deployId;
fullName = NameUtils.GetFixtureFullName(config, start, name);
}
public TestLog CreateTestLog(string name = "")
public TestLog CreateTestLog(DateTime start, string name = "")
{
return new TestLog(fullName, deployId, name);
return TestLog.Create(this, start, name);
}
public void DeleteFolder()
public static FixtureLog Create(LogConfig config, DateTime start, string deployId, string name = "")
{
Directory.Delete(fullName, true);
}
protected override string GetFullName()
{
return fullName;
var fullName = NameUtils.GetFixtureFullName(config, start, name);
var log = CreateMainLog(fullName, name);
return new FixtureLog(log, deployId);
}
}
}
}

View File

@ -1,20 +1,21 @@
namespace DistTestCore.Logs
using Logging;
namespace DistTestCore.Logs
{
public class TestLog : BaseTestLog
{
private readonly string fullName;
public TestLog(string folder, string deployId, string name = "") : base(deployId)
public TestLog(ILog backingLog, string methodName, string deployId, string name = "")
: base(backingLog, deployId)
{
var methodName = NameUtils.GetTestMethodName(name);
fullName = Path.Combine(folder, methodName);
Log($"*** Begin: {methodName}");
backingLog.Log($"*** Begin: {methodName}");
}
protected override string GetFullName()
public static TestLog Create(FixtureLog parentLog, DateTime start, string name = "")
{
return fullName;
var methodName = NameUtils.GetTestLogFileName(start, name);
var fullName = Path.Combine(parentLog.GetFullName(), methodName);
var backingLog = CreateMainLog(fullName, name);
return new TestLog(backingLog, methodName, parentLog.DeployId);
}
}
}

View File

@ -5,6 +5,11 @@ namespace DistTestCore
{
public static class NameUtils
{
public static string GetTestLogFileName(DateTime start, string name = "")
{
return $"{Pad(start.Hour)}-{Pad(start.Minute)}-{Pad(start.Second)}Z_{GetTestMethodName(name)}";
}
public static string GetTestMethodName(string name = "")
{
if (!string.IsNullOrEmpty(name)) return name;
@ -16,7 +21,7 @@ namespace DistTestCore
public static string GetFixtureFullName(LogConfig config, DateTime start, string name)
{
var folder = DetermineFolder(config, start);
var fixtureName = GetFixtureName(name, start);
var fixtureName = GetRawFixtureName();
return Path.Combine(folder, fixtureName);
}
@ -25,6 +30,7 @@ namespace DistTestCore
var test = TestContext.CurrentContext.Test;
if (test.ClassName!.Contains("AdhocContext")) return "none";
var className = test.ClassName!.Substring(test.ClassName.LastIndexOf('.') + 1);
className += FormatArguments(test);
return className.Replace('.', '-');
}
@ -54,7 +60,7 @@ namespace DistTestCore
private static string FormatArguments(TestContext.TestAdapter test)
{
if (test.Arguments == null || !test.Arguments.Any()) return "";
if (test.Arguments == null || test.Arguments.Length == 0) return "";
return $"[{string.Join(',', test.Arguments.Select(FormatArgument).ToArray())}]";
}
@ -69,6 +75,8 @@ namespace DistTestCore
private static string ReplaceInvalidCharacters(string name)
{
return name
.Replace("codexstorage/nim-codex:", "")
.Replace("-dist-tests", "")
.Replace(":", "_")
.Replace("/", "_")
.Replace("\\", "_");
@ -82,13 +90,6 @@ namespace DistTestCore
Pad(start.Day));
}
private static string GetFixtureName(string name, DateTime start)
{
var className = GetRawFixtureName();
if (!string.IsNullOrEmpty(name)) className = name;
return $"{Pad(start.Hour)}-{Pad(start.Minute)}-{Pad(start.Second)}Z_{className.Replace('.', '-')}";
}
private static string Pad(int n)
{
return n.ToString().PadLeft(2, '0');

View File

@ -1,47 +1,35 @@
using CodexClient;
using CodexPlugin;
using DistTestCore;
using NUnit.Framework;
namespace CodexTests
{
public class AutoBootstrapDistTest : CodexDistTest
{
private readonly Dictionary<TestLifecycle, ICodexNode> bootstrapNodes = new Dictionary<TestLifecycle, ICodexNode>();
private bool isBooting = false;
public ICodexNode BootstrapNode { get; private set; } = null!;
[SetUp]
public void SetUpBootstrapNode()
public void SetupBootstrapNode()
{
var tl = Get();
if (!bootstrapNodes.ContainsKey(tl))
{
bootstrapNodes.Add(tl, StartCodex(s => s.WithName("BOOTSTRAP_" + tl.TestNamespace)));
}
isBooting = true;
BootstrapNode = StartCodex(s => s.WithName("BOOTSTRAP_" + GetTestNamespace()));
isBooting = false;
}
[TearDown]
public void TearDownBootstrapNode()
{
bootstrapNodes.Remove(Get());
BootstrapNode.Stop(waitTillStopped: false);
}
protected override void OnCodexSetup(ICodexSetup setup)
{
if (isBooting) return;
var node = BootstrapNode;
if (node != null) setup.WithBootstrapNode(node);
}
protected ICodexNode? BootstrapNode
{
get
{
var tl = Get();
if (bootstrapNodes.TryGetValue(tl, out var node))
{
return node;
}
return null;
}
}
}
}

View File

@ -31,7 +31,7 @@ namespace ExperimentalTests.BasicTests
);
var geth = StartGethNode(s => s.IsMiner().WithName("disttest-geth"));
var contracts = Ci.StartCodexContracts(geth);
var contracts = Ci.StartCodexContracts(geth, BootstrapNode.Version);
var numberOfHosts = 5;
var hosts = StartCodex(numberOfHosts, s => s

View File

@ -1,6 +1,5 @@
using BlockchainUtils;
using CodexClient;
using CodexClient.Hooks;
using CodexContractsPlugin;
using CodexNetDeployer;
using CodexPlugin;
@ -17,85 +16,14 @@ using Newtonsoft.Json;
using NUnit.Framework;
using NUnit.Framework.Constraints;
using OverwatchTranscript;
using Utils;
namespace CodexTests
{
public class CodexLogTrackerProvider : ICodexHooksProvider
{
private readonly Action<ICodexNode> addNode;
public CodexLogTrackerProvider(Action<ICodexNode> addNode)
{
this.addNode = addNode;
}
// See TestLifecycle.cs DownloadAllLogs()
public ICodexNodeHooks CreateHooks(string nodeName)
{
return new CodexLogTracker(addNode);
}
public class CodexLogTracker : ICodexNodeHooks
{
private readonly Action<ICodexNode> addNode;
public CodexLogTracker(Action<ICodexNode> addNode)
{
this.addNode = addNode;
}
public void OnFileDownloaded(ByteSize size, ContentId cid)
{
}
public void OnFileDownloading(ContentId cid)
{
}
public void OnFileUploaded(string uid, ByteSize size, ContentId cid)
{
}
public void OnFileUploading(string uid, ByteSize size)
{
}
public void OnNodeStarted(ICodexNode node, string peerId, string nodeId)
{
addNode(node);
}
public void OnNodeStarting(DateTime startUtc, string image, EthAccount? ethAccount)
{
}
public void OnNodeStopping()
{
}
public void OnStorageAvailabilityCreated(StorageAvailability response)
{
}
public void OnStorageContractSubmitted(StoragePurchaseContract storagePurchaseContract)
{
}
public void OnStorageContractUpdated(StoragePurchase purchaseStatus)
{
}
}
}
public class CodexDistTest : DistTest
{
private static readonly object _lock = new object();
private static readonly Dictionary<TestLifecycle, CodexTranscriptWriter> writers = new Dictionary<TestLifecycle, CodexTranscriptWriter>();
private static readonly Dictionary<TestLifecycle, BlockCache> blockCaches = new Dictionary<TestLifecycle, BlockCache>();
// this entire structure is not good and needs to be destroyed at the earliest convenience:
private static readonly Dictionary<TestLifecycle, List<ICodexNode>> nodes = new Dictionary<TestLifecycle, List<ICodexNode>>();
private readonly BlockCache blockCache = new BlockCache();
private readonly List<ICodexNode> nodes = new List<ICodexNode>();
private CodexTranscriptWriter? writer;
public CodexDistTest()
{
@ -105,38 +33,25 @@ namespace CodexTests
ProjectPlugin.Load<MetricsPlugin.MetricsPlugin>();
}
[SetUp]
public void SetupCodexDistTest()
{
writer = SetupTranscript();
}
[TearDown]
public void TearDownCodexDistTest()
{
TeardownTranscript();
}
protected override void Initialize(FixtureLog fixtureLog)
{
var localBuilder = new LocalCodexBuilder(fixtureLog);
localBuilder.Intialize();
localBuilder.Build();
}
protected override void LifecycleStart(TestLifecycle lifecycle)
{
base.LifecycleStart(lifecycle);
SetupTranscript(lifecycle);
Ci.AddCodexHooksProvider(new CodexLogTrackerProvider(n =>
{
lock (_lock)
{
if (!nodes.ContainsKey(lifecycle)) nodes.Add(lifecycle, new List<ICodexNode>());
nodes[lifecycle].Add(n);
}
}));
}
protected override void LifecycleStop(TestLifecycle lifecycle, DistTestResult result)
{
base.LifecycleStop(lifecycle, result);
TeardownTranscript(lifecycle, result);
if (!result.Success)
{
var codexNodes = nodes[lifecycle];
foreach (var node in codexNodes) node.DownloadLog();
}
Ci.AddCodexHooksProvider(new CodexLogTrackerProvider(nodes.Add));
}
public ICodexNode StartCodex()
@ -167,7 +82,7 @@ namespace CodexTests
public IGethNode StartGethNode(Action<IGethSetup> setup)
{
return Ci.StartGethNode(GetBlockCache(), setup);
return Ci.StartGethNode(blockCache, setup);
}
public PeerConnectionTestHelpers CreatePeerConnectionTestHelpers()
@ -177,7 +92,7 @@ namespace CodexTests
public PeerDownloadTestHelpers CreatePeerDownloadTestHelpers()
{
return new PeerDownloadTestHelpers(GetTestLog(), Get().GetFileManager());
return new PeerDownloadTestHelpers(GetTestLog(), GetFileManager());
}
public void AssertBalance(ICodexContracts contracts, ICodexNode codexNode, Constraint constraint, string msg = "")
@ -255,82 +170,47 @@ namespace CodexTests
return null;
}
private void SetupTranscript(TestLifecycle lifecycle)
private CodexTranscriptWriter? SetupTranscript()
{
var attr = GetTranscriptAttributeOfCurrentTest();
if (attr == null) return;
if (attr == null) return null;
var config = new CodexTranscriptWriterConfig(
attr.OutputFilename,
attr.IncludeBlockReceivedEvents
);
var log = new LogPrefixer(lifecycle.Log, "(Transcript) ");
var log = new LogPrefixer(GetTestLog(), "(Transcript) ");
var writer = new CodexTranscriptWriter(log, config, Transcript.NewWriter(log));
Ci.AddCodexHooksProvider(writer);
lock (_lock)
{
writers.Add(lifecycle, writer);
}
return writer;
}
private void TeardownTranscript(TestLifecycle lifecycle, DistTestResult result)
private void TeardownTranscript()
{
var attr = GetTranscriptAttributeOfCurrentTest();
if (attr == null) return;
var outputFilepath = GetOutputFullPath(lifecycle, attr);
CodexTranscriptWriter writer = null!;
lock (_lock)
{
writer = writers[lifecycle];
writers.Remove(lifecycle);
}
if (writer == null) return;
var result = GetTestResult();
var log = GetTestLog();
writer.AddResult(result.Success, result.Result);
try
{
Stopwatch.Measure(lifecycle.Log, "Transcript.ProcessLogs", () =>
Stopwatch.Measure(log, "Transcript.ProcessLogs", () =>
{
writer.ProcessLogs(lifecycle.DownloadAllLogs());
writer.ProcessLogs(DownloadAllLogs());
});
Stopwatch.Measure(lifecycle.Log, $"Transcript.Finalize: {outputFilepath}", () =>
Stopwatch.Measure(log, $"Transcript.FinalizeWriter", () =>
{
writer.IncludeFile(lifecycle.Log.LogFile.Filename);
writer.Finalize(outputFilepath);
writer.IncludeFile(log.GetFullName() + ".log");
writer.FinalizeWriter();
});
}
catch (Exception ex)
{
lifecycle.Log.Error("Failure during transcript teardown: " + ex);
log.Error("Failure during transcript teardown: " + ex);
}
}
private string GetOutputFullPath(TestLifecycle lifecycle, CreateTranscriptAttribute attr)
{
var outputPath = Path.GetDirectoryName(lifecycle.Log.LogFile.Filename);
if (outputPath == null) throw new Exception("Logfile path is null");
var filename = Path.GetFileNameWithoutExtension(lifecycle.Log.LogFile.Filename);
if (string.IsNullOrEmpty(filename)) throw new Exception("Logfile name is null or empty");
var outputFile = Path.Combine(outputPath, filename + "_" + attr.OutputFilename);
if (!outputFile.EndsWith(".owts")) outputFile += ".owts";
return outputFile;
}
private BlockCache GetBlockCache()
{
var lifecycle = Get();
lock (_lock)
{
if (!blockCaches.ContainsKey(lifecycle))
{
blockCaches[lifecycle] = new BlockCache();
}
}
return blockCaches[lifecycle];
}
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]

View File

@ -0,0 +1,73 @@
using CodexClient;
using CodexClient.Hooks;
using Utils;
namespace CodexTests
{
public class CodexLogTrackerProvider : ICodexHooksProvider
{
private readonly Action<ICodexNode> addNode;
public CodexLogTrackerProvider(Action<ICodexNode> addNode)
{
this.addNode = addNode;
}
// See TestLifecycle.cs DownloadAllLogs()
public ICodexNodeHooks CreateHooks(string nodeName)
{
return new CodexLogTracker(addNode);
}
public class CodexLogTracker : ICodexNodeHooks
{
private readonly Action<ICodexNode> addNode;
public CodexLogTracker(Action<ICodexNode> addNode)
{
this.addNode = addNode;
}
public void OnFileDownloaded(ByteSize size, ContentId cid)
{
}
public void OnFileDownloading(ContentId cid)
{
}
public void OnFileUploaded(string uid, ByteSize size, ContentId cid)
{
}
public void OnFileUploading(string uid, ByteSize size)
{
}
public void OnNodeStarted(ICodexNode node, string peerId, string nodeId)
{
addNode(node);
}
public void OnNodeStarting(DateTime startUtc, string image, EthAccount? ethAccount)
{
}
public void OnNodeStopping()
{
}
public void OnStorageAvailabilityCreated(StorageAvailability response)
{
}
public void OnStorageContractSubmitted(StoragePurchaseContract storagePurchaseContract)
{
}
public void OnStorageContractUpdated(StoragePurchase purchaseStatus)
{
}
}
}
}

View File

@ -21,7 +21,7 @@ namespace ExperimentalTests.DownloadConnectivityTests
public void MarketplaceDoesNotInterfereWithPeerDownload()
{
var geth = StartGethNode(s => s.IsMiner());
var contracts = Ci.StartCodexContracts(geth);
var contracts = Ci.StartCodexContracts(geth, BootstrapNode.Version);
var nodes = StartCodex(2, s => s.EnableMarketplace(geth, contracts, m => m
.WithInitial(10.Eth(), 1000.TstWei())));

View File

@ -31,7 +31,7 @@ namespace ExperimentalTests.PeerDiscoveryTests
public void MarketplaceDoesNotInterfereWithPeerDiscovery()
{
var geth = StartGethNode(s => s.IsMiner());
var contracts = Ci.StartCodexContracts(geth);
var contracts = Ci.StartCodexContracts(geth, BootstrapNode.Version);
var nodes = StartCodex(2, s => s.EnableMarketplace(geth, contracts, m => m
.WithInitial(10.Eth(), 1000.TstWei())));

View File

@ -1,370 +0,0 @@
using CodexClient;
using CodexContractsPlugin;
using CodexDiscordBotPlugin;
using CodexPlugin;
using CodexTests;
using Core;
using DiscordRewards;
using DistTestCore;
using GethPlugin;
using KubernetesWorkflow.Types;
using Logging;
using Newtonsoft.Json;
using NUnit.Framework;
using Utils;
namespace ExperimentalTests.UtilityTests
{
[TestFixture]
public class DiscordBotTests : AutoBootstrapDistTest
{
private readonly RewardRepo repo = new RewardRepo();
private readonly TestToken hostInitialBalance = 3000000.TstWei();
private readonly TestToken clientInitialBalance = 1000000000.TstWei();
private readonly EthAccount clientAccount = EthAccountGenerator.GenerateNew();
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<ChainEventMessage> receivedEvents = new List<ChainEventMessage>();
[Test]
[DontDownloadLogs]
[Ignore("Used to debug testnet bots.")]
public void BotRewardTest()
{
var geth = StartGethNode(s => s.IsMiner().WithName("disttest-geth"));
var contracts = Ci.StartCodexContracts(geth);
var gethInfo = CreateGethInfo(geth, contracts);
var botContainer = StartDiscordBot(gethInfo);
var rewarderContainer = StartRewarderBot(gethInfo, botContainer);
StartHosts(geth, contracts);
var client = StartClient(geth, contracts);
var apiCalls = new RewardApiCalls(GetTestLog(), Ci, botContainer);
apiCalls.Start(OnCommand);
var purchaseContract = ClientPurchasesStorage(client);
purchaseContract.WaitForStorageContractStarted();
purchaseContract.WaitForStorageContractFinished();
// todo: removed from codexclient:
//contracts.WaitUntilNextPeriod();
//contracts.WaitUntilNextPeriod();
//var blocks = 3;
//Log($"Waiting {blocks} blocks for nodes to process payouts...");
//Thread.Sleep(GethContainerRecipe.BlockInterval * blocks);
Thread.Sleep(rewarderInterval * 3);
apiCalls.Stop();
AssertEventOccurance("Created as New.", 1);
AssertEventOccurance("SlotFilled", Convert.ToInt32(GetNumberOfRequiredHosts()));
AssertEventOccurance("Transit: New -> Started", 1);
AssertEventOccurance("Transit: Started -> Finished", 1);
foreach (var r in repo.Rewards)
{
var seen = rewardsSeen.Any(s => r.RoleId == s);
Log($"{Lookup(r.RoleId)} = {seen}");
}
Assert.That(repo.Rewards.All(r => rewardsSeen.Contains(r.RoleId)));
}
private string Lookup(ulong rewardId)
{
var reward = repo.Rewards.Single(r => r.RoleId == rewardId);
return $"({rewardId})'{reward.Message}'";
}
private void AssertEventOccurance(string msg, int expectedCount)
{
Assert.That(receivedEvents.Count(e => e.Message.Contains(msg)), Is.EqualTo(expectedCount),
$"Event '{msg}' did not occure correct number of times.");
}
private void OnCommand(string timestamp, GiveRewardsCommand call)
{
Log($"<API call {timestamp}>");
foreach (var e in call.EventsOverview)
{
Assert.That(receivedEvents.All(r => r.BlockNumber < e.BlockNumber), "Received event out of order.");
}
receivedEvents.AddRange(call.EventsOverview);
foreach (var e in call.EventsOverview)
{
Log("\tEvent: " + e);
}
foreach (var r in call.Rewards)
{
var reward = repo.Rewards.Single(a => a.RoleId == r.RewardId);
if (r.UserAddresses.Any()) rewardsSeen.Add(reward.RoleId);
foreach (var address in r.UserAddresses)
{
var user = IdentifyAccount(address);
Log("\tReward: " + user + ": " + reward.Message);
}
}
Log($"</API call>");
}
private IStoragePurchaseContract ClientPurchasesStorage(ICodexNode client)
{
var testFile = GenerateTestFile(GetMinFileSize());
var contentId = client.UploadFile(testFile);
var purchase = new StoragePurchaseRequest(contentId)
{
PricePerBytePerSecond = 2.TstWei(),
CollateralPerByte = 10.TstWei(),
MinRequiredNumberOfNodes = GetNumberOfRequiredHosts(),
NodeFailureTolerance = 2,
ProofProbability = 5,
Duration = GetMinRequiredRequestDuration(),
Expiry = GetMinRequiredRequestDuration() - TimeSpan.FromMinutes(1)
};
return client.Marketplace.RequestStorage(purchase);
}
private ICodexNode StartClient(IGethNode geth, ICodexContracts contracts)
{
var node = StartCodex(s => s
.WithName("Client")
.EnableMarketplace(geth, contracts, m => m
.WithAccount(clientAccount)
.WithInitial(10.Eth(), clientInitialBalance)));
Log($"Client {node.EthAccount.EthAddress}");
return node;
}
private RunningPod StartRewarderBot(DiscordBotGethInfo gethInfo, RunningContainer botContainer)
{
return Ci.DeployRewarderBot(new RewarderBotStartupConfig(
name: "rewarder-bot",
discordBotHost: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Host,
discordBotPort: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Port,
intervalMinutes: Convert.ToInt32(Math.Round(rewarderInterval.TotalMinutes)),
historyStartUtc: DateTime.UtcNow,
gethInfo: gethInfo,
dataPath: null
));
}
private DiscordBotGethInfo CreateGethInfo(IGethNode geth, ICodexContracts contracts)
{
return new DiscordBotGethInfo(
host: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Host,
port: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Port,
privKey: geth.StartResult.Account.PrivateKey,
marketplaceAddress: contracts.Deployment.MarketplaceAddress,
tokenAddress: contracts.Deployment.TokenAddress,
abi: contracts.Deployment.Abi
);
}
private RunningContainer StartDiscordBot(DiscordBotGethInfo gethInfo)
{
var bot = Ci.DeployCodexDiscordBot(new DiscordBotStartupConfig(
name: "discord-bot",
token: "aaa",
serverName: "ThatBen's server",
adminRoleName: "bottest-admins",
adminChannelName: "admin-channel",
rewardChannelName: "rewards-channel",
kubeNamespace: "notneeded",
gethInfo: gethInfo
));
return bot.Containers.Single();
}
private void StartHosts(IGethNode geth, ICodexContracts contracts)
{
var hosts = StartCodex(GetNumberOfLiveHosts(), s => s
.WithName("Host")
.WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn)
{
ContractClock = CodexLogLevel.Trace,
})
.WithStorageQuota(Mult(GetMinFileSizePlus(50), GetNumberOfLiveHosts()))
.EnableMarketplace(geth, contracts, m => m
.WithInitial(10.Eth(), hostInitialBalance)
.AsStorageNode()
.AsValidator()));
var availability = new StorageAvailability(
totalSpace: Mult(GetMinFileSize(), GetNumberOfLiveHosts()),
maxDuration: TimeSpan.FromMinutes(30),
minPricePerBytePerSecond: 1.TstWei(),
totalCollateral: hostInitialBalance
);
foreach (var host in hosts)
{
hostAccounts.Add(host.EthAccount);
host.Marketplace.MakeStorageAvailable(availability);
}
}
private int GetNumberOfLiveHosts()
{
return Convert.ToInt32(GetNumberOfRequiredHosts()) + 3;
}
private ByteSize Mult(ByteSize size, int mult)
{
return new ByteSize(size.SizeInBytes * mult);
}
private ByteSize GetMinFileSizePlus(int plusMb)
{
return new ByteSize(GetMinFileSize().SizeInBytes + plusMb.MB().SizeInBytes);
}
private ByteSize GetMinFileSize()
{
ulong minSlotSize = 0;
ulong minNumHosts = 0;
foreach (var r in repo.Rewards)
{
var s = Convert.ToUInt64(r.CheckConfig.MinSlotSize.SizeInBytes);
var h = r.CheckConfig.MinNumberOfHosts;
if (s > minSlotSize) minSlotSize = s;
if (h > minNumHosts) minNumHosts = h;
}
var minFileSize = (minSlotSize + 1024) * minNumHosts;
return new ByteSize(Convert.ToInt64(minFileSize));
}
private uint GetNumberOfRequiredHosts()
{
return Convert.ToUInt32(repo.Rewards.Max(r => r.CheckConfig.MinNumberOfHosts));
}
private TimeSpan GetMinRequiredRequestDuration()
{
return repo.Rewards.Max(r => r.CheckConfig.MinDuration) + TimeSpan.FromSeconds(10);
}
private string IdentifyAccount(string address)
{
if (address == clientAccount.EthAddress.Address) return "Client";
try
{
var index = hostAccounts.FindIndex(a => a.EthAddress.Address == address);
return "Host" + index;
}
catch
{
return "UNKNOWN";
}
}
public class RewardApiCalls
{
private readonly ContainerFileMonitor monitor;
public RewardApiCalls(ILog log, CoreInterface ci, RunningContainer botContainer)
{
monitor = new ContainerFileMonitor(log, ci, botContainer, "/app/datapath/logs/discordbot.log");
}
public void Start(Action<string, GiveRewardsCommand> onCommand)
{
monitor.Start(line => ParseLine(line, onCommand));
}
public void Stop()
{
monitor.Stop();
}
private void ParseLine(string line, Action<string, GiveRewardsCommand> onCommand)
{
try
{
var timestamp = line.Substring(0, 30);
var json = line.Substring(31);
var cmd = JsonConvert.DeserializeObject<GiveRewardsCommand>(json);
if (cmd != null)
{
onCommand(timestamp, cmd);
}
}
catch
{
}
}
}
public class ContainerFileMonitor
{
private readonly ILog log;
private readonly CoreInterface ci;
private readonly RunningContainer botContainer;
private readonly string filePath;
private readonly CancellationTokenSource cts = new CancellationTokenSource();
private readonly List<string> seenLines = new List<string>();
private Task worker = Task.CompletedTask;
private Action<string> onNewLine = c => { };
public ContainerFileMonitor(ILog log, CoreInterface ci, RunningContainer botContainer, string filePath)
{
this.log = log;
this.ci = ci;
this.botContainer = botContainer;
this.filePath = filePath;
}
public void Start(Action<string> onNewLine)
{
this.onNewLine = onNewLine;
worker = Task.Run(Worker);
}
public void Stop()
{
cts.Cancel();
worker.Wait();
}
// did any container crash? that's why it repeats?
private void Worker()
{
while (!cts.IsCancellationRequested)
{
Update();
}
}
private void Update()
{
Thread.Sleep(TimeSpan.FromSeconds(10));
if (cts.IsCancellationRequested) return;
var botLog = ci.ExecuteContainerCommand(botContainer, "cat", filePath);
var lines = botLog.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
// log.Log("line: " + line);
if (!seenLines.Contains(line))
{
seenLines.Add(line);
onNewLine(line);
}
}
}
}
}
}

View File

@ -0,0 +1,19 @@
using NUnit.Framework;
using Utils;
namespace FrameworkTests.Utils
{
[TestFixture]
public class RandomUtilsTests
{
[Test]
[Combinatorial]
public void TestRandomStringLength(
[Values(1, 5, 10, 1023, 1024, 1025, 2222)] int length)
{
var str = RandomUtils.GenerateRandomString(length);
Assert.That(str.Length, Is.EqualTo(length));
}
}
}

View File

@ -7,6 +7,7 @@ namespace AutoClient
public class CodexWrapper
{
private readonly App app;
private static readonly Random r = new Random();
public CodexWrapper(App app, ICodexNode node)
{
@ -26,11 +27,11 @@ namespace AutoClient
var result = Node.Marketplace.RequestStorage(new StoragePurchaseRequest(cid)
{
CollateralPerByte = app.Config.CollateralPerByte.TstWei(),
Duration = TimeSpan.FromMinutes(app.Config.ContractDurationMinutes),
Duration = GetDuration(),
Expiry = TimeSpan.FromMinutes(app.Config.ContractExpiryMinutes),
MinRequiredNumberOfNodes = Convert.ToUInt32(app.Config.NumHosts),
NodeFailureTolerance = Convert.ToUInt32(app.Config.HostTolerance),
PricePerBytePerSecond = app.Config.PricePerBytePerSecond.TstWei(),
PricePerBytePerSecond = GetPricePerBytePerSecond(),
ProofProbability = 15
});
return result;
@ -40,5 +41,25 @@ namespace AutoClient
{
return Node.GetPurchaseStatus(pid);
}
private TestToken GetPricePerBytePerSecond()
{
var i = app.Config.PricePerBytePerSecond;
i -= 100;
i += r.Next(0, 1000);
return i.TstWei();
}
private TimeSpan GetDuration()
{
var i = app.Config.ContractDurationMinutes;
var day = 60 * 24;
i -= day;
i -= 10; // We don't want to accidentally hit exactly 7 days because that's the limit of the storage node availabilities.
i += r.Next(0, day * 2);
return TimeSpan.FromMinutes(i);
}
}
}

View File

@ -59,6 +59,9 @@ namespace AutoClient
"/root/codex-testnet-starter/scripts/eth_7.address" + ";" +
"/root/codex-testnet-starter/scripts/eth_8.address";
[Uniform("slowModeDelayMinutes", "smdm", "SLOWMODEDELAYMINUTES", false, "When contract failure threshold is reached, slow down process for each file by this amount of minutes.")]
public int SlowModeDelayMinutes { get; set; } = 60 * 1;
public string LogPath
{
get

View File

@ -7,6 +7,11 @@ namespace AutoClient.Modes.FolderStore
public interface IFileSaverEventHandler
{
void SaveChanges();
}
public interface IFileSaverResultHandler
{
void OnSuccess();
void OnFailure();
}
@ -17,16 +22,18 @@ namespace AutoClient.Modes.FolderStore
private readonly Stats stats;
private readonly string folderFile;
private readonly FileStatus entry;
private readonly IFileSaverEventHandler handler;
private readonly IFileSaverEventHandler saveHandler;
private readonly IFileSaverResultHandler resultHandler;
public FileSaver(ILog log, LoadBalancer loadBalancer, Stats stats, string folderFile, FileStatus entry, IFileSaverEventHandler handler)
public FileSaver(ILog log, LoadBalancer loadBalancer, Stats stats, string folderFile, FileStatus entry, IFileSaverEventHandler saveHandler, IFileSaverResultHandler resultHandler)
{
this.log = log;
this.loadBalancer = loadBalancer;
this.stats = stats;
this.folderFile = folderFile;
this.entry = entry;
this.handler = handler;
this.saveHandler = saveHandler;
this.resultHandler = resultHandler;
}
public void Process()
@ -46,9 +53,9 @@ namespace AutoClient.Modes.FolderStore
loadBalancer.DispatchOnCodex(instance =>
{
entry.CodexNodeId = instance.Node.GetName();
handler.SaveChanges();
saveHandler.SaveChanges();
var run = new FileSaverRun(log, instance, stats, folderFile, entry, handler);
var run = new FileSaverRun(log, instance, stats, folderFile, entry, saveHandler, resultHandler);
run.Process();
});
}
@ -57,7 +64,7 @@ namespace AutoClient.Modes.FolderStore
{
loadBalancer.DispatchOnSpecificCodex(instance =>
{
var run = new FileSaverRun(log, instance, stats, folderFile, entry, handler);
var run = new FileSaverRun(log, instance, stats, folderFile, entry, saveHandler, resultHandler);
run.Process();
}, entry.CodexNodeId);
}
@ -70,17 +77,19 @@ namespace AutoClient.Modes.FolderStore
private readonly Stats stats;
private readonly string folderFile;
private readonly FileStatus entry;
private readonly IFileSaverEventHandler handler;
private readonly IFileSaverEventHandler saveHandler;
private readonly IFileSaverResultHandler resultHandler;
private readonly QuotaCheck quotaCheck;
public FileSaverRun(ILog log, CodexWrapper instance, Stats stats, string folderFile, FileStatus entry, IFileSaverEventHandler handler)
public FileSaverRun(ILog log, CodexWrapper instance, Stats stats, string folderFile, FileStatus entry, IFileSaverEventHandler saveHandler, IFileSaverResultHandler resultHandler)
{
this.log = log;
this.instance = instance;
this.stats = stats;
this.folderFile = folderFile;
this.entry = entry;
this.handler = handler;
this.saveHandler = saveHandler;
this.resultHandler = resultHandler;
quotaCheck = new QuotaCheck(log, folderFile, instance);
}
@ -127,7 +136,7 @@ namespace AutoClient.Modes.FolderStore
Thread.Sleep(TimeSpan.FromMinutes(1.0));
}
Log("Could not upload: Insufficient local storage quota.");
handler.OnFailure();
resultHandler.OnFailure();
return false;
}
@ -206,9 +215,9 @@ namespace AutoClient.Modes.FolderStore
entry.BasicCid = string.Empty;
stats.FailedUploads++;
log.Error("Failed to upload: " + exc);
handler.OnFailure();
resultHandler.OnFailure();
}
handler.SaveChanges();
saveHandler.SaveChanges();
}
private void CreateNewPurchase()
@ -224,17 +233,18 @@ namespace AutoClient.Modes.FolderStore
WaitForStarted(request);
stats.StorageRequestStats.SuccessfullyStarted++;
handler.SaveChanges();
saveHandler.SaveChanges();
Log($"Successfully started new purchase: '{entry.PurchaseId}' for {Time.FormatDuration(request.Purchase.Duration)}");
resultHandler.OnSuccess();
}
catch (Exception exc)
{
entry.EncodedCid = string.Empty;
entry.PurchaseId = string.Empty;
handler.SaveChanges();
saveHandler.SaveChanges();
log.Error("Failed to start new purchase: " + exc);
handler.OnFailure();
resultHandler.OnFailure();
}
}
@ -253,7 +263,7 @@ namespace AutoClient.Modes.FolderStore
throw new Exception("CID received from storage request was not protected.");
}
handler.SaveChanges();
saveHandler.SaveChanges();
Log("Saved new purchaseId: " + entry.PurchaseId);
return request;
}
@ -289,7 +299,7 @@ namespace AutoClient.Modes.FolderStore
Log("Request failed to start. State: " + update.State);
entry.EncodedCid = string.Empty;
entry.PurchaseId = string.Empty;
handler.SaveChanges();
saveHandler.SaveChanges();
return;
}
}
@ -297,7 +307,7 @@ namespace AutoClient.Modes.FolderStore
}
catch (Exception exc)
{
handler.OnFailure();
resultHandler.OnFailure();
Log($"Exception in {nameof(WaitForSubmittedToStarted)}: {exc}");
throw;
}

View File

@ -1,4 +1,5 @@
using Logging;
using Utils;
namespace AutoClient.Modes.FolderStore
{
@ -10,14 +11,16 @@ namespace AutoClient.Modes.FolderStore
private readonly JsonFile<FolderStatus> statusFile;
private readonly FolderStatus status;
private readonly BalanceChecker balanceChecker;
private readonly SlowModeHandler slowModeHandler;
private int changeCounter = 0;
private int failureCount = 0;
private int saveFolderJsonCounter = 0;
public FolderSaver(App app, LoadBalancer loadBalancer)
{
this.app = app;
this.loadBalancer = loadBalancer;
balanceChecker = new BalanceChecker(app);
slowModeHandler = new SlowModeHandler(app);
statusFile = new JsonFile<FolderStatus>(app, Path.Combine(app.Config.FolderToStore, FolderSaverFilename));
status = statusFile.Load();
@ -25,10 +28,11 @@ namespace AutoClient.Modes.FolderStore
public void Run()
{
saveFolderJsonCounter = 0;
var folderFiles = Directory.GetFiles(app.Config.FolderToStore);
if (!folderFiles.Any()) throw new Exception("No files found in " + app.Config.FolderToStore);
var saveFolderJsonCounter = 0;
balanceChecker.Check();
foreach (var folderFile in folderFiles)
{
@ -40,35 +44,30 @@ namespace AutoClient.Modes.FolderStore
SaveFile(folderFile);
}
if (failureCount > 3)
{
app.Log.Error("Failure count reached threshold. Stopping...");
app.Cts.Cancel();
return;
}
if (changeCounter > 1)
{
changeCounter = 0;
saveFolderJsonCounter++;
if (saveFolderJsonCounter > 5)
{
saveFolderJsonCounter = 0;
if (failureCount > 0)
{
app.Log.Log($"Failure count is reset. (Was: {failureCount})");
failureCount = 0;
}
balanceChecker.Check();
SaveFolderSaverJsonFile();
}
}
slowModeHandler.Check();
CheckAndSaveChanges();
statusFile.Save(status);
Thread.Sleep(100);
}
}
private void CheckAndSaveChanges()
{
if (changeCounter > 1)
{
changeCounter = 0;
saveFolderJsonCounter++;
if (saveFolderJsonCounter > 5)
{
saveFolderJsonCounter = 0;
balanceChecker.Check();
SaveFolderSaverJsonFile();
}
}
}
private void SaveFile(string folderFile)
{
var localFilename = Path.GetFileName(folderFile);
@ -113,7 +112,6 @@ namespace AutoClient.Modes.FolderStore
}
private const int MinCodexStorageFilesize = 262144;
private readonly Random random = new Random();
private readonly string paddingMessage = $"Codex currently requires a minimum filesize of {MinCodexStorageFilesize} bytes for datasets used in storage contracts. " +
$"Anything smaller, and the erasure-coding algorithms used for data durability won't function. Therefore, we apply this padding field to make sure this " +
$"file is larger than the minimal size. The following is pseudo-random: ";
@ -125,29 +123,16 @@ namespace AutoClient.Modes.FolderStore
if (info.Length < min)
{
var required = Math.Max(1024, min - info.Length);
status.Padding = paddingMessage + GenerateRandomString(required);
status.Padding = paddingMessage + RandomUtils.GenerateRandomString(required);
statusFile.Save(status);
}
}
private string GenerateRandomString(long required)
{
var result = "";
while (result.Length < required)
{
var bytes = new byte[1024];
random.NextBytes(bytes);
result += string.Join("", bytes.Select(b => b.ToString()));
}
return result;
}
private FileSaver CreateFileSaver(string folderFile, FileStatus entry)
{
var fixedLength = entry.Filename.PadRight(35);
var prefix = $"[{fixedLength}] ";
return new FileSaver(new LogPrefixer(app.Log, prefix), loadBalancer, status.Stats, folderFile, entry, this);
return new FileSaver(new LogPrefixer(app.Log, prefix), loadBalancer, status.Stats, folderFile, entry, this, slowModeHandler);
}
public void SaveChanges()
@ -155,10 +140,5 @@ namespace AutoClient.Modes.FolderStore
statusFile.Save(status);
changeCounter++;
}
public void OnFailure()
{
failureCount++;
}
}
}

View File

@ -0,0 +1,54 @@
namespace AutoClient.Modes.FolderStore
{
public class SlowModeHandler : IFileSaverResultHandler
{
private readonly App app;
private int failureCount = 0;
private bool slowMode = false;
private int recoveryCount = 0;
public SlowModeHandler(App app)
{
this.app = app;
}
public void OnSuccess()
{
failureCount = 0;
if (slowMode)
{
recoveryCount++;
if (recoveryCount > 3)
{
Log("Recovery limit reached. Exiting slow mode.");
slowMode = false;
failureCount = 0;
}
}
}
public void OnFailure()
{
failureCount++;
if (failureCount > 3 && !slowMode)
{
Log("Failure limit reached. Entering slow mode.");
slowMode = true;
recoveryCount = 0;
}
}
public void Check()
{
if (slowMode)
{
Thread.Sleep(TimeSpan.FromMinutes(app.Config.SlowModeDelayMinutes));
}
}
private void Log(string msg)
{
app.Log.Log(msg);
}
}
}

View File

@ -1,7 +1,6 @@
using BiblioTech.Options;
using Discord;
using Discord.WebSocket;
using Org.BouncyCastle.Utilities;
namespace BiblioTech
{

View File

@ -0,0 +1,66 @@
using Logging;
namespace BiblioTech
{
public class CallDispatcher
{
private readonly ILog log;
private readonly object _lock = new object();
private readonly List<Action> queue = new List<Action>();
private readonly AutoResetEvent autoResetEvent = new AutoResetEvent(false);
public CallDispatcher(ILog log)
{
this.log = log;
}
public void Add(Action call)
{
lock (_lock)
{
queue.Add(call);
autoResetEvent.Set();
if (queue.Count > 100)
{
log.Error("Queue overflow!");
queue.Clear();
}
}
}
public void Start()
{
Task.Run(() =>
{
while (true)
{
try
{
Worker();
}
catch (Exception ex)
{
log.Error("Exception in CallDispatcher: " + ex);
}
}
});
}
private void Worker()
{
autoResetEvent.WaitOne();
var tasks = Array.Empty<Action>();
lock (_lock)
{
tasks = queue.ToArray();
queue.Clear();
}
foreach (var task in tasks)
{
task();
}
}
}
}

View File

@ -0,0 +1,78 @@
using BiblioTech.Rewards;
using Discord;
using Logging;
using System.Threading.Tasks;
namespace BiblioTech.CodexChecking
{
public class ActiveP2pRoleRemover
{
private readonly Configuration config;
private readonly ILog log;
private readonly CheckRepo repo;
public ActiveP2pRoleRemover(Configuration config, ILog log, CheckRepo repo)
{
this.config = config;
this.log = log;
this.repo = repo;
}
public void Start()
{
if (config.ActiveP2pRoleDurationMinutes > 0)
{
Task.Run(Worker);
}
}
private void Worker()
{
var loopDelay = TimeSpan.FromMinutes(config.ActiveP2pRoleDurationMinutes) / 60;
var min = TimeSpan.FromMinutes(10.0);
if (loopDelay < min) loopDelay = min;
try
{
while (true)
{
Thread.Sleep(loopDelay);
CheckP2pRoleRemoval();
}
}
catch (Exception ex)
{
log.Error($"Exception in {nameof(ActiveP2pRoleRemover)}: {ex}");
Environment.Exit(1);
}
}
private void CheckP2pRoleRemoval()
{
var expiryMoment = DateTime.UtcNow - TimeSpan.FromMinutes(config.ActiveP2pRoleDurationMinutes);
Program.RoleDriver.IterateUsersWithRoles(
(g, u, r) => OnUserWithRole(g, u, r, expiryMoment),
Program.Config.ActiveP2pParticipantRoleId);
}
private async Task OnUserWithRole(IRoleGiver giver, IUser user, ulong roleId, DateTime expiryMoment)
{
var report = repo.GetOrCreate(user.Id);
if (report.UploadCheck.CompletedUtc > expiryMoment) return;
if (report.DownloadCheck.CompletedUtc > expiryMoment) return;
await giver.RemoveActiveP2pParticipant(user.Id);
}
private bool ShouldRemoveRole(IUser user, DateTime expiryMoment)
{
var report = repo.GetOrCreate(user.Id);
if (report.UploadCheck.CompletedUtc > expiryMoment) return false;
if (report.DownloadCheck.CompletedUtc > expiryMoment) return false;
return true;
}
}
}

View File

@ -0,0 +1,78 @@
using Newtonsoft.Json;
namespace BiblioTech.CodexChecking
{
public class CheckRepo
{
private const string modelFilename = "model.json";
private readonly Configuration config;
private readonly object _lock = new object();
private CheckRepoModel? model = null;
public CheckRepo(Configuration config)
{
this.config = config;
}
public CheckReport GetOrCreate(ulong userId)
{
lock (_lock)
{
if (model == null) LoadModel();
var existing = model.Reports.SingleOrDefault(r => r.UserId == userId);
if (existing == null)
{
var newEntry = new CheckReport
{
UserId = userId,
};
model.Reports.Add(newEntry);
SaveChanges();
return newEntry;
}
return existing;
}
}
public void SaveChanges()
{
File.WriteAllText(GetModelFilepath(), JsonConvert.SerializeObject(model, Formatting.Indented));
}
private void LoadModel()
{
if (!File.Exists(GetModelFilepath()))
{
model = new CheckRepoModel();
SaveChanges();
return;
}
model = JsonConvert.DeserializeObject<CheckRepoModel>(File.ReadAllText(GetModelFilepath()));
}
private string GetModelFilepath()
{
return Path.Combine(config.ChecksDataPath, modelFilename);
}
}
public class CheckRepoModel
{
public List<CheckReport> Reports { get; set; } = new List<CheckReport>();
}
public class CheckReport
{
public ulong UserId { get; set; }
public TransferCheck UploadCheck { get; set; } = new TransferCheck();
public TransferCheck DownloadCheck { get; set; } = new TransferCheck();
}
public class TransferCheck
{
public DateTime CompletedUtc { get; set; } = DateTime.MinValue;
public string UniqueData { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,228 @@
using CodexClient;
using FileUtils;
using Logging;
using Utils;
namespace BiblioTech.CodexChecking
{
public interface ICheckResponseHandler
{
Task CheckNotStarted();
Task NowCompleted(string checkName);
Task GiveRoleReward();
Task InvalidData();
Task CouldNotDownloadCid();
Task GiveCidToUser(string cid);
Task GiveDataFileToUser(string fileContent);
Task ToAdminChannel(string msg);
}
public class CodexTwoWayChecker
{
private readonly ILog log;
private readonly Configuration config;
private readonly CheckRepo repo;
private readonly CodexWrapper codexWrapper;
public CodexTwoWayChecker(ILog log, Configuration config, CheckRepo repo, CodexWrapper codexWrapper)
{
this.log = log;
this.config = config;
this.repo = repo;
this.codexWrapper = codexWrapper;
}
public async Task StartDownloadCheck(ICheckResponseHandler handler, ulong userId)
{
var check = repo.GetOrCreate(userId).DownloadCheck;
if (IsUniqueDataStale(check))
{
check.UniqueData = GenerateUniqueData();
repo.SaveChanges();
}
var cid = UploadData(check.UniqueData);
await handler.GiveCidToUser(cid);
}
public async Task VerifyDownloadCheck(ICheckResponseHandler handler, ulong userId, string receivedData)
{
var check = repo.GetOrCreate(userId).DownloadCheck;
if (string.IsNullOrEmpty(check.UniqueData))
{
await handler.CheckNotStarted();
return;
}
Log($"Verifying for downloadCheck: received: '{receivedData}' check: '{check.UniqueData}'");
if (string.IsNullOrEmpty(receivedData) || receivedData != check.UniqueData)
{
await handler.InvalidData();
return;
}
await CheckNowCompleted(handler, check, userId, "DownloadCheck");
}
public async Task StartUploadCheck(ICheckResponseHandler handler, ulong userId)
{
var check = repo.GetOrCreate(userId).UploadCheck;
if (IsUniqueDataStale(check))
{
check.UniqueData = GenerateUniqueData();
repo.SaveChanges();
}
await handler.GiveDataFileToUser(check.UniqueData);
}
public async Task VerifyUploadCheck(ICheckResponseHandler handler, ulong userId, string receivedCid)
{
var check = repo.GetOrCreate(userId).UploadCheck;
if (string.IsNullOrEmpty(receivedCid))
{
await handler.InvalidData();
return;
}
var manifest = GetManifest(receivedCid);
if (manifest == null)
{
await handler.CouldNotDownloadCid();
return;
}
if (IsManifestLengthCompatible(handler, check, manifest))
{
if (IsContentCorrect(handler, check, receivedCid))
{
await CheckNowCompleted(handler, check, userId, "UploadCheck");
return;
}
}
await handler.InvalidData();
}
private string GenerateUniqueData()
{
return $"{RandomBusyMessage.Get().Substring(5)}{RandomUtils.GenerateRandomString(12)}";
}
private bool IsUniqueDataStale(TransferCheck check)
{
var expiry = DateTime.UtcNow - TimeSpan.FromMinutes(10.0);
return
string.IsNullOrEmpty(check.UniqueData) ||
check.CompletedUtc < expiry;
}
private string UploadData(string uniqueData)
{
var filePath = Path.Combine(config.ChecksDataPath, Guid.NewGuid().ToString());
try
{
File.WriteAllText(filePath, uniqueData);
var file = new TrackedFile(log, filePath, "checkData");
return codexWrapper.OnCodex(node =>
{
return node.UploadFile(file).Id;
});
}
catch (Exception ex)
{
log.Error("Exception when uploading data: " + ex);
throw;
}
finally
{
if (File.Exists(filePath)) File.Delete(filePath);
}
}
private Manifest? GetManifest(string receivedCid)
{
try
{
return codexWrapper.OnCodex(node =>
{
return node.DownloadManifestOnly(new ContentId(receivedCid)).Manifest;
});
}
catch
{
return null;
}
}
private bool IsManifestLengthCompatible(ICheckResponseHandler handler, TransferCheck check, Manifest manifest)
{
var dataLength = check.UniqueData.Length;
var manifestLength = manifest.OriginalBytes.SizeInBytes;
Log($"Checking manifest length: dataLength={dataLength},manifestLength={manifestLength}");
return
manifestLength > (dataLength - 1) &&
manifestLength < (dataLength + 1);
}
private bool IsContentCorrect(ICheckResponseHandler handler, TransferCheck check, string receivedCid)
{
try
{
var content = codexWrapper.OnCodex(node =>
{
var file = node.DownloadContent(new ContentId(receivedCid));
if (file == null) return string.Empty;
try
{
return File.ReadAllText(file.Filename).Trim();
}
finally
{
if (File.Exists(file.Filename)) File.Delete(file.Filename);
}
});
Log($"Checking content: content={content},check={check.UniqueData}");
return content == check.UniqueData;
}
catch
{
return false;
}
}
private async Task CheckNowCompleted(ICheckResponseHandler handler, TransferCheck check, ulong userId, string checkName)
{
await handler.NowCompleted(checkName);
check.CompletedUtc = DateTime.UtcNow;
repo.SaveChanges();
await CheckUserForRoleRewards(handler, userId);
}
private async Task CheckUserForRoleRewards(ICheckResponseHandler handler, ulong userId)
{
var check = repo.GetOrCreate(userId);
if (check.UploadCheck.CompletedUtc != DateTime.MinValue &&
check.DownloadCheck.CompletedUtc != DateTime.MinValue)
{
await handler.GiveRoleReward();
}
}
private void Log(string msg)
{
log.Log(msg);
}
}
}

View File

@ -0,0 +1,85 @@
using CodexClient;
using IdentityModel.Client;
using Logging;
using Utils;
using WebUtils;
namespace BiblioTech.CodexChecking
{
public class CodexWrapper
{
private readonly CodexNodeFactory factory;
private readonly ILog log;
private readonly Configuration config;
private readonly object codexLock = new object();
private ICodexNode? currentCodexNode;
public CodexWrapper(ILog log, Configuration config)
{
this.log = log;
this.config = config;
var httpFactory = CreateHttpFactory();
factory = new CodexNodeFactory(log, httpFactory, dataDir: config.DataPath);
}
public void OnCodex(Action<ICodexNode> action)
{
lock (codexLock)
{
action(Get());
}
}
public T OnCodex<T>(Func<ICodexNode, T> func)
{
lock (codexLock)
{
return func(Get());
}
}
private ICodexNode Get()
{
if (currentCodexNode == null)
{
currentCodexNode = CreateCodex();
}
return currentCodexNode;
}
private ICodexNode CreateCodex()
{
var endpoint = config.CodexEndpoint;
var splitIndex = endpoint.LastIndexOf(':');
var host = endpoint.Substring(0, splitIndex);
var port = Convert.ToInt32(endpoint.Substring(splitIndex + 1));
var address = new Address(
logName: $"cdx@{host}:{port}",
host: host,
port: port
);
var instance = CodexInstance.CreateFromApiEndpoint("ac", address);
return factory.CreateCodexNode(instance);
}
private HttpFactory CreateHttpFactory()
{
if (string.IsNullOrEmpty(config.CodexEndpointAuth) || !config.CodexEndpointAuth.Contains(":"))
{
return new HttpFactory(log);
}
var tokens = config.CodexEndpointAuth.Split(':');
if (tokens.Length != 2) throw new Exception("Expected '<username>:<password>' in CodexEndpointAuth parameter.");
return new HttpFactory(log, onClientCreated: client =>
{
client.SetBasicAuthentication(tokens[0], tokens[1]);
});
}
}
}

View File

@ -1,204 +0,0 @@
using CodexClient;
using Logging;
using Utils;
namespace BiblioTech
{
public class CodexCidChecker
{
private static readonly string nl = Environment.NewLine;
private readonly Configuration config;
private readonly ILog log;
private readonly Mutex checkMutex = new Mutex();
private readonly CodexNodeFactory factory;
private ICodexNode? currentCodexNode;
public CodexCidChecker(Configuration config, ILog log)
{
this.config = config;
this.log = log;
factory = new CodexNodeFactory(log, dataDir: config.DataPath);
if (!string.IsNullOrEmpty(config.CodexEndpointAuth) && config.CodexEndpointAuth.Contains(":"))
{
throw new Exception("Todo: codexnodefactory httpfactory support basicauth!");
//var tokens = config.CodexEndpointAuth.Split(':');
//if (tokens.Length != 2) throw new Exception("Expected '<username>:<password>' in CodexEndpointAuth parameter.");
//client.SetBasicAuthentication(tokens[0], tokens[1]);
}
}
public CheckResponse PerformCheck(string cid)
{
if (string.IsNullOrEmpty(config.CodexEndpoint))
{
return new CheckResponse(false, "Codex CID checker is not (yet) available.", "");
}
try
{
checkMutex.WaitOne();
var codex = GetCodex();
var nodeCheck = CheckCodex(codex);
if (!nodeCheck) return new CheckResponse(false, "Codex node is not available. Cannot perform check.", $"Codex node at '{config.CodexEndpoint}' did not respond correctly to debug/info.");
return PerformCheck(codex, cid);
}
catch (Exception ex)
{
return new CheckResponse(false, "Internal server error", ex.ToString());
}
finally
{
checkMutex.ReleaseMutex();
}
}
private CheckResponse PerformCheck(ICodexNode codex, string cid)
{
try
{
var manifest = codex.DownloadManifestOnly(new ContentId(cid));
return SuccessMessage(manifest);
}
catch (Exception ex)
{
return UnexpectedException(ex);
}
}
#region Response formatting
private CheckResponse SuccessMessage(LocalDataset content)
{
return FormatResponse(
success: true,
title: $"Success: '{content.Cid}'",
error: "",
$"size: {content.Manifest.OriginalBytes} bytes",
$"blockSize: {content.Manifest.BlockSize} bytes",
$"protected: {content.Manifest.Protected}"
);
}
private CheckResponse UnexpectedException(Exception ex)
{
return FormatResponse(
success: false,
title: "Unexpected error",
error: ex.ToString(),
content: "Details will be sent to the bot-admin channel."
);
}
private CheckResponse UnexpectedReturnCode(string response)
{
var msg = "Unexpected return code. Response: " + response;
return FormatResponse(
success: false,
title: "Unexpected return code",
error: msg,
content: msg
);
}
private CheckResponse FailedToFetch(string response)
{
var msg = "Failed to download content. Response: " + response;
return FormatResponse(
success: false,
title: "Could not download content",
error: msg,
msg,
$"Connection trouble? See 'https://docs.codex.storage/learn/troubleshoot'"
);
}
private CheckResponse CidFormatInvalid(string response)
{
return FormatResponse(
success: false,
title: "Invalid format",
error: "",
content: "Provided CID is not formatted correctly."
);
}
private CheckResponse FormatResponse(bool success, string title, string error, params string[] content)
{
var msg = string.Join(nl,
new string[]
{
title,
"```"
}
.Concat(content)
.Concat(new string[]
{
"```"
})
) + nl + nl;
return new CheckResponse(success, msg, error);
}
#endregion
#region Codex Node API
private ICodexNode GetCodex()
{
if (currentCodexNode == null) currentCodexNode = CreateCodex();
return currentCodexNode;
}
private bool CheckCodex(ICodexNode node)
{
try
{
var info = node.GetDebugInfo();
if (info == null || string.IsNullOrEmpty(info.Id)) return false;
return true;
}
catch (Exception e)
{
log.Error(e.ToString());
return false;
}
}
private ICodexNode CreateCodex()
{
var endpoint = config.CodexEndpoint;
var splitIndex = endpoint.LastIndexOf(':');
var host = endpoint.Substring(0, splitIndex);
var port = Convert.ToInt32(endpoint.Substring(splitIndex + 1));
var address = new Address(
logName: $"cdx@{host}:{port}",
host: host,
port: port
);
var instance = CodexInstance.CreateFromApiEndpoint("ac", address);
return factory.CreateCodexNode(instance);
}
#endregion
}
public class CheckResponse
{
public CheckResponse(bool success, string message, string error)
{
Success = success;
Message = message;
Error = error;
}
public bool Success { get; }
public string Message { get; }
public string Error { get; }
}
}

View File

@ -4,6 +4,9 @@ using Discord;
using Newtonsoft.Json;
using BiblioTech.Rewards;
using Logging;
using BiblioTech.CodexChecking;
using Nethereum.Model;
using static Org.BouncyCastle.Math.EC.ECCurve;
namespace BiblioTech
{
@ -11,13 +14,15 @@ namespace BiblioTech
{
private readonly DiscordSocketClient client;
private readonly CustomReplacement replacement;
private readonly ActiveP2pRoleRemover roleRemover;
private readonly BaseCommand[] commands;
private readonly ILog log;
public CommandHandler(ILog log, DiscordSocketClient client, CustomReplacement replacement, params BaseCommand[] commands)
public CommandHandler(ILog log, DiscordSocketClient client, CustomReplacement replacement, ActiveP2pRoleRemover roleRemover, params BaseCommand[] commands)
{
this.client = client;
this.replacement = replacement;
this.roleRemover = roleRemover;
this.commands = commands;
this.log = log;
client.Ready += Client_Ready;
@ -30,10 +35,15 @@ namespace BiblioTech
Program.AdminChecker.SetGuild(guild);
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, log, replacement);
var adminChannel = GetChannel(guild, Program.Config.AdminChannelId);
if (adminChannel == null) throw new Exception("No admin message channel");
var chainEventsChannel = GetChannel(guild, Program.Config.ChainEventsChannelId);
var rewardsChannel = GetChannel(guild, Program.Config.RewardsChannelId);
Program.AdminChecker.SetAdminChannel(adminChannel);
Program.RoleDriver = new RoleDriver(client, Program.UserRepo, log, rewardsChannel);
Program.ChainActivityHandler = new ChainActivityHandler(log, Program.UserRepo);
Program.EventsSender = new ChainEventsSender(log, replacement, chainEventsChannel);
var builders = commands.Select(c =>
{
@ -65,6 +75,8 @@ namespace BiblioTech
{
log.Log($"{cmd.Name} ({cmd.Description}) [{DescribOptions(cmd.Options)}]");
}
roleRemover.Start();
}
catch (HttpException exception)
{
@ -72,9 +84,16 @@ namespace BiblioTech
log.Error(json);
throw;
}
Program.Dispatcher.Start();
log.Log("Initialized.");
}
private SocketTextChannel? GetChannel(SocketGuild guild, ulong id)
{
if (id == 0) return null;
return guild.TextChannels.SingleOrDefault(c => c.Id == id);
}
private string DescribOptions(IReadOnlyCollection<SocketApplicationCommandOption> options)
{
return string.Join(",", options.Select(DescribeOption).ToArray());

View File

@ -8,16 +8,10 @@ namespace BiblioTech.Commands
private readonly ClearUserAssociationCommand clearCommand = new ClearUserAssociationCommand();
private readonly ReportCommand reportCommand = new ReportCommand();
private readonly WhoIsCommand whoIsCommand = new WhoIsCommand();
private readonly AddSprCommand addSprCommand;
private readonly ClearSprsCommand clearSprsCommand;
private readonly GetSprCommand getSprCommand;
private readonly LogReplaceCommand logReplaceCommand;
public AdminCommand(SprCommand sprCommand, CustomReplacement replacement)
public AdminCommand(CustomReplacement replacement)
{
addSprCommand = new AddSprCommand(sprCommand);
clearSprsCommand = new ClearSprsCommand(sprCommand);
getSprCommand = new GetSprCommand(sprCommand);
logReplaceCommand = new LogReplaceCommand(replacement);
}
@ -30,9 +24,6 @@ namespace BiblioTech.Commands
clearCommand,
reportCommand,
whoIsCommand,
addSprCommand,
clearSprsCommand,
getSprCommand,
logReplaceCommand
};
@ -53,9 +44,6 @@ namespace BiblioTech.Commands
await clearCommand.CommandHandler(context);
await reportCommand.CommandHandler(context);
await whoIsCommand.CommandHandler(context);
await addSprCommand.CommandHandler(context);
await clearSprsCommand.CommandHandler(context);
await getSprCommand.CommandHandler(context);
await logReplaceCommand.CommandHandler(context);
}
@ -144,78 +132,6 @@ namespace BiblioTech.Commands
}
}
public class AddSprCommand : SubCommandOption
{
private readonly SprCommand sprCommand;
private readonly StringOption stringOption = new StringOption("spr", "Codex SPR", true);
public AddSprCommand(SprCommand sprCommand)
: base(name: "addspr",
description: "Adds a Codex SPR, to be given to users with '/boot'.")
{
this.sprCommand = sprCommand;
}
public override CommandOption[] Options => new[] { stringOption };
protected override async Task onSubCommand(CommandContext context)
{
var spr = await stringOption.Parse(context);
if (!string.IsNullOrEmpty(spr) )
{
sprCommand.Add(spr);
await context.Followup("A-OK!");
}
else
{
await context.Followup("SPR is null or empty.");
}
}
}
public class ClearSprsCommand : SubCommandOption
{
private readonly SprCommand sprCommand;
private readonly StringOption stringOption = new StringOption("areyousure", "set to 'true' if you are.", true);
public ClearSprsCommand(SprCommand sprCommand)
: base(name: "clearsprs",
description: "Clears all Codex SPRs in the bot. Users won't be able to use '/boot' till new ones are added.")
{
this.sprCommand = sprCommand;
}
public override CommandOption[] Options => new[] { stringOption };
protected override async Task onSubCommand(CommandContext context)
{
var areyousure = await stringOption.Parse(context);
if (areyousure != "true") return;
sprCommand.Clear();
await context.Followup("Cleared all SPRs.");
}
}
public class GetSprCommand : SubCommandOption
{
private readonly SprCommand sprCommand;
public GetSprCommand(SprCommand sprCommand)
: base(name: "getsprs",
description: "Shows all Codex SPRs in the bot.")
{
this.sprCommand = sprCommand;
}
protected override async Task onSubCommand(CommandContext context)
{
await context.Followup("SPRs: " + string.Join(", ", sprCommand.Get().Select(s => $"'{s}'")));
}
}
public class LogReplaceCommand : SubCommandOption
{
private readonly CustomReplacement replacement;

View File

@ -1,111 +0,0 @@
using BiblioTech.Options;
using Discord;
namespace BiblioTech.Commands
{
public class CheckCidCommand : BaseCommand
{
private readonly StringOption cidOption = new StringOption(
name: "cid",
description: "Codex Content-Identifier",
isRequired: true);
private readonly CodexCidChecker checker;
private readonly CidStorage cidStorage;
public CheckCidCommand(CodexCidChecker checker)
{
this.checker = checker;
this.cidStorage = new CidStorage(Path.Combine(Program.Config.DataPath, "valid_cids.txt"));
}
public override string Name => "check";
public override string StartingMessage => RandomBusyMessage.Get();
public override string Description => "Checks if content is available in the testnet.";
public override CommandOption[] Options => new[] { cidOption };
protected override async Task Invoke(CommandContext context)
{
var user = context.Command.User;
var cid = await cidOption.Parse(context);
if (string.IsNullOrEmpty(cid))
{
await context.Followup("Option 'cid' was not received.");
return;
}
var response = checker.PerformCheck(cid);
await Program.AdminChecker.SendInAdminChannel($"User {Mention(user)} used '/{Name}' for cid '{cid}'. Lookup-success: {response.Success}. Message: '{response.Message}' Error: '{response.Error}'");
if (response.Success)
{
await CheckAltruisticRole(context, user, cid, response.Message);
return;
}
await context.Followup(response.Message);
}
private async Task CheckAltruisticRole(CommandContext context, IUser user, string cid, string responseMessage)
{
if (cidStorage.TryAddCid(cid, user.Id))
{
if (await GiveAltruisticRole(context, user, responseMessage))
{
return;
}
}
else
{
await context.Followup($"{responseMessage}\n\nThis CID has already been used by another user. No role will be granted.");
return;
}
await context.Followup(responseMessage);
}
private async Task<bool> GiveAltruisticRole(CommandContext context, IUser user, string responseMessage)
{
try
{
await Program.RoleDriver.GiveAltruisticRole(user);
await context.Followup($"{responseMessage}\n\nCongratulations! You've been granted the Altruistic Mode role for checking a valid CID!");
return true;
}
catch (Exception ex)
{
await Program.AdminChecker.SendInAdminChannel($"Failed to grant Altruistic Mode role to user {Mention(user)}: {ex.Message}");
return false;
}
}
}
public class CidStorage
{
private readonly string filePath;
private static readonly object _lock = new object();
public CidStorage(string filePath)
{
this.filePath = filePath;
if (!File.Exists(filePath))
{
File.WriteAllText(filePath, string.Empty);
}
}
public bool TryAddCid(string cid, ulong userId)
{
lock (_lock)
{
var existingEntries = File.ReadAllLines(filePath);
if (existingEntries.Any(line => line.Split(',')[0] == cid))
{
return false;
}
File.AppendAllLines(filePath, new[] { $"{cid},{userId}" });
return true;
}
}
}
}

View File

@ -0,0 +1,58 @@
using BiblioTech.CodexChecking;
using BiblioTech.Options;
namespace BiblioTech.Commands
{
public class CheckDownloadCommand : BaseCommand
{
private readonly CodexTwoWayChecker checker;
private readonly StringOption contentOption = new StringOption(
name: "content",
description: "Content of the downloaded file",
isRequired: false);
public CheckDownloadCommand(CodexTwoWayChecker checker)
{
this.checker = checker;
}
public override string Name => "checkdownload";
public override string StartingMessage => RandomBusyMessage.Get();
public override string Description => "Checks the download connectivity of your Codex node.";
public override CommandOption[] Options => [contentOption];
protected override async Task Invoke(CommandContext context)
{
var user = context.Command.User;
var content = await contentOption.Parse(context);
try
{
var handler = new CheckResponseHandler(context, user);
if (string.IsNullOrEmpty(content))
{
await checker.StartDownloadCheck(handler, user.Id);
}
else
{
if (content.Length > 1024)
{
await context.Followup("Provided content is too long!");
return;
}
await checker.VerifyDownloadCheck(handler, user.Id, content);
}
}
catch (Exception ex)
{
await RespondWithError(context, ex);
}
}
private async Task RespondWithError(CommandContext context, Exception ex)
{
await Program.AdminChecker.SendInAdminChannel("Exception during CheckDownloadCommand: " + ex);
await context.Followup("I'm sorry to report something has gone wrong in an unexpected way. Error details are already posted in the admin channel.");
}
}
}

View File

@ -0,0 +1,102 @@
using System.Linq;
using BiblioTech.CodexChecking;
using BiblioTech.Options;
using Discord;
namespace BiblioTech.Commands
{
public class CheckResponseHandler : ICheckResponseHandler
{
private CommandContext context;
private readonly IUser user;
public CheckResponseHandler(CommandContext context, IUser user)
{
this.context = context;
this.user = user;
}
public async Task CheckNotStarted()
{
await context.Followup("Run this command without any arguments first, to begin the check process.");
}
public async Task CouldNotDownloadCid()
{
await context.Followup("Could not download the CID.");
}
public async Task GiveCidToUser(string cid)
{
await context.Followup(
FormatCatchyMessage("[💾] Please download this CID using your Codex node.",
$"👉 `{cid}`.",
"👉 Then provide the *content of the downloaded file* as argument to this command."));
}
public async Task GiveDataFileToUser(string fileContent)
{
await context.SendFile(fileContent,
FormatCatchyMessage("[💿] Please download the attached file.",
"👉 Upload it to your Codex node.",
"👉 Then provide the *CID* as argument to this command."));
}
private string FormatCatchyMessage(string title, params string[] content)
{
var entries = new List<string>();
entries.Add(title);
entries.Add("```");
entries.AddRange(content);
entries.Add("```");
return string.Join(Environment.NewLine, entries.ToArray());
}
public async Task GiveRoleReward()
{
try
{
await Program.RoleDriver.RunRoleGiver(async r =>
{
await r.GiveAltruisticRole(user.Id);
await r.GiveActiveP2pParticipant(user.Id);
});
await context.Followup($"Congratulations! You've been granted the Altruistic Mode role!");
}
catch (Exception ex)
{
await Program.AdminChecker.SendInAdminChannel($"Failed to grant Altruistic Mode role to user <@{user.Id}>: {ex.Message}");
}
}
public async Task InvalidData()
{
await context.Followup("The received data didn't match. Check has failed.");
}
public async Task NowCompleted(string checkName)
{
// check if eth address is known for user.
var data = Program.UserRepo.GetUser(user);
if (data.CurrentAddress == null)
{
await context.Followup($"Successfully completed the check!{Environment.NewLine}" +
$"You haven't yet set your ethereum address. Consider using '/set' to set it.{Environment.NewLine}" +
$"(You can find your address in the 'eth.address' file of your Codex node.)");
await Program.AdminChecker.SendInAdminChannel($"User <@{user.Id}> has completed check: {checkName}" +
$" - EthAddress not set for user. User was reminded.");
}
else
{
await context.Followup("Successfully completed the check!");
await Program.AdminChecker.SendInAdminChannel($"User <@{user.Id}> has completed check: {checkName}");
}
}
public async Task ToAdminChannel(string msg)
{
await Program.AdminChecker.SendInAdminChannel(msg);
}
}
}

View File

@ -0,0 +1,53 @@
using BiblioTech.CodexChecking;
using BiblioTech.Options;
namespace BiblioTech.Commands
{
public class CheckUploadCommand : BaseCommand
{
private readonly CodexTwoWayChecker checker;
private readonly StringOption cidOption = new StringOption(
name: "cid",
description: "Codex Content-Identifier",
isRequired: false);
public CheckUploadCommand(CodexTwoWayChecker checker)
{
this.checker = checker;
}
public override string Name => "checkupload";
public override string StartingMessage => RandomBusyMessage.Get();
public override string Description => "Checks the upload connectivity of your Codex node.";
public override CommandOption[] Options => [cidOption];
protected override async Task Invoke(CommandContext context)
{
var user = context.Command.User;
var cid = await cidOption.Parse(context);
try
{
var handler = new CheckResponseHandler(context, user);
if (string.IsNullOrEmpty(cid))
{
await checker.StartUploadCheck(handler, user.Id);
}
else
{
await checker.VerifyUploadCheck(handler, user.Id, cid);
}
}
catch (Exception ex)
{
await RespondWithError(context, ex);
}
}
private async Task RespondWithError(CommandContext context, Exception ex)
{
await Program.AdminChecker.SendInAdminChannel("Exception during CheckUploadCommand: " + ex);
await context.Followup("I'm sorry to report something has gone wrong in an unexpected way. Error details are already posted in the admin channel.");
}
}
}

View File

@ -1,48 +0,0 @@
using BiblioTech.Options;
namespace BiblioTech.Commands
{
public class SprCommand : BaseCommand
{
private readonly Random random = new Random();
private readonly List<string> knownSprs = new List<string>();
public override string Name => "boot";
public override string StartingMessage => RandomBusyMessage.Get();
public override string Description => "Gets an SPR. (Signed peer record, used for bootstrapping.)";
protected override async Task Invoke(CommandContext context)
{
await ReplyWithRandomSpr(context);
}
public void Add(string spr)
{
if (knownSprs.Contains(spr)) return;
knownSprs.Add(spr);
}
public void Clear()
{
knownSprs.Clear();
}
public string[] Get()
{
return knownSprs.ToArray();
}
private async Task ReplyWithRandomSpr(CommandContext context)
{
if (!knownSprs.Any())
{
await context.Followup("I'm sorry, no SPRs are available... :c");
return;
}
var i = random.Next(0, knownSprs.Count);
var spr = knownSprs[i];
await context.Followup($"Your SPR: `{spr}`");
}
}
}

View File

@ -69,8 +69,8 @@ namespace BiblioTech.Commands
{
await context.Followup(new string[]
{
"Done! Thank you for joining the test net!",
"By default, the bot will @-mention you with test-net related notifications.",
"Done! Thank you for joining!",
"By default, the bot will @-mention you with discord role notifications.",
$"You can enable/disable this behavior with the '/{notifyCommand.Name}' command."
});

View File

@ -26,9 +26,6 @@ namespace BiblioTech
[Uniform("chain-events-channel-id", "cc", "CHAINEVENTSCHANNELID", false, "ID of the Discord server channel where chain events will be posted.")]
public ulong ChainEventsChannelId { get; set; }
[Uniform("altruistic-role-id", "ar", "ALTRUISTICROLE", true, "ID of the Discord server role for Altruistic Mode.")]
public ulong AltruisticRoleId { get; set; }
[Uniform("reward-api-port", "rp", "REWARDAPIPORT", true, "TCP listen port for the reward API.")]
public int RewardApiPort { get; set; } = 31080;
@ -47,8 +44,40 @@ namespace BiblioTech
[Uniform("codex-endpoint-auth", "cea", "CODEXENDPOINTAUTH", false, "Codex endpoint basic auth. Colon separated username and password. (default: empty, no auth used.)")]
public string CodexEndpointAuth { get; set; } = "";
#region Role Rewards
/// <summary>
/// Awarded when both checkupload and checkdownload have been completed.
/// </summary>
[Uniform("altruistic-role-id", "ar", "ALTRUISTICROLE", true, "ID of the Discord server role for Altruistic Mode.")]
public ulong AltruisticRoleId { get; set; }
/// <summary>
/// Awarded as long as either checkupload or checkdownload were completed within the last ActiveP2pRoleDuration minutes.
/// </summary>
[Uniform("active-p2p-role-id", "apri", "ACTIVEP2PROLEID", false, "ID of discord server role for active p2p participants.")]
public ulong ActiveP2pParticipantRoleId { get; set; }
[Uniform("active-p2p-role-duration", "aprd", "ACTIVEP2PROLEDURATION", false, "Duration in minutes for the active p2p participant role from the last successful check command.")]
public int ActiveP2pRoleDurationMinutes { get; set; }
/// <summary>
/// Awarded as long as the user is hosting at least 1 slot.
/// </summary>
[Uniform("active-host-role-id", "ahri", "ACTIVEHOSTROLEID", false, "Id of discord server role for active slot hosters.")]
public ulong ActiveHostRoleId { get; set; }
/// <summary>
/// Awarded as long as the user has at least 1 active storage purchase contract.
/// </summary>
[Uniform("active-client-role-id", "acri", "ACTIVECLIENTROLEID", false, "Id of discord server role for users with at least 1 active purchase contract.")]
public ulong ActiveClientRoleId { get; set; }
#endregion
public string EndpointsPath => Path.Combine(DataPath, "endpoints");
public string UserDataPath => Path.Combine(DataPath, "users");
public string ChecksDataPath => Path.Combine(DataPath, "checks");
public string LogPath => Path.Combine(DataPath, "logs");
public bool DebugNoDiscord => NoDiscord == 1;
}

View File

@ -15,18 +15,72 @@ namespace BiblioTech
this.log = log;
}
public async Task GiveAltruisticRole(IUser user)
public async Task RunRoleGiver(Func<IRoleGiver, Task> action)
{
await Task.CompletedTask;
log.Log($"Give altruistic role to {user.Id}");
await action(new LoggingRoleGiver(log));
}
public async Task GiveRewards(GiveRewardsCommand rewards)
public async Task IterateUsersWithRoles(Func<IRoleGiver, IUser, ulong, Task> onUserWithRole, params ulong[] rolesToIterate)
{
await Task.CompletedTask;
}
log.Log(JsonConvert.SerializeObject(rewards, Formatting.None));
public async Task IterateUsersWithRoles(Func<IRoleGiver, IUser, ulong, Task> onUserWithRole, Func<IRoleGiver, Task> whenDone, params ulong[] rolesToIterate)
{
await Task.CompletedTask;
}
private class LoggingRoleGiver : IRoleGiver
{
private readonly ILog log;
public LoggingRoleGiver(ILog log)
{
this.log = log;
}
public async Task GiveActiveClient(ulong userId)
{
log.Log($"Giving ActiveClient role to " + userId);
await Task.CompletedTask;
}
public async Task GiveActiveHost(ulong userId)
{
log.Log($"Giving ActiveHost role to " + userId);
await Task.CompletedTask;
}
public async Task GiveActiveP2pParticipant(ulong userId)
{
log.Log($"Giving ActiveP2p role to " + userId);
await Task.CompletedTask;
}
public async Task RemoveActiveP2pParticipant(ulong userId)
{
log.Log($"Removing ActiveP2p role from " + userId);
await Task.CompletedTask;
}
public async Task GiveAltruisticRole(ulong userId)
{
log.Log($"Giving Altruistic role to " + userId);
await Task.CompletedTask;
}
public async Task RemoveActiveClient(ulong userId)
{
log.Log($"Removing ActiveClient role from " + userId);
await Task.CompletedTask;
}
public async Task RemoveActiveHost(ulong userId)
{
log.Log($"Removing ActiveHost role from " + userId);
await Task.CompletedTask;
}
}
}
}

View File

@ -49,6 +49,23 @@ namespace BiblioTech.Options
}
}
public async Task SendFile(string fileContent, string message)
{
if (fileContent.Length < 1) throw new Exception("File content is empty.");
var filename = Guid.NewGuid().ToString() + ".tmp";
File.WriteAllText(filename, fileContent);
await Command.FollowupWithFileAsync(filename, "Codex_UploadCheckFile.txt", text: message, ephemeral: true);
// Detached task for cleaning up the stream resources.
_ = Task.Run(() =>
{
Thread.Sleep(TimeSpan.FromMinutes(2));
File.Delete(filename);
});
}
private string FormatChunk(string[] chunk)
{
return string.Join(Environment.NewLine, chunk);

View File

@ -12,11 +12,12 @@ namespace BiblioTech.Options
public async Task<string?> Parse(CommandContext context)
{
var strData = context.Options.SingleOrDefault(o => o.Name == Name);
if (strData == null)
if (strData == null && IsRequired)
{
await context.Followup("String option not received.");
return null;
}
if (strData == null) return null;
return strData.Value as string;
}
}

View File

@ -1,9 +1,13 @@
using ArgsUniform;
using BiblioTech.CodexChecking;
using BiblioTech.Commands;
using BiblioTech.Rewards;
using Discord;
using Discord.WebSocket;
using DiscordRewards;
using Logging;
using Nethereum.Model;
using Newtonsoft.Json;
namespace BiblioTech
{
@ -12,16 +16,17 @@ namespace BiblioTech
private DiscordSocketClient client = null!;
private CustomReplacement replacement = null!;
public static CallDispatcher Dispatcher { get; private set; } = null!;
public static Configuration Config { get; private set; } = null!;
public static UserRepo UserRepo { get; } = new UserRepo();
public static AdminChecker AdminChecker { get; private set; } = null!;
public static IDiscordRoleDriver RoleDriver { get; set; } = null!;
public static ChainActivityHandler ChainActivityHandler { get; set; } = null!;
public static ChainEventsSender EventsSender { get; set; } = null!;
public static ILog Log { get; private set; } = null!;
public static Task Main(string[] args)
{
Log = new ConsoleLog();
var uniformArgs = new ArgsUniform<Configuration>(PrintHelp, args);
Config = uniformArgs.Parse();
@ -30,9 +35,12 @@ namespace BiblioTech
new ConsoleLog()
);
Dispatcher = new CallDispatcher(Log);
EnsurePath(Config.DataPath);
EnsurePath(Config.UserDataPath);
EnsurePath(Config.EndpointsPath);
EnsurePath(Config.ChecksDataPath);
return new Program().MainAsync(args);
}
@ -80,18 +88,20 @@ namespace BiblioTech
client = new DiscordSocketClient();
client.Log += ClientLog;
var checker = new CodexCidChecker(Config, Log);
var checkRepo = new CheckRepo(Config);
var codexWrapper = new CodexWrapper(Log, Config);
var checker = new CodexTwoWayChecker(Log, Config, checkRepo, codexWrapper);
var notifyCommand = new NotifyCommand();
var associateCommand = new UserAssociateCommand(notifyCommand);
var sprCommand = new SprCommand();
var handler = new CommandHandler(Log, client, replacement,
var roleRemover = new ActiveP2pRoleRemover(Config, Log, checkRepo);
var handler = new CommandHandler(Log, client, replacement, roleRemover,
new GetBalanceCommand(associateCommand),
new MintCommand(associateCommand),
sprCommand,
associateCommand,
notifyCommand,
new CheckCidCommand(checker),
new AdminCommand(sprCommand, replacement)
new CheckUploadCommand(checker),
new CheckDownloadCommand(checker),
new AdminCommand(replacement)
);
await client.LoginAsync(TokenType.Bot, Config.ApplicationToken);

View File

@ -14,7 +14,9 @@
"Analyzing the wavelengths...",
"Charging the flux-capacitor...",
"Jumping to hyperspace...",
"Computing the ultimate answer..."
"Computing the ultimate answer...",
"Turning it off and on again...",
"Compiling from sources..."
};
public static string Get()

View File

@ -0,0 +1,134 @@
using Discord;
using DiscordRewards;
using Logging;
namespace BiblioTech.Rewards
{
public class ChainActivityHandler
{
private readonly ILog log;
private readonly UserRepo repo;
private ActiveUserIds? previousIds = null;
public ChainActivityHandler(ILog log, UserRepo repo)
{
this.log = log;
this.repo = repo;
}
public async Task ProcessChainActivity(ActiveChainAddresses activeChainAddresses)
{
var activeUserIds = ConvertToUserIds(activeChainAddresses);
if (!HasChanged(activeUserIds)) return;
await GiveAndRemoveRoles(activeUserIds);
}
private async Task GiveAndRemoveRoles(ActiveUserIds activeUserIds)
{
await Program.RoleDriver.IterateUsersWithRoles(
(g, u, r) => OnUserWithRole(g, u, r, activeUserIds),
whenDone: g => GiveRolesToRemaining(g, activeUserIds),
Program.Config.ActiveClientRoleId,
Program.Config.ActiveHostRoleId);
}
private async Task OnUserWithRole(IRoleGiver giver, IUser user, ulong roleId, ActiveUserIds activeIds)
{
if (roleId == Program.Config.ActiveClientRoleId)
{
await CheckUserWithRole(user, activeIds.Clients, giver.RemoveActiveClient);
}
else if (roleId == Program.Config.ActiveHostRoleId)
{
await CheckUserWithRole(user, activeIds.Hosts, giver.RemoveActiveHost);
}
else
{
throw new Exception("Unknown roleId received!");
}
}
private async Task CheckUserWithRole(IUser user, List<ulong> activeUsers, Func<ulong, Task> removeActiveRole)
{
if (ShouldUserHaveRole(user, activeUsers))
{
activeUsers.Remove(user.Id);
}
else
{
await removeActiveRole(user.Id);
}
}
private bool ShouldUserHaveRole(IUser user, List<ulong> activeUsers)
{
return activeUsers.Any(id => id == user.Id);
}
private async Task GiveRolesToRemaining(IRoleGiver giver, ActiveUserIds ids)
{
foreach (var client in ids.Clients) await giver.GiveActiveClient(client);
foreach (var host in ids.Hosts) await giver.GiveActiveHost(host);
}
private bool HasChanged(ActiveUserIds activeUserIds)
{
if (previousIds == null)
{
previousIds = activeUserIds;
return true;
}
if (!IsEquivalent(previousIds.Hosts, activeUserIds.Hosts)) return true;
if (!IsEquivalent(previousIds.Clients, activeUserIds.Clients)) return true;
return false;
}
private static bool IsEquivalent(IEnumerable<ulong> a, IEnumerable<ulong> b)
{
return a.SequenceEqual(b);
}
private ActiveUserIds ConvertToUserIds(ActiveChainAddresses activeChainAddresses)
{
return new ActiveUserIds
(
hosts: Map(activeChainAddresses.Hosts),
clients: Map(activeChainAddresses.Clients)
);
}
private ulong[] Map(string[] ethAddresses)
{
var result = new List<ulong>();
foreach (var ethAddress in ethAddresses)
{
var userMaybe = repo.GetUserDataForAddressMaybe(new Utils.EthAddress(ethAddress));
if (userMaybe != null)
{
result.Add(userMaybe.DiscordId);
}
}
return result.Order().ToArray();
}
private void Log(string msg)
{
log.Log(msg);
}
private class ActiveUserIds
{
public ActiveUserIds(IEnumerable<ulong> hosts, IEnumerable<ulong> clients)
{
Hosts = hosts.ToList();
Clients = clients.ToList();
}
public List<ulong> Hosts { get; }
public List<ulong> Clients { get; }
}
}
}

View File

@ -28,14 +28,14 @@ namespace BiblioTech.Rewards
public void Add(string from, string to)
{
if (replacements.ContainsKey(from))
AddOrUpdate(from, to);
var lower = from.ToLowerInvariant();
if (lower != from)
{
replacements[from] = to;
}
else
{
replacements.Add(from, to);
AddOrUpdate(lower, to);
}
Save();
}
@ -55,6 +55,18 @@ namespace BiblioTech.Rewards
return result;
}
private void AddOrUpdate(string from, string to)
{
if (replacements.ContainsKey(from))
{
replacements[from] = to;
}
else
{
replacements.Add(from, to);
}
}
private void Save()
{
ReplaceJson[] replaces = replacements.Select(pair =>

View File

@ -1,102 +0,0 @@
using Discord.WebSocket;
using Discord;
using DiscordRewards;
namespace BiblioTech.Rewards
{
public class RewardContext
{
private readonly Dictionary<ulong, IGuildUser> users;
private readonly Dictionary<ulong, RoleReward> roles;
private readonly SocketTextChannel? rewardsChannel;
public RewardContext(Dictionary<ulong, IGuildUser> users, Dictionary<ulong, RoleReward> roles, SocketTextChannel? rewardsChannel)
{
this.users = users;
this.roles = roles;
this.rewardsChannel = rewardsChannel;
}
public async Task ProcessGiveRewardsCommand(UserReward[] rewards)
{
foreach (var rewardCommand in rewards)
{
if (roles.ContainsKey(rewardCommand.RewardCommand.RewardId))
{
var role = roles[rewardCommand.RewardCommand.RewardId];
await ProcessRewardCommand(role, rewardCommand);
}
else
{
Program.Log.Error($"RoleID not found on guild: {rewardCommand.RewardCommand.RewardId}");
}
}
}
private async Task ProcessRewardCommand(RoleReward role, UserReward reward)
{
foreach (var user in reward.Users)
{
await GiveReward(role, user);
}
}
private async Task GiveReward(RoleReward role, UserData user)
{
if (!users.ContainsKey(user.DiscordId))
{
Program.Log.Log($"User by id '{user.DiscordId}' not found.");
return;
}
var guildUser = users[user.DiscordId];
var alreadyHas = guildUser.RoleIds.ToArray();
var logMessage = $"Giving reward '{role.SocketRole.Id}' to user '{user.DiscordId}'({user.Name})[" +
$"alreadyHas:{string.Join(",", alreadyHas.Select(a => a.ToString()))}]: ";
if (alreadyHas.Any(r => r == role.Reward.RoleId))
{
logMessage += "Already has role";
Program.Log.Log(logMessage);
return;
}
await GiveRole(guildUser, role.SocketRole);
await SendNotification(role, user, guildUser);
await Task.Delay(1000);
logMessage += "Role given. Notification sent.";
Program.Log.Log(logMessage);
}
private async Task GiveRole(IGuildUser user, SocketRole role)
{
try
{
Program.Log.Log($"Giving role {role.Name}={role.Id} to user {user.DisplayName}");
await user.AddRoleAsync(role);
}
catch (Exception ex)
{
Program.Log.Error($"Failed to give role '{role.Name}' to user '{user.DisplayName}': {ex}");
}
}
private async Task SendNotification(RoleReward reward, UserData userData, IGuildUser user)
{
try
{
if (userData.NotificationsEnabled && rewardsChannel != null)
{
var msg = reward.Reward.Message.Replace(RewardConfig.UsernameTag, $"<@{user.Id}>");
await rewardsChannel.SendMessageAsync(msg);
}
}
catch (Exception ex)
{
Program.Log.Error($"Failed to notify user '{user.DisplayName}' about role '{reward.SocketRole.Name}': {ex}");
}
}
}
}

View File

@ -4,10 +4,26 @@ using Microsoft.AspNetCore.Mvc;
namespace BiblioTech.Rewards
{
/// <summary>
/// We like callbacks in this interface because we're trying to batch role-modifying operations,
/// So that we're not poking the server lots of times very quickly.
/// </summary>
public interface IDiscordRoleDriver
{
Task GiveRewards(GiveRewardsCommand rewards);
Task GiveAltruisticRole(IUser user);
Task RunRoleGiver(Func<IRoleGiver, Task> action);
Task IterateUsersWithRoles(Func<IRoleGiver, IUser, ulong, Task> onUserWithRole, params ulong[] rolesToIterate);
Task IterateUsersWithRoles(Func<IRoleGiver, IUser, ulong, Task> onUserWithRole, Func<IRoleGiver, Task> whenDone, params ulong[] rolesToIterate);
}
public interface IRoleGiver
{
Task GiveAltruisticRole(ulong userId);
Task GiveActiveP2pParticipant(ulong userId);
Task RemoveActiveP2pParticipant(ulong userId);
Task GiveActiveHost(ulong userId);
Task RemoveActiveHost(ulong userId);
Task GiveActiveClient(ulong userId);
Task RemoveActiveClient(ulong userId);
}
[Route("api/[controller]")]
@ -21,16 +37,19 @@ namespace BiblioTech.Rewards
}
[HttpPost]
public async Task<string> Give(GiveRewardsCommand cmd)
public async Task<string> Give(EventsAndErrors cmd)
{
try
Program.Dispatcher.Add(() =>
{
await Program.RoleDriver.GiveRewards(cmd);
}
catch (Exception ex)
Program.ChainActivityHandler.ProcessChainActivity(cmd.ActiveChainAddresses).Wait();
});
Program.Dispatcher.Add(() =>
{
Program.Log.Error("Exception: " + ex);
}
Program.EventsSender.ProcessChainEvents(cmd.EventsOverview, cmd.Errors).Wait();
});
await Task.CompletedTask;
return "OK";
}
}

View File

@ -1,6 +1,7 @@
using Discord;
using Discord.WebSocket;
using DiscordRewards;
using k8s.KubeConfigModels;
using Logging;
using Newtonsoft.Json;
using Utils;
@ -10,145 +11,52 @@ namespace BiblioTech.Rewards
public class RoleDriver : IDiscordRoleDriver
{
private readonly DiscordSocketClient client;
private readonly UserRepo userRepo;
private readonly ILog log;
private readonly SocketTextChannel? rewardsChannel;
private readonly ChainEventsSender eventsSender;
private readonly RewardRepo repo = new RewardRepo();
public RoleDriver(DiscordSocketClient client, ILog log, CustomReplacement replacement)
public RoleDriver(DiscordSocketClient client, UserRepo userRepo, ILog log, SocketTextChannel? rewardsChannel)
{
this.client = client;
this.userRepo = userRepo;
this.log = log;
rewardsChannel = GetChannel(Program.Config.RewardsChannelId);
eventsSender = new ChainEventsSender(log, replacement, GetChannel(Program.Config.ChainEventsChannelId));
this.rewardsChannel = rewardsChannel;
}
public async Task GiveRewards(GiveRewardsCommand rewards)
public async Task RunRoleGiver(Func<IRoleGiver, Task> action)
{
log.Log($"Processing rewards command: '{JsonConvert.SerializeObject(rewards)}'");
var context = OpenRoleModifyContext();
var mapper = new RoleMapper(context);
await action(mapper);
}
if (rewards.Rewards.Any())
public async Task IterateUsersWithRoles(Func<IRoleGiver, IUser, ulong, Task> onUserWithRole, params ulong[] rolesToIterate)
{
await IterateUsersWithRoles(onUserWithRole, g => Task.CompletedTask, rolesToIterate);
}
public async Task IterateUsersWithRoles(Func<IRoleGiver, IUser, ulong, Task> onUserWithRole, Func<IRoleGiver, Task> whenDone, params ulong[] rolesToIterate)
{
var context = OpenRoleModifyContext();
var mapper = new RoleMapper(context);
foreach (var user in context.Users)
{
await ProcessRewards(rewards);
}
await eventsSender.ProcessChainEvents(rewards.EventsOverview, rewards.Errors);
}
public async Task GiveAltruisticRole(IUser user)
{
var guild = GetGuild();
var role = guild.Roles.SingleOrDefault(r => r.Id == Program.Config.AltruisticRoleId);
if (role == null) return;
var guildUser = guild.Users.SingleOrDefault(u => u.Id == user.Id);
if (guildUser == null) return;
await guildUser.AddRoleAsync(role);
}
private async Task ProcessRewards(GiveRewardsCommand rewards)
{
try
{
var guild = GetGuild();
// We load all role and user information first,
// so we don't ask the server for the same info multiple times.
var context = new RewardContext(
await LoadAllUsers(guild),
LookUpAllRoles(guild, rewards),
rewardsChannel);
await context.ProcessGiveRewardsCommand(LookUpUsers(rewards));
}
catch (Exception ex)
{
log.Error("Failed to process rewards: " + ex);
}
}
private SocketTextChannel? GetChannel(ulong id)
{
if (id == 0) return null;
return GetGuild().TextChannels.SingleOrDefault(c => c.Id == id);
}
private async Task<Dictionary<ulong, IGuildUser>> LoadAllUsers(SocketGuild guild)
{
log.Log("Loading all users..");
var result = new Dictionary<ulong, IGuildUser>();
var users = guild.GetUsersAsync();
await foreach (var ulist in users)
{
foreach (var u in ulist)
foreach (var role in rolesToIterate)
{
result.Add(u.Id, u);
//var roleIds = string.Join(",", u.RoleIds.Select(r => r.ToString()).ToArray());
//log.Log($" > {u.Id}({u.DisplayName}) has [{roleIds}]");
}
}
return result;
}
private Dictionary<ulong, RoleReward> LookUpAllRoles(SocketGuild guild, GiveRewardsCommand rewards)
{
var result = new Dictionary<ulong, RoleReward>();
foreach (var r in rewards.Rewards)
{
if (!result.ContainsKey(r.RewardId))
{
var rewardConfig = repo.Rewards.SingleOrDefault(rr => rr.RoleId == r.RewardId);
if (rewardConfig == null)
if (user.RoleIds.Contains(role))
{
log.Log($"No Reward is configured for id '{r.RewardId}'.");
}
else
{
var socketRole = guild.GetRole(r.RewardId);
if (socketRole == null)
{
log.Log($"Guild Role by id '{r.RewardId}' not found.");
}
else
{
result.Add(r.RewardId, new RoleReward(socketRole, rewardConfig));
}
await onUserWithRole(mapper, user, role);
}
}
}
return result;
await whenDone(mapper);
}
private UserReward[] LookUpUsers(GiveRewardsCommand rewards)
private RoleModifyContext OpenRoleModifyContext()
{
return rewards.Rewards.Select(LookUpUserData).ToArray();
}
private UserReward LookUpUserData(RewardUsersCommand command)
{
return new UserReward(command,
command.UserAddresses
.Select(LookUpUserDataForAddress)
.Where(d => d != null)
.Cast<UserData>()
.ToArray());
}
private UserData? LookUpUserDataForAddress(string address)
{
try
{
var userData = Program.UserRepo.GetUserDataForAddress(new 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)
{
log.Error("Error during UserData lookup: " + ex);
return null;
}
var context = new RoleModifyContext(GetGuild(), userRepo, log, rewardsChannel);
context.Initialize();
return context;
}
private SocketGuild GetGuild()
@ -163,27 +71,48 @@ namespace BiblioTech.Rewards
}
}
public class RoleReward
public class RoleMapper : IRoleGiver
{
public RoleReward(SocketRole socketRole, RewardConfig reward)
private readonly RoleModifyContext context;
public RoleMapper(RoleModifyContext context)
{
SocketRole = socketRole;
Reward = reward;
this.context = context;
}
public SocketRole SocketRole { get; }
public RewardConfig Reward { get; }
}
public class UserReward
{
public UserReward(RewardUsersCommand rewardCommand, UserData[] users)
public async Task GiveActiveClient(ulong userId)
{
RewardCommand = rewardCommand;
Users = users;
await context.GiveRole(userId, Program.Config.ActiveClientRoleId);
}
public RewardUsersCommand RewardCommand { get; }
public UserData[] Users { get; }
public async Task GiveActiveHost(ulong userId)
{
await context.GiveRole(userId, Program.Config.ActiveHostRoleId);
}
public async Task GiveActiveP2pParticipant(ulong userId)
{
await context.GiveRole(userId, Program.Config.ActiveP2pParticipantRoleId);
}
public async Task RemoveActiveP2pParticipant(ulong userId)
{
await context.RemoveRole(userId, Program.Config.ActiveP2pParticipantRoleId);
}
public async Task GiveAltruisticRole(ulong userId)
{
await context.GiveRole(userId, Program.Config.AltruisticRoleId);
}
public async Task RemoveActiveClient(ulong userId)
{
await context.RemoveRole(userId, Program.Config.ActiveClientRoleId);
}
public async Task RemoveActiveHost(ulong userId)
{
await context.RemoveRole(userId, Program.Config.ActiveHostRoleId);
}
}
}

View File

@ -0,0 +1,135 @@
using Discord.WebSocket;
using Discord;
using DiscordRewards;
using Nethereum.Model;
using Logging;
namespace BiblioTech.Rewards
{
public class RoleModifyContext
{
private Dictionary<ulong, IGuildUser> users = new();
private Dictionary<ulong, SocketRole> roles = new();
private DateTime lastLoad = DateTime.MinValue;
private readonly object _lock = new object();
private readonly SocketGuild guild;
private readonly UserRepo userRepo;
private readonly ILog log;
private readonly SocketTextChannel? rewardsChannel;
public RoleModifyContext(SocketGuild guild, UserRepo userRepo, ILog log, SocketTextChannel? rewardsChannel)
{
this.guild = guild;
this.userRepo = userRepo;
this.log = log;
this.rewardsChannel = rewardsChannel;
}
public void Initialize()
{
lock (_lock)
{
var span = DateTime.UtcNow - lastLoad;
if (span > TimeSpan.FromMinutes(10))
{
lastLoad = DateTime.UtcNow;
log.Log("Loading all users and roles...");
var task = LoadAllUsers(guild);
task.Wait();
this.users = task.Result;
this.roles = LoadAllRoles(guild);
}
}
}
public IGuildUser[] Users => users.Values.ToArray();
public async Task GiveRole(ulong userId, ulong roleId)
{
Log($"Giving role {roleId} to user {userId}");
var role = GetRole(roleId);
var guildUser = GetUser(userId);
if (role == null) return;
if (guildUser == null) return;
await guildUser.AddRoleAsync(role);
await Program.AdminChecker.SendInAdminChannel($"Added role '{role.Name}' for user <@{userId}>.");
await SendNotification(guildUser, role);
}
public async Task RemoveRole(ulong userId, ulong roleId)
{
Log($"Removing role {roleId} from user {userId}");
var role = GetRole(roleId);
var guildUser = GetUser(userId);
if (role == null) return;
if (guildUser == null) return;
await guildUser.RemoveRoleAsync(role);
await Program.AdminChecker.SendInAdminChannel($"Removed role '{role.Name}' for user <@{userId}>.");
}
private SocketRole? GetRole(ulong roleId)
{
if (roles.ContainsKey(roleId)) return roles[roleId];
return null;
}
private IGuildUser? GetUser(ulong userId)
{
if (users.ContainsKey(userId)) return users[userId];
return null;
}
private void Log(string msg)
{
log.Log(msg);
}
private async Task<Dictionary<ulong, IGuildUser>> LoadAllUsers(SocketGuild guild)
{
var result = new Dictionary<ulong, IGuildUser>();
var users = guild.GetUsersAsync();
await foreach (var ulist in users)
{
foreach (var u in ulist)
{
result.Add(u.Id, u);
}
}
return result;
}
private Dictionary<ulong, SocketRole> LoadAllRoles(SocketGuild guild)
{
var result = new Dictionary<ulong, SocketRole>();
var roles = guild.Roles.ToArray();
foreach (var role in roles)
{
result.Add(role.Id, role);
}
return result;
}
private async Task SendNotification(IGuildUser user, SocketRole role)
{
try
{
var userData = userRepo.GetUser(user);
if (userData == null) return;
if (userData.NotificationsEnabled && rewardsChannel != null)
{
var msg = $"<@{user.Id}> has received '{role.Name}'.";
await rewardsChannel.SendMessageAsync(msg);
}
}
catch (Exception ex)
{
log.Error($"Failed to notify user '{user.DisplayName}' about role '{role.Name}': {ex}");
}
}
}
}

View File

@ -41,6 +41,12 @@ namespace BiblioTech
return cache.Values.ToArray();
}
public UserData GetUser(IUser user)
{
if (cache.Count == 0) LoadAllUserData();
return GetOrCreate(user);
}
public void AddMintEventForUser(IUser user, EthAddress usedAddress, Transaction<Ether>? eth, Transaction<TestToken>? tokens)
{
lock (repoLock)
@ -68,10 +74,10 @@ namespace BiblioTech
lock (repoLock)
{
var userData = GetUserData(user);
var userData = GetUserDataMaybe(user);
if (userData == null)
{
result.Add("User has not joined the test net.");
result.Add("User has not interacted with bot.");
}
else
{
@ -100,36 +106,33 @@ namespace BiblioTech
public string[] GetUserReport(IUser user)
{
var userData = GetUserData(user);
var userData = GetUserDataMaybe(user);
if (userData == null) return new[] { "User has not joined the test net." };
return userData.CreateOverview();
}
public string[] GetUserReport(EthAddress ethAddress)
{
var userData = GetUserDataForAddress(ethAddress);
var userData = GetUserDataForAddressMaybe(ethAddress);
if (userData == null) return new[] { "No user is using this eth address." };
return userData.CreateOverview();
}
public UserData? GetUserDataForAddress(EthAddress? address)
public UserData? GetUserDataForAddressMaybe(EthAddress? address)
{
if (address == null) return null;
// If this becomes a performance problem, switch to in-memory cached list.
var files = Directory.GetFiles(Program.Config.UserDataPath);
foreach (var file in files)
var lower = address.Address.ToLowerInvariant();
if (string.IsNullOrEmpty(lower)) return null;
if (cache.Count == 0) LoadAllUserData();
foreach (var item in cache.Values)
{
try
if (item.CurrentAddress != null &&
item.CurrentAddress.Address.ToLowerInvariant() == lower)
{
var user = JsonConvert.DeserializeObject<UserData>(File.ReadAllText(file))!;
if (user.CurrentAddress != null &&
user.CurrentAddress.Address == address.Address)
{
return user;
}
return item;
}
catch { }
}
return null;
@ -137,7 +140,7 @@ namespace BiblioTech
private SetAddressResponse SetUserAddress(IUser user, EthAddress? address)
{
if (GetUserDataForAddress(address) != null)
if (GetUserDataForAddressMaybe(address) != null)
{
return SetAddressResponse.AddressAlreadyInUse;
}
@ -152,13 +155,12 @@ namespace BiblioTech
private void SetUserNotification(IUser user, bool notifyEnabled)
{
var userData = GetUserData(user);
if (userData == null) return;
var userData = GetOrCreate(user);
userData.NotificationsEnabled = notifyEnabled;
SaveUserData(userData);
}
private UserData? GetUserData(IUser user)
private UserData? GetUserDataMaybe(IUser user)
{
if (cache.ContainsKey(user.Id))
{
@ -177,7 +179,7 @@ namespace BiblioTech
private UserData GetOrCreate(IUser user)
{
var userData = GetUserData(user);
var userData = GetUserDataMaybe(user);
if (userData == null)
{
return CreateAndSaveNewUserData(user);

View File

@ -64,8 +64,12 @@ namespace CodexNetDeployer
var gethDeployment = DeployGeth(ci);
var gethNode = ci.WrapGethDeployment(gethDeployment, new BlockCache());
var bootNode = ci.StartCodexNode();
var versionInfo = bootNode.GetDebugInfo().Version;
bootNode.Stop(waitTillStopped: true);
Log("Geth started. Deploying Codex contracts...");
var contractsDeployment = ci.DeployCodexContracts(gethNode);
var contractsDeployment = ci.DeployCodexContracts(gethNode, versionInfo);
var contracts = ci.WrapCodexContractsDeployment(gethNode, contractsDeployment);
Log("Codex contracts deployed.");

View File

@ -21,7 +21,7 @@ namespace TestNetRewarder
return result == "Pong";
}
public async Task<bool> SendRewards(GiveRewardsCommand command)
public async Task<bool> SendRewards(EventsAndErrors command)
{
if (command == null) return false;
var result = await HttpPostJson(command);

View File

@ -33,6 +33,9 @@ namespace TestNetRewarder
[Uniform("proof-submitted-events", "pse", "PROOFSUBMITTEDEVENTS", false, "When greater than zero, chain event summary will include proof-submitted events.")]
public int ShowProofSubmittedEvents { get; set; } = 0; // Defaulted to zero, aprox 7 to 10 such events every 2 minutes in testnet (from autoclient alone!)
[Uniform("proof-period-report-hours", "pprh", "PROOFPERIODREPORTHOURS", false, "Frequency in hours with which proof period reports are created.")]
public int ProofReportHours { get; set; } = 24;
public string LogPath
{
get

View File

@ -8,7 +8,6 @@ namespace TestNetRewarder
public class Processor : ITimeSegmentHandler
{
private readonly RequestBuilder builder;
private readonly RewardChecker rewardChecker;
private readonly EventsFormatter eventsFormatter;
private readonly ChainState chainState;
private readonly Configuration config;
@ -23,23 +22,19 @@ namespace TestNetRewarder
this.log = log;
lastPeriodUpdateUtc = DateTime.UtcNow;
if (config.ProofReportHours < 1) throw new Exception("ProofReportHours must be one or greater");
builder = new RequestBuilder();
rewardChecker = new RewardChecker(builder);
eventsFormatter = new EventsFormatter(config);
var handler = new ChainStateChangeHandlerMux(
rewardChecker.Handler,
eventsFormatter
);
chainState = new ChainState(log, contracts, handler, config.HistoryStartUtc,
chainState = new ChainState(log, contracts, eventsFormatter, config.HistoryStartUtc,
doProofPeriodMonitoring: config.ShowProofPeriodReports > 0);
}
public async Task Initialize()
{
var events = eventsFormatter.GetInitializationEvents(config);
var request = builder.Build(events, Array.Empty<string>());
var request = builder.Build(chainState, events, Array.Empty<string>());
if (request.HasAny())
{
await client.SendRewards(request);
@ -54,9 +49,8 @@ namespace TestNetRewarder
var numberOfChainEvents = await ProcessEvents(timeRange);
var duration = sw.Elapsed;
if (numberOfChainEvents == 0) return TimeSegmentResponse.Underload;
if (numberOfChainEvents > 10) return TimeSegmentResponse.Overload;
if (duration > TimeSpan.FromSeconds(1)) return TimeSegmentResponse.Overload;
if (duration > TimeSpan.FromSeconds(1)) return TimeSegmentResponse.Underload;
if (duration > TimeSpan.FromSeconds(3)) return TimeSegmentResponse.Overload;
return TimeSegmentResponse.OK;
}
catch (Exception ex)
@ -76,7 +70,7 @@ namespace TestNetRewarder
var events = eventsFormatter.GetEvents();
var errors = eventsFormatter.GetErrors();
var request = builder.Build(events, errors);
var request = builder.Build(chainState, events, errors);
if (request.HasAny())
{
await client.SendRewards(request);
@ -87,7 +81,7 @@ namespace TestNetRewarder
private void ProcessPeriodUpdate()
{
if (config.ShowProofPeriodReports < 1) return;
if (DateTime.UtcNow < (lastPeriodUpdateUtc + TimeSpan.FromHours(1.0))) return;
if (DateTime.UtcNow < (lastPeriodUpdateUtc + TimeSpan.FromHours(config.ProofReportHours))) return;
lastPeriodUpdateUtc = DateTime.UtcNow;
eventsFormatter.ProcessPeriodReports(chainState.PeriodMonitor.GetAndClearReports());

View File

@ -45,6 +45,7 @@ namespace TestNetRewarder
Log.Log("Starting TestNet Rewarder...");
var segmenter = new TimeSegmenter(Log, Config.Interval, Config.HistoryStartUtc, processor);
await EnsureBotOnline();
await processor.Initialize();
while (!CancellationToken.IsCancellationRequested)

View File

@ -1,40 +1,55 @@
using DiscordRewards;
using CodexContractsPlugin.ChainMonitor;
using DiscordRewards;
using Utils;
namespace TestNetRewarder
{
public class RequestBuilder : IRewardGiver
public class RequestBuilder
{
private readonly Dictionary<ulong, List<EthAddress>> rewards = new Dictionary<ulong, List<EthAddress>>();
public void Give(RewardConfig reward, EthAddress receiver)
public EventsAndErrors Build(ChainState chainState, ChainEventMessage[] lines, string[] errors)
{
if (rewards.ContainsKey(reward.RoleId))
var activeChainAddresses = CollectActiveAddresses(chainState);
return new EventsAndErrors
{
rewards[reward.RoleId].Add(receiver);
EventsOverview = lines,
Errors = errors,
ActiveChainAddresses = activeChainAddresses
};
}
private ActiveChainAddresses CollectActiveAddresses(ChainState chainState)
{
var hosts = new List<string>();
var clients = new List<string>();
foreach (var request in chainState.Requests)
{
CollectAddresses(request, hosts, clients);
}
else
return new ActiveChainAddresses
{
rewards.Add(reward.RoleId, new List<EthAddress> { receiver });
Hosts = hosts.ToArray(),
Clients = clients.ToArray()
};
}
private void CollectAddresses(IChainStateRequest request, List<string> hosts, List<string> clients)
{
if (request.State != CodexContractsPlugin.RequestState.Started) return;
AddIfNew(clients, request.Client);
foreach (var host in request.Hosts.GetHosts())
{
AddIfNew(hosts, host);
}
}
public GiveRewardsCommand Build(ChainEventMessage[] lines, string[] errors)
private void AddIfNew(List<string> list, EthAddress address)
{
var result = new GiveRewardsCommand
{
Rewards = rewards.Select(p => new RewardUsersCommand
{
RewardId = p.Key,
UserAddresses = p.Value.Select(v => v.Address).ToArray()
}).ToArray(),
EventsOverview = lines,
Errors = errors
};
rewards.Clear();
return result;
var addr = address.Address;
if (!list.Contains(addr)) list.Add(addr);
}
}
}

View File

@ -1,113 +0,0 @@
using BlockchainUtils;
using CodexContractsPlugin.ChainMonitor;
using DiscordRewards;
using System.Numerics;
using Utils;
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 OnRequestFailed(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)
{
}
public void OnSlotReservationsFull(RequestEvent requestEvent, BigInteger slotIndex)
{
}
public void OnError(string msg)
{
}
public void OnProofSubmitted(BlockTimeEntry block, string id)
{
}
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;
ulong min = Convert.ToUInt64(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;
}
}
}

View File

@ -1,17 +0,0 @@
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 ChainStateChangeHandlerMux(checks);
}
public IChainStateChangeHandler Handler { get; }
}
}