Compare commits

..
Author SHA1 Message Date
Ben a0abea4432 Merge branch 'master' into feature/block-retransmit
# Conflicts:
#	ProjectPlugins/CodexPlugin/ApiChecker.cs
2024-10-22 12:54:22 +02:00
Ben 2f39327db2 Adds multiswarm test 2024-10-22 10:19:00 +02:00
Ben d452293cac makes autoclient delete old data files 2024-10-21 11:10:39 +02:00
benbierens 3e245b707c Fix updating of moment buckets 2024-10-21 11:05:48 +02:00
benbierens 9e842207ab improves error reporting for discord bot 2024-10-18 11:03:05 +02:00
benbierens e29ffe4f9c blockTimeFinder now uses inclusive time ranges 2024-10-18 10:35:49 +02:00
benbierens d8a6df5845 attempt to fix infinite loop in moment-reference-builder 2024-10-18 09:03:19 +02:00
benbierens 1859994ec6 nice csv outputs 2024-10-18 08:45:54 +02:00
Ben Bierens 60b489ced1 Merge pull request #104 from codex-storage/feature/bot-checks-cids
Feature/bot checks cids
2024-10-18 08:23:58 +02:00
benbierens b72b4a850b writes csv 2024-10-17 09:37:15 +02:00
benbierens fc942b11f8 Implements /check command 2024-10-16 14:05:02 +02:00
benbierens 2c88ddfb6b brings in api with new calls 2024-10-16 12:42:52 +02:00
benbierens e352e5c65c Moves code to file 2024-10-14 11:30:07 +02:00
benbierens 2cbe030cff Implements logical operations for indexSets 2024-10-14 11:19:40 +02:00
benbierens a38e93a607 set up logical operations for indexset 2024-10-14 10:10:18 +02:00
benbierens 269365e101 Orders chain events by blocknumber 2024-10-14 09:43:05 +02:00
benbierens 02ca9db001 wip: blockreceives to csv 2024-10-14 09:11:29 +02:00
benbierens 672092b232 handles codex events without node or peer 2024-10-14 09:11:09 +02:00
benbierens 1bd84a4892 combinatorial swarm tests 2024-10-11 11:20:59 +02:00
benbierens 5896735884 set up swarm test 2024-10-11 10:30:07 +02:00
Ben bebeb3766e Fixes unstable transcript test. 2024-10-10 15:31:38 +02:00
Ben e0cbf8c84d Updates emoji maps 2024-10-10 14:03:42 +02:00
Ben 3ed91d7310 Bumps containers to dotnet 8.0 2024-10-10 11:54:42 +02:00
Ben 9573814574 Adds emoji icons to chain events log 2024-10-10 11:45:04 +02:00
Ben 5313f8a7ac Adds SlotReservationsFull event to chain state tracker 2024-10-10 11:15:07 +02:00
Ben c239d555dc More reliable test 2024-10-09 11:37:16 +02:00
Ben 2e9d7641a3 Adds test to capture upload-interrupt node crash. 2024-10-09 09:11:04 +02:00
Ben fc9249da20 updates to 0.1.6 2024-10-08 14:49:19 +02:00
Ben 978e085219 updates contracts plugin 2024-10-08 10:38:49 +02:00
Ben 960b0c3788 Automatic updating of CodexPlugin openapi.yaml. 2024-10-08 09:29:26 +02:00
benbierens 9ca4bf8afc Updates marketplace plugin 2024-10-07 15:59:05 +02:00
Slava cfdc25335c Add .gitattributes for line endings in .bat, .cmd and .sh scripts (#103) 2024-10-05 23:08:49 +03:00
benbierens 04f087efe4 Bump to dotnet 8 2024-10-03 14:02:28 +02:00
Ben f6aa122245 Fixes issue where timesegments is locked up for max 50x segmentDuration when catching up to realtime. 2024-09-30 16:23:55 +02:00
Ben db4c4a87e0 Rewarderbot: Speeds up time segments when catching up to realtime. 2024-09-30 15:34:40 +02:00
Ben ffb5eb294a Fix kubernetes port name issue 2024-09-30 15:23:14 +02:00
Ben 6a8c74e02a Adds short-format UTC to block header for each chain-events channel entry. 2024-09-30 11:09:44 +02:00
Ben 200de1d7f7 Update: Bot reports interactions in admin channel 2024-09-30 10:59:22 +02:00
Ben f801cb082e update 2024-09-24 15:21:33 +02:00
Ben 2a61dad556 updates codex to 0.1.4 2024-09-24 13:06:00 +02:00
Ben 2d90349b7b Updates autobootstrap fixture for parallel running. 2024-09-24 10:10:59 +02:00
Ben c4b6d01530 Adds check for usage of availabilities. 2024-09-23 11:31:57 +02:00
Ben c9fedac592 Adds call to get availabilities 2024-09-23 10:52:12 +02:00
benbierens cedec0d4cc defaults for EC params that actually work 2024-09-12 15:29:50 +02:00
benbierens 769b9c3aca div by zero in time calculation 2024-09-12 15:14:15 +02:00
benbierens 88c675adf9 implements performance logger for autoclient 2024-09-12 15:03:06 +02:00
benbierens 75fcc68caf Merge branch 'feature/better-autoclient' 2024-09-12 14:42:42 +02:00
benbierens a41272f160 Removes rest server 2024-09-12 14:42:19 +02:00
benbierens 8e018cbae9 setup 2024-09-12 14:38:15 +02:00
Ben 3c447eb4c5 multiple nodes on one autoclient 2024-09-12 12:05:42 +02:00
benbierens d53b760731 fixes docker path 2024-09-11 14:26:28 +02:00
benbierens fcadceb009 References autoclientcenter from autoclient 2024-09-11 14:24:12 +02:00
benbierens a02d9558e5 Merge branch 'feature/better-autoclient' 2024-09-11 14:08:29 +02:00
benbierens b3013a9b65 moves project 2024-09-11 14:08:21 +02:00
benbierens 6b0a16b627 Merge branch 'feature/better-autoclient' 2024-09-11 14:07:04 +02:00
benbierens eac06e8b3a builds docker image 2024-09-11 14:06:48 +02:00
benbierens f7fa35c7ba Implements center service 2024-09-11 14:00:22 +02:00
Ben a7526aaed1 setup center api 2024-09-11 12:08:06 +02:00
benbierens e7d9e833f1 Merge branch 'feature/self-updating-contracts-code' 2024-08-30 12:42:24 +02:00
benbierens 13dd0a649c Moves self-updater call to starter class. 2024-08-30 12:25:41 +02:00
benbierens bf5bd8d726 Successful automatic update 2024-08-30 11:17:36 +02:00
benbierens 8dcf9ff15e first implementation of contracts self-updater 2024-08-30 10:58:27 +02:00
benbierens 489209d549 generates deployment base class 2024-08-30 10:27:03 +02:00
Ben 017cee43c0 Merge branch 'index-encoding' 2024-08-27 13:32:46 +02:00
135 changed files with 3397 additions and 1167 deletions
+10
View File
@@ -0,0 +1,10 @@
# Set default behavior to automatically normalize line endings.
* text=auto
# Force bash scripts to always use lf line endings so that if a repo is accessed
# in Unix via a file share from Windows, the scripts will work.
*.sh text eol=lf
# Likewise, force cmd and batch scripts to always use crlf
*.cmd text eol=crlf
*.bat text eol=crlf
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
@@ -3,7 +3,8 @@
public class GiveRewardsCommand
{
public RewardUsersCommand[] Rewards { get; set; } = Array.Empty<RewardUsersCommand>();
public string[] EventsOverview { get; set; } = Array.Empty<string>();
public ChainEventMessage[] EventsOverview { get; set; } = Array.Empty<ChainEventMessage>();
public string[] Errors { get; set; } = Array.Empty<string>();
public bool HasAny()
{
@@ -16,4 +17,10 @@
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;
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
+1 -1
View File
@@ -24,8 +24,8 @@
.Replace("]", "-")
.Replace(",", "-");
result = result.Trim('-');
if (result.Length > maxLength) result = result.Substring(0, maxLength);
result = result.Trim('-');
return result;
}
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>KubernetesWorkflow</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -24,13 +24,12 @@ namespace KubernetesWorkflow.Types
[JsonIgnore]
public RunningPod RunningPod { get; internal set; } = null!;
public Address GetAddress(ILog log, string portTag)
public Address GetAddress(string portTag)
{
var addresses = Addresses.Where(a => a.PortTag == portTag).ToArray();
if (!addresses.Any()) throw new Exception("No addresses found for portTag: " + portTag);
var select = SelectAddress(addresses);
log.Debug($"Container '{Name}' selected for tag '{portTag}' address: '{select}'");
return select.Address;
}
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>Logging</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -29,7 +29,8 @@ namespace NethereumWorkflow.BlockUtils
public ulong? GetHighestBlockNumberBefore(DateTime moment)
{
bounds.Initialize();
if (moment <= bounds.Genesis.Utc) return null;
if (moment < bounds.Genesis.Utc) return null;
if (moment == bounds.Genesis.Utc) return bounds.Genesis.BlockNumber;
if (moment >= bounds.Current.Utc) return bounds.Current.BlockNumber;
return Log(() => Search(bounds.Genesis, bounds.Current, moment, HighestBeforeSelector));
@@ -38,7 +39,8 @@ namespace NethereumWorkflow.BlockUtils
public ulong? GetLowestBlockNumberAfter(DateTime moment)
{
bounds.Initialize();
if (moment >= bounds.Current.Utc) return null;
if (moment > bounds.Current.Utc) return null;
if (moment == bounds.Current.Utc) return bounds.Current.BlockNumber;
if (moment <= bounds.Genesis.Utc) return bounds.Genesis.BlockNumber;
return Log(()=> Search(bounds.Genesis, bounds.Current, moment, LowestAfterSelector)); ;
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>NethereumWorkflow</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -7,6 +7,7 @@ namespace OverwatchTranscript
public interface IFinalizedBucket
{
bool IsEmpty { get; }
void Update();
DateTime? SeeTopUtc();
BucketTop? TakeTop();
}
@@ -28,7 +29,8 @@ namespace OverwatchTranscript
private readonly string bucketFile;
private readonly ConcurrentQueue<BucketTop> topQueue = new ConcurrentQueue<BucketTop>();
private readonly AutoResetEvent itemDequeued = new AutoResetEvent(false);
private bool stopping;
private readonly AutoResetEvent itemEnqueued = new AutoResetEvent(false);
private bool sourceIsEmpty;
public EventBucketReader(ILog log, string bucketFile)
{
@@ -42,34 +44,38 @@ namespace OverwatchTranscript
public bool IsEmpty { get; private set; }
public void Update()
{
if (IsEmpty) return;
while (topQueue.Count == 0)
{
UpdateIsEmpty();
if (IsEmpty) return;
itemDequeued.Set();
itemEnqueued.WaitOne(200);
}
}
public DateTime? SeeTopUtc()
{
if (IsEmpty) return null;
while (true)
if (topQueue.TryPeek(out BucketTop? top))
{
UpdateIsEmpty();
if (IsEmpty) return null;
if (topQueue.TryPeek(out BucketTop? top))
{
return top.Utc;
}
return top.Utc;
}
return null;
}
public BucketTop? TakeTop()
{
if (IsEmpty) return null;
while (true)
if (topQueue.TryDequeue(out BucketTop? top))
{
UpdateIsEmpty();
if (IsEmpty) return null;
if (topQueue.TryDequeue(out BucketTop? top))
{
itemDequeued.Set();
return top;
}
itemDequeued.Set();
return top;
}
return null;
}
private void ReadBucket()
@@ -85,23 +91,25 @@ namespace OverwatchTranscript
if (top != null)
{
topQueue.Enqueue(top);
itemEnqueued.Set();
}
else
{
stopping = true;
sourceIsEmpty = true;
UpdateIsEmpty();
return;
}
}
itemDequeued.Reset();
itemDequeued.WaitOne();
itemDequeued.WaitOne(5000);
}
}
private void UpdateIsEmpty()
{
var empty = stopping && topQueue.IsEmpty;
if (!IsEmpty && empty)
var allEmpty = sourceIsEmpty && topQueue.IsEmpty;
if (!IsEmpty && allEmpty)
{
File.Delete(bucketFile);
IsEmpty = true;
@@ -24,6 +24,8 @@ namespace OverwatchTranscript
log.Debug($"Building references for {buckets.Count} buckets.");
while (buckets.Any())
{
foreach (var b in buckets) b.Update();
buckets.RemoveAll(b => b.IsEmpty);
if (!buckets.Any()) break;
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
+41
View File
@@ -0,0 +1,41 @@
namespace Utils
{
public static class PluginPathUtils
{
private const string ProjectPluginsFolderName = "ProjectPlugins";
private static string projectPluginsDir = string.Empty;
public static string ProjectPluginsDir
{
get
{
if (string.IsNullOrEmpty(projectPluginsDir)) projectPluginsDir = FindProjectPluginsDir();
return projectPluginsDir;
}
}
private static string FindProjectPluginsDir()
{
var current = Directory.GetCurrentDirectory();
while (true)
{
var localFolders = Directory.GetDirectories(current);
var projectPluginsFolders = localFolders.Where(l => l.EndsWith(ProjectPluginsFolderName)).ToArray();
if (projectPluginsFolders.Length == 1)
{
return projectPluginsFolders.Single();
}
var parent = Directory.GetParent(current);
if (parent == null)
{
var msg = $"Unable to locate '{ProjectPluginsFolderName}' folder. Travelled up from: '{Directory.GetCurrentDirectory()}'";
Console.WriteLine(msg);
throw new Exception(msg);
}
current = parent.FullName;
}
}
}
}
+26 -9
View File
@@ -3,24 +3,41 @@
public static class RandomUtils
{
private static readonly Random random = new Random();
private static readonly object @lock = new object();
public static T GetOneRandom<T>(this T[] items)
{
lock (@lock)
{
var i = random.Next(0, items.Length);
var result = items[i];
return result;
}
}
public static T PickOneRandom<T>(this List<T> remainingItems)
{
var i = random.Next(0, remainingItems.Count);
var result = remainingItems[i];
remainingItems.RemoveAt(i);
return result;
lock (@lock)
{
var i = random.Next(0, remainingItems.Count);
var result = remainingItems[i];
remainingItems.RemoveAt(i);
return result;
}
}
public static T[] Shuffled<T>(T[] items)
{
var result = new List<T>();
var source = items.ToList();
while (source.Any())
lock (@lock)
{
result.Add(RandomUtils.PickOneRandom(source));
var result = new List<T>();
var source = items.ToList();
while (source.Any())
{
result.Add(RandomUtils.PickOneRandom(source));
}
return result.ToArray();
}
return result.ToArray();
}
}
}
+4
View File
@@ -71,6 +71,10 @@
task();
return;
}
catch (OperationCanceledException)
{
return;
}
catch (Exception ex)
{
var failure = CaptureFailure(ex);
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>Utils</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
@@ -1,4 +1,5 @@
using CodexContractsPlugin.Marketplace;
using System.Collections.Generic;
using Utils;
namespace CodexContractsPlugin.ChainMonitor
@@ -12,7 +13,8 @@ namespace CodexContractsPlugin.ChainMonitor
RequestCancelledEventDTO[] cancelled,
RequestFailedEventDTO[] failed,
SlotFilledEventDTO[] slotFilled,
SlotFreedEventDTO[] slotFreed
SlotFreedEventDTO[] slotFreed,
SlotReservationsFullEventDTO[] slotReservationsFull
)
{
BlockInterval = blockInterval;
@@ -22,6 +24,9 @@ namespace CodexContractsPlugin.ChainMonitor
Failed = failed;
SlotFilled = slotFilled;
SlotFreed = slotFreed;
SlotReservationsFull = slotReservationsFull;
All = ConcatAll<IHasBlock>(requests, fulfilled, cancelled, failed, slotFilled, SlotFreed, SlotReservationsFull);
}
public BlockInterval BlockInterval { get; }
@@ -31,21 +36,8 @@ namespace CodexContractsPlugin.ChainMonitor
public RequestFailedEventDTO[] Failed { get; }
public SlotFilledEventDTO[] SlotFilled { get; }
public SlotFreedEventDTO[] SlotFreed { get; }
public IHasBlock[] All
{
get
{
var all = new List<IHasBlock>();
all.AddRange(Requests);
all.AddRange(Fulfilled);
all.AddRange(Cancelled);
all.AddRange(Failed);
all.AddRange(SlotFilled);
all.AddRange(SlotFreed);
return all.ToArray();
}
}
public SlotReservationsFullEventDTO[] SlotReservationsFull { get; }
public IHasBlock[] All { get; }
public static ChainEvents FromBlockInterval(ICodexContracts contracts, BlockInterval blockInterval)
{
@@ -66,8 +58,19 @@ namespace CodexContractsPlugin.ChainMonitor
events.GetRequestCancelledEvents(),
events.GetRequestFailedEvents(),
events.GetSlotFilledEvents(),
events.GetSlotFreedEvents()
events.GetSlotFreedEvents(),
events.GetSlotReservationsFull()
);
}
private T[] ConcatAll<T>(params T[][] arrays)
{
var result = Array.Empty<T>();
foreach (var array in arrays)
{
result = result.Concat(array).ToArray();
}
return result;
}
}
}
@@ -16,6 +16,9 @@ namespace CodexContractsPlugin.ChainMonitor
void OnRequestFailed(RequestEvent requestEvent);
void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex);
void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex);
void OnSlotReservationsFull(RequestEvent requestEvent, BigInteger slotIndex);
void OnError(string msg);
}
public class RequestEvent
@@ -48,24 +51,29 @@ namespace CodexContractsPlugin.ChainMonitor
public TimeRange TotalSpan { get; private set; }
public IChainStateRequest[] Requests => requests.ToArray();
public void Update()
public int Update()
{
Update(DateTime.UtcNow);
return Update(DateTime.UtcNow);
}
public void Update(DateTime toUtc)
public int Update(DateTime toUtc)
{
var span = new TimeRange(TotalSpan.To, toUtc);
var events = ChainEvents.FromTimeRange(contracts, span);
Apply(events);
TotalSpan = new TimeRange(TotalSpan.From, span.To);
return events.All.Length;
}
private void Apply(ChainEvents events)
{
if (events.BlockInterval.TimeRange.From < TotalSpan.From)
throw new Exception("Attempt to update ChainState with set of events from before its current record.");
{
var msg = "Attempt to update ChainState with set of events from before its current record.";
handler.OnError(msg);
throw new Exception(msg);
}
log.Log($"ChainState updating: {events.BlockInterval}");
@@ -108,7 +116,7 @@ namespace CodexContractsPlugin.ChainMonitor
private void ApplyEvent(RequestFulfilledEventDTO @event)
{
var r = FindRequest(@event.RequestId);
var r = FindRequest(@event);
if (r == null) return;
r.UpdateState(@event.Block.BlockNumber, RequestState.Started);
handler.OnRequestFulfilled(new RequestEvent(@event.Block, r));
@@ -116,7 +124,7 @@ namespace CodexContractsPlugin.ChainMonitor
private void ApplyEvent(RequestCancelledEventDTO @event)
{
var r = FindRequest(@event.RequestId);
var r = FindRequest(@event);
if (r == null) return;
r.UpdateState(@event.Block.BlockNumber, RequestState.Cancelled);
handler.OnRequestCancelled(new RequestEvent(@event.Block, r));
@@ -124,7 +132,7 @@ namespace CodexContractsPlugin.ChainMonitor
private void ApplyEvent(RequestFailedEventDTO @event)
{
var r = FindRequest(@event.RequestId);
var r = FindRequest(@event);
if (r == null) return;
r.UpdateState(@event.Block.BlockNumber, RequestState.Failed);
handler.OnRequestFailed(new RequestEvent(@event.Block, r));
@@ -132,7 +140,7 @@ namespace CodexContractsPlugin.ChainMonitor
private void ApplyEvent(SlotFilledEventDTO @event)
{
var r = FindRequest(@event.RequestId);
var r = FindRequest(@event);
if (r == null) return;
r.Hosts.Add(@event.Host, (int)@event.SlotIndex);
r.Log($"[{@event.Block.BlockNumber}] SlotFilled (host:'{@event.Host}', slotIndex:{@event.SlotIndex})");
@@ -141,13 +149,21 @@ namespace CodexContractsPlugin.ChainMonitor
private void ApplyEvent(SlotFreedEventDTO @event)
{
var r = FindRequest(@event.RequestId);
var r = FindRequest(@event);
if (r == null) return;
r.Hosts.RemoveHost((int)@event.SlotIndex);
r.Log($"[{@event.Block.BlockNumber}] SlotFreed (slotIndex:{@event.SlotIndex})");
handler.OnSlotFreed(new RequestEvent(@event.Block, r), @event.SlotIndex);
}
private void ApplyEvent(SlotReservationsFullEventDTO @event)
{
var r = FindRequest(@event);
if (r == null) return;
r.Log($"[{@event.Block.BlockNumber}] SlotReservationsFull (slotIndex:{@event.SlotIndex})");
handler.OnSlotReservationsFull(new RequestEvent(@event.Block, r), @event.SlotIndex);
}
private void ApplyTimeImplicitEvents(ulong blockNumber, DateTime eventsUtc)
{
foreach (var r in requests)
@@ -161,10 +177,23 @@ namespace CodexContractsPlugin.ChainMonitor
}
}
private ChainStateRequest? FindRequest(byte[] requestId)
private ChainStateRequest? FindRequest(IHasRequestId request)
{
var r = requests.SingleOrDefault(r => Equal(r.Request.RequestId, requestId));
if (r == null) log.Log("Unable to find request by ID!");
var r = requests.SingleOrDefault(r => Equal(r.Request.RequestId, request.RequestId));
if (r == null)
{
var blockNumber = "unknown";
if (request is IHasBlock blk)
{
blockNumber = blk.Block.BlockNumber.ToString();
}
var msg = $"Received event of type '{request.GetType()}' in block '{blockNumber}' for request by Id: '{request.RequestId}'. " +
$"Failed to find request. Request creation event not seen! (Tracker start time: {TotalSpan.From})";
log.Error(msg);
handler.OnError(msg);
}
return r;
}
@@ -1,10 +1,5 @@
using GethPlugin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace CodexContractsPlugin.ChainMonitor
{
@@ -51,5 +46,15 @@ namespace CodexContractsPlugin.ChainMonitor
{
foreach (var handler in Handlers) handler.OnSlotFreed(requestEvent, slotIndex);
}
public void OnSlotReservationsFull(RequestEvent requestEvent, BigInteger slotIndex)
{
foreach (var handler in Handlers) handler.OnSlotReservationsFull(requestEvent, slotIndex);
}
public void OnError(string msg)
{
foreach (var handler in Handlers) handler.OnError(msg);
}
}
}
@@ -32,5 +32,13 @@ namespace CodexContractsPlugin.ChainMonitor
public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex)
{
}
public void OnSlotReservationsFull(RequestEvent requestEvent, BigInteger slotIndex)
{
}
public void OnError(string msg)
{
}
}
}
@@ -19,7 +19,7 @@ namespace CodexContractsPlugin
{
var config = startupConfig.Get<CodexContractsContainerConfig>();
var address = config.GethNode.StartResult.Container.GetAddress(new NullLog(), GethContainerRecipe.HttpPortTag);
var address = config.GethNode.StartResult.Container.GetAddress(GethContainerRecipe.HttpPortTag);
SetSchedulingAffinity(notIn: "false");
@@ -1,6 +1,7 @@
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using Logging;
using Nethereum.Contracts;
using Nethereum.Hex.HexTypes;
using NethereumWorkflow.BlockUtils;
using Utils;
@@ -16,6 +17,7 @@ namespace CodexContractsPlugin
RequestFailedEventDTO[] GetRequestFailedEvents();
SlotFilledEventDTO[] GetSlotFilledEvents();
SlotFreedEventDTO[] GetSlotFreedEvents();
SlotReservationsFullEventDTO[] GetSlotReservationsFull();
}
public class CodexContractsEvents : ICodexContractsEvents
@@ -38,49 +40,32 @@ namespace CodexContractsPlugin
{
var events = gethNode.GetEvents<StorageRequestedEventDTO>(deployment.MarketplaceAddress, BlockInterval);
var i = new ContractInteractions(log, gethNode);
return events
.Select(e =>
return events.Select(e =>
{
var requestEvent = i.GetRequest(deployment.MarketplaceAddress, e.Event.RequestId);
var result = requestEvent.ReturnValue1;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.RequestId = e.Event.RequestId;
return result;
})
.ToArray();
var requestEvent = i.GetRequest(deployment.MarketplaceAddress, e.Event.RequestId);
var result = requestEvent.ReturnValue1;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.RequestId = e.Event.RequestId;
return result;
}).ToArray();
}
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents()
{
var events = gethNode.GetEvents<RequestFulfilledEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
return events.Select(SetBlockOnEvent).ToArray();
}
public RequestCancelledEventDTO[] GetRequestCancelledEvents()
{
var events = gethNode.GetEvents<RequestCancelledEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
return events.Select(SetBlockOnEvent).ToArray();
}
public RequestFailedEventDTO[] GetRequestFailedEvents()
{
var events = gethNode.GetEvents<RequestFailedEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
return events.Select(SetBlockOnEvent).ToArray();
}
public SlotFilledEventDTO[] GetSlotFilledEvents()
@@ -98,12 +83,20 @@ namespace CodexContractsPlugin
public SlotFreedEventDTO[] GetSlotFreedEvents()
{
var events = gethNode.GetEvents<SlotFreedEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
return events.Select(SetBlockOnEvent).ToArray();
}
public SlotReservationsFullEventDTO[] GetSlotReservationsFull()
{
var events = gethNode.GetEvents<SlotReservationsFullEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(SetBlockOnEvent).ToArray();
}
private T SetBlockOnEvent<T>(EventLog<T> e) where T : IHasBlock
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}
private BlockTimeEntry GetBlock(ulong number)
@@ -1,11 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nethereum.Generators" Version="4.21.4" />
<PackageReference Include="Nethereum.Generators.Net" Version="4.21.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\Core\Core.csproj" />
<ProjectReference Include="..\GethPlugin\GethPlugin.csproj" />
@@ -1,4 +1,5 @@
using Core;
using CodexContractsPlugin.Marketplace;
using Core;
using GethPlugin;
using KubernetesWorkflow;
using KubernetesWorkflow.Types;
@@ -64,7 +65,8 @@ namespace CodexContractsPlugin
var extractor = new ContractsContainerInfoExtractor(tools.GetLog(), workflow, container);
var marketplaceAddress = extractor.ExtractMarketplaceAddress();
var abi = extractor.ExtractMarketplaceAbi();
var (abi, bytecode) = extractor.ExtractMarketplaceAbiAndByteCode();
EnsureCompatbility(abi, bytecode);
var interaction = new ContractInteractions(tools.GetLog(), gethNode);
var tokenAddress = interaction.GetTokenAddress(marketplaceAddress);
@@ -78,6 +80,18 @@ namespace CodexContractsPlugin
return new CodexContractsDeployment(marketplaceAddress, abi, tokenAddress);
}
private void EnsureCompatbility(string abi, string bytecode)
{
var expectedByteCode = MarketplaceDeploymentBase.BYTECODE.ToLowerInvariant();
if (bytecode != expectedByteCode)
{
Log("Deployed contract is incompatible with current build of CodexContracts plugin. Running self-updater...");
var selfUpdater = new SelfUpdater();
selfUpdater.Update(abi, bytecode);
}
}
private void Log(string msg)
{
tools.GetLog().Log(msg);
@@ -31,14 +31,14 @@ namespace CodexContractsPlugin
return marketplaceAddress;
}
public string ExtractMarketplaceAbi()
public (string, string) ExtractMarketplaceAbiAndByteCode()
{
log.Debug();
var marketplaceAbi = Retry(FetchMarketplaceAbi);
if (string.IsNullOrEmpty(marketplaceAbi)) throw new InvalidOperationException("Unable to fetch marketplace artifacts from codex-contracts node. Test infra failure.");
var (abi, bytecode) = Retry(FetchMarketplaceAbiAndByteCode);
if (string.IsNullOrEmpty(abi)) throw new InvalidOperationException("Unable to fetch marketplace artifacts from codex-contracts node. Test infra failure.");
log.Debug("Got Marketplace ABI: " + marketplaceAbi);
return marketplaceAbi;
log.Debug("Got Marketplace ABI: " + abi);
return (abi, bytecode);
}
private string FetchMarketplaceAddress()
@@ -48,7 +48,7 @@ namespace CodexContractsPlugin
return marketplace!.address;
}
private string FetchMarketplaceAbi()
private (string, string) FetchMarketplaceAbiAndByteCode()
{
var json = workflow.ExecuteCommand(container, "cat", CodexContractsContainerRecipe.MarketplaceArtifactFilename);
@@ -56,19 +56,12 @@ namespace CodexContractsPlugin
var abi = artifact["abi"];
var byteCode = artifact["bytecode"];
var abiResult = abi!.ToString(Formatting.None);
var byteCodeResult = byteCode!.ToString(Formatting.None);
if (byteCodeResult
.ToLowerInvariant()
.Replace("\"", "") != MarketplaceDeploymentBase.BYTECODE.ToLowerInvariant())
{
throw new Exception("BYTECODE in CodexContractsPlugin does not match BYTECODE deployed by container. Update Marketplace.cs generated code?");
}
return abiResult;
var byteCodeResult = byteCode!.ToString(Formatting.None).ToLowerInvariant().Replace("\"", "");
return (abiResult, byteCodeResult);
}
private static string Retry(Func<string> fetch)
private static T Retry<T>(Func<T> fetch)
{
return Time.Retry(fetch, nameof(ContractsContainerInfoExtractor));
}
@@ -10,7 +10,12 @@ namespace CodexContractsPlugin.Marketplace
BlockTimeEntry Block { get; set; }
}
public partial class Request : RequestBase, IHasBlock
public interface IHasRequestId
{
byte[] RequestId { get; set; }
}
public partial class Request : RequestBase, IHasBlock, IHasRequestId
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
@@ -28,32 +33,38 @@ namespace CodexContractsPlugin.Marketplace
}
}
public partial class RequestFulfilledEventDTO : IHasBlock
public partial class RequestFulfilledEventDTO : IHasBlock, IHasRequestId
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
}
public partial class RequestCancelledEventDTO : IHasBlock
public partial class RequestCancelledEventDTO : IHasBlock, IHasRequestId
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
}
public partial class RequestFailedEventDTO : IHasBlock
public partial class RequestFailedEventDTO : IHasBlock, IHasRequestId
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
}
public partial class SlotFilledEventDTO : IHasBlock
public partial class SlotFilledEventDTO : IHasBlock, IHasRequestId
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
public EthAddress Host { get; set; }
}
public partial class SlotFreedEventDTO : IHasBlock
public partial class SlotFreedEventDTO : IHasBlock, IHasRequestId
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
}
public partial class SlotReservationsFullEventDTO : IHasBlock, IHasRequestId
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
File diff suppressed because one or more lines are too long
@@ -0,0 +1,96 @@
using Utils;
namespace CodexContractsPlugin
{
public class SelfUpdater
{
public void Update(string abi, string bytecode)
{
var filePath = GetMarketplaceFilePath();
var content = GenerateContent(abi, bytecode);
var contentLines = content.Split("\r\n");
var beginWith = new string[]
{
"using Nethereum.ABI.FunctionEncoding.Attributes;",
"using Nethereum.Contracts;",
"using System.Numerics;",
"",
"// Generated code, do not modify.",
"",
"#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.",
"namespace CodexContractsPlugin.Marketplace",
"{"
};
var endWith = new string[]
{
"}",
"#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable."
};
File.Delete(filePath);
File.WriteAllLines(filePath,
beginWith.Concat(
contentLines.Concat(
endWith))
);
throw new Exception("Oh no! CodexContracts were updated. Current build of CodexContractsPlugin is incompatible. " +
"But fear not! SelfUpdater.cs has automatically updated the plugin. Just rebuild and rerun and it should work. " +
"Just in case, manual update instructions are found here: 'CodexContractsPlugin/Marketplace/README.md'.");
}
private string GetMarketplaceFilePath()
{
var projectPluginDir = PluginPathUtils.ProjectPluginsDir;
var path = Path.Combine(projectPluginDir, "CodexContractsPlugin", "Marketplace", "Marketplace.cs");
if (!File.Exists(path)) throw new Exception("Marketplace file not found. Expected: " + path);
return path;
}
private string GenerateContent(string abi, string bytecode)
{
var deserializer = new Nethereum.Generators.Net.GeneratorModelABIDeserialiser();
var abiModel = deserializer.DeserialiseABI(abi);
var abiCtor = abiModel.Constructor;
var c = new Nethereum.Generators.CQS.ContractDeploymentCQSMessageGenerator(abiCtor, "namespace", bytecode, "Marketplace", Nethereum.Generators.Core.CodeGenLanguage.CSharp);
var lines = "";
lines += c.GenerateClass();
lines += "\r\n";
foreach (var eventAbi in abiModel.Events)
{
var d = new Nethereum.Generators.DTOs.EventDTOGenerator(eventAbi, "namespace", Nethereum.Generators.Core.CodeGenLanguage.CSharp);
lines += d.GenerateClass();
lines += "\r\n";
}
foreach (var errorAbi in abiModel.Errors)
{
var e = new Nethereum.Generators.DTOs.ErrorDTOGenerator(errorAbi, "namespace", Nethereum.Generators.Core.CodeGenLanguage.CSharp);
lines += e.GenerateClass();
lines += "\r\n";
}
foreach (var funcAbi in abiModel.Functions)
{
var f = new Nethereum.Generators.DTOs.FunctionOutputDTOGenerator(funcAbi, "namespace", Nethereum.Generators.Core.CodeGenLanguage.CSharp);
var ff = new Nethereum.Generators.CQS.FunctionCQSMessageGenerator(funcAbi, "namespace", "funcoutput", Nethereum.Generators.Core.CodeGenLanguage.CSharp);
lines += f.GenerateClass();
lines += "\r\n";
lines += ff.GenerateClass();
lines += "\r\n";
}
foreach (var structAbi in abiModel.Structs)
{
var g = new Nethereum.Generators.DTOs.StructTypeGenerator(structAbi, "namespace", Nethereum.Generators.Core.CodeGenLanguage.CSharp);
lines += g.GenerateClass();
lines += "\r\n";
}
return lines;
}
}
}
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
+18 -3
View File
@@ -3,13 +3,14 @@ using KubernetesWorkflow.Types;
using Logging;
using System.Security.Cryptography;
using System.Text;
using Utils;
namespace CodexPlugin
{
public class ApiChecker
{
// <INSERT-OPENAPI-YAML-HASH>
private const string OpenApiYamlHash = "67-76-AB-FC-54-4F-EB-81-F5-E4-F8-27-DF-82-92-41-63-A5-EA-1B-17-14-0C-BE-20-9C-B3-DF-CE-E4-AA-38";
private const string OpenApiYamlHash = "D5-C3-18-71-E8-FF-8F-89-9C-6B-98-3C-F2-C2-D2-37-0A-9F-27-23-35-67-EA-F6-1F-F9-D5-C6-63-34-5A-92";
private const string OpenApiFilePath = "/codex/openapi.yaml";
private const string DisableEnvironmentVariable = "CODEXPLUGIN_DISABLE_APICHECK";
@@ -21,8 +22,9 @@ namespace CodexPlugin
private const string Failure =
"Codex API compatibility check failed! " +
"openapi.yaml used by CodexPlugin does not match openapi.yaml in Codex container. Please update the openapi.yaml in " +
"'ProjectPlugins/CodexPlugin' and rebuild this project. If you wish to disable API compatibility checking, please set " +
"openapi.yaml used by CodexPlugin does not match openapi.yaml in Codex container. The openapi.yaml in " +
"'ProjectPlugins/CodexPlugin' has been overwritten with the container one. " +
"Please and rebuild this project. If you wish to disable API compatibility checking, please set " +
$"the environment variable '{DisableEnvironmentVariable}' or set the disable bool in 'ProjectPlugins/CodexPlugin/ApiChecker.cs'.";
private static bool checkPassed = false;
@@ -71,10 +73,23 @@ namespace CodexPlugin
return;
}
OverwriteOpenApiYaml(containerApi);
log.Error(Failure);
throw new Exception(Failure);
}
private void OverwriteOpenApiYaml(string containerApi)
{
Log("API compatibility check failed. Updating CodexPlugin...");
var openApiFilePath = Path.Combine(PluginPathUtils.ProjectPluginsDir, "CodexPlugin", "openapi.yaml");
if (!File.Exists(openApiFilePath)) throw new Exception("Unable to locate CodexPlugin/openapi.yaml. Expected: " + openApiFilePath);
File.Delete(openApiFilePath);
File.WriteAllText(openApiFilePath, containerApi);
Log("CodexPlugin/openapi.yaml has been updated.");
}
private string Hash(string file)
{
var fileBytes = Encoding.ASCII.GetBytes(file
+11 -5
View File
@@ -73,7 +73,7 @@ namespace CodexPlugin
public Stream DownloadFile(string contentId, Action<Failure> onFailure)
{
var fileResponse = OnCodex(
api => api.DownloadNetworkAsync(contentId),
api => api.DownloadNetworkStreamAsync(contentId),
CreateRetryConfig(nameof(DownloadFile), onFailure));
if (fileResponse.StatusCode != 200) throw new Exception("Download failed with StatusCode: " + fileResponse.StatusCode);
@@ -88,19 +88,25 @@ namespace CodexPlugin
public StorageAvailability SalesAvailability(StorageAvailability request)
{
var body = mapper.Map(request);
var read = OnCodex<SalesAvailabilityREAD>(api => api.OfferStorageAsync(body));
var read = OnCodex(api => api.OfferStorageAsync(body));
return mapper.Map(read);
}
public StorageAvailability[] GetAvailabilities()
{
var collection = OnCodex(api => api.GetAvailabilitiesAsync());
return mapper.Map(collection);
}
public string RequestStorage(StoragePurchaseRequest request)
{
var body = mapper.Map(request);
return OnCodex<string>(api => api.CreateStorageRequestAsync(request.ContentId.Id, body));
return OnCodex(api => api.CreateStorageRequestAsync(request.ContentId.Id, body));
}
public CodexSpace Space()
{
var space = OnCodex<Space>(api => api.SpaceAsync());
var space = OnCodex(api => api.SpaceAsync());
return mapper.Map(space);
}
@@ -189,7 +195,7 @@ namespace CodexPlugin
private Address GetAddress()
{
return Container.Containers.Single().GetAddress(log, CodexContainerRecipe.ApiPortTag);
return Container.Containers.Single().GetAddress(CodexContainerRecipe.ApiPortTag);
}
private string GetHttpId()
@@ -7,8 +7,7 @@ namespace CodexPlugin
{
public class CodexContainerRecipe : ContainerRecipeFactory
{
private const string DefaultDockerImage = "codexstorage/nim-codex:sha-64b82de-dist-tests";
private const string DefaultDockerImage = "codexstorage/nim-codex:0.1.6-dist-tests";
public const string ApiPortTag = "codex_api_port";
public const string ListenPortTag = "codex_listen_port";
public const string MetricsPortTag = "codex_metrics_port";
+5
View File
@@ -244,6 +244,11 @@ namespace CodexPlugin
Version = debugInfo.Version;
}
public override string ToString()
{
return $"CodexNode:{GetName()}";
}
private string[] GetPeerMultiAddresses(CodexNode peer, DebugInfo peerInfo)
{
// The peer we want to connect is in a different pod.
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
+35 -13
View File
@@ -63,6 +63,26 @@ namespace CodexPlugin
};
}
public StorageAvailability[] Map(ICollection<SalesAvailabilityREAD> availabilities)
{
return availabilities.Select(a => Map(a)).ToArray();
}
public StorageAvailability Map(SalesAvailabilityREAD availability)
{
return new StorageAvailability
(
ToByteSize(availability.TotalSize),
ToTimespan(availability.Duration),
new TestToken(ToBigIng(availability.MinPrice)),
new TestToken(ToBigIng(availability.MaxCollateral))
)
{
Id = availability.Id,
FreeSpace = ToByteSize(availability.FreeSize),
};
}
// TODO: Fix openapi spec for this call.
//public StoragePurchase Map(CodexOpenApi.Purchase purchase)
//{
@@ -105,19 +125,6 @@ namespace CodexPlugin
// };
//}
public StorageAvailability Map(CodexOpenApi.SalesAvailabilityREAD read)
{
return new StorageAvailability(
totalSpace: new ByteSize(Convert.ToInt64(read.TotalSize)),
maxDuration: TimeSpan.FromSeconds(Convert.ToDouble(read.Duration)),
minPriceForTotalSpace: new TestToken(BigInteger.Parse(read.MinPrice)),
maxCollateral: new TestToken(BigInteger.Parse(read.MaxCollateral))
)
{
Id = read.Id
};
}
public CodexSpace Map(Space space)
{
return new CodexSpace
@@ -222,5 +229,20 @@ namespace CodexPlugin
{
return t.TstWei.ToString("D");
}
private BigInteger ToBigIng(string tokens)
{
return BigInteger.Parse(tokens);
}
private TimeSpan ToTimespan(string duration)
{
return TimeSpan.FromSeconds(Convert.ToInt32(duration));
}
private ByteSize ToByteSize(string size)
{
return new ByteSize(Convert.ToInt64(size));
}
}
}
@@ -7,6 +7,7 @@ namespace CodexPlugin
public interface IMarketplaceAccess
{
string MakeStorageAvailable(StorageAvailability availability);
StorageAvailability[] GetAvailabilities();
IStoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase);
}
@@ -61,6 +62,14 @@ namespace CodexPlugin
return response.Id;
}
public StorageAvailability[] GetAvailabilities()
{
var result = codexAccess.GetAvailabilities();
Log($"Got {result.Length} availabilities:");
foreach (var a in result) a.Log(log);
return result;
}
private void Log(string msg)
{
log.Log($"{codexAccess.Container.Containers.Single().Name} {msg}");
@@ -81,6 +90,12 @@ namespace CodexPlugin
throw new NotImplementedException();
}
public StorageAvailability[] GetAvailabilities()
{
Unavailable();
throw new NotImplementedException();
}
private void Unavailable()
{
FrameworkAssert.Fail("Incorrect test setup: Marketplace was not enabled for this group of Codex nodes. Add 'EnableMarketplace(...)' after 'SetupCodexNodes()' to enable it.");
@@ -84,10 +84,11 @@ namespace CodexPlugin
public TimeSpan MaxDuration { get; }
public TestToken MinPriceForTotalSpace { get; }
public TestToken MaxCollateral { get; }
public ByteSize FreeSpace { get; set; } = ByteSize.Zero;
public void Log(ILog log)
{
log.Log($"Making storage available... (" +
log.Log($"Storage Availability: (" +
$"totalSize: {TotalSpace}, " +
$"maxDuration: {Time.FormatDuration(MaxDuration)}, " +
$"minPriceForTotalSpace: {MinPriceForTotalSpace}, " +
+117 -43
View File
@@ -23,6 +23,8 @@ components:
Id:
type: string
description: 32bits identifier encoded in hex-decimal string.
minLength: 66
maxLength: 66
example: 0x...
BigInt:
@@ -81,33 +83,12 @@ components:
id:
$ref: "#/components/schemas/PeerId"
ErasureParameters:
type: object
properties:
totalChunks:
type: integer
PoRParameters:
description: Parameters for Proof of Retrievability
type: object
properties:
u:
type: string
publicKey:
type: string
name:
type: string
Content:
type: object
description: Parameters specifying the content
properties:
cid:
$ref: "#/components/schemas/Cid"
erasure:
$ref: "#/components/schemas/ErasureParameters"
por:
$ref: "#/components/schemas/PoRParameters"
DebugInfo:
type: object
@@ -136,7 +117,7 @@ components:
$ref: "#/components/schemas/Duration"
minPrice:
type: string
description: Minimum price to be paid (in amount of tokens) as decimal string
description: Minimal price paid (in amount of tokens) for the whole hosted request's slot for the request's duration as decimal string
maxCollateral:
type: string
description: Maximum collateral user is willing to pay per filled Slot (in amount of tokens) as decimal string
@@ -168,7 +149,39 @@ components:
$ref: "#/components/schemas/StorageRequest"
slotIndex:
type: string
description: Slot Index as hexadecimal string
description: Slot Index as decimal string
SlotAgent:
type: object
properties:
id:
$ref: "#/components/schemas/SlotId"
slotIndex:
type: string
description: Slot Index as decimal string
requestId:
$ref: "#/components/schemas/Id"
request:
$ref: "#/components/schemas/StorageRequest"
reservation:
$ref: "#/components/schemas/Reservation"
state:
type: string
description: Description of the slot's
enum:
- SaleCancelled
- SaleDownloading
- SaleErrored
- SaleFailed
- SaleFilled
- SaleFilling
- SaleFinished
- SaleIgnored
- SaleInitialProving
- SalePayout
- SalePreparing
- SaleProving
- SaleUnknown
Reservation:
type: object
@@ -183,7 +196,7 @@ components:
$ref: "#/components/schemas/Id"
slotIndex:
type: string
description: Slot Index as hexadecimal string
description: Slot Index as decimal string
StorageRequestCreation:
type: object
@@ -259,6 +272,15 @@ components:
state:
type: string
description: Description of the Request's state
enum:
- cancelled
- error
- failed
- finished
- pending
- started
- submitted
- unknown
error:
type: string
description: If Request failed, then here is presented the error message
@@ -308,15 +330,15 @@ components:
quotaMaxBytes:
type: integer
format: int64
description: "Maximum storage space used by the node"
description: "Maximum storage space (in bytes) available for the node in Codex's local repository."
quotaUsedBytes:
type: integer
format: int64
description: "Amount of storage space currently in use"
description: "Amount of storage space (in bytes) currently used for storing files in Codex's local repository."
quotaReservedBytes:
type: integer
format: int64
description: "Amount of storage space reserved"
description: "Amount of storage reserved (in bytes) in the Codex's local repository for future use when storage requests will be picked up and hosted by the node using node's availabilities. This does not include the storage currently in use."
servers:
- url: "http://localhost:8080/api/codex/v1"
@@ -434,9 +456,35 @@ paths:
"/data/{cid}/network":
get:
summary: "Download a file from the network in a streaming manner. If the file is not available locally, it will be retrieved from other nodes in the network if able."
summary: "Download a file from the network to the local node if it's not available locally. Note: Download is performed async. Call can return before download is completed."
tags: [ Data ]
operationId: downloadNetwork
parameters:
- in: path
name: cid
required: true
schema:
$ref: "#/components/schemas/Cid"
description: "File to be downloaded."
responses:
"200":
description: Manifest information for download that has been started.
content:
application/json:
schema:
$ref: "#/components/schemas/DataItem"
"400":
description: Invalid CID is specified
"404":
description: Failed to download dataset manifest
"500":
description: Well it was bad-bad
"/data/{cid}/network/stream":
get:
summary: "Download a file from the network in a streaming manner. If the file is not available locally, it will be retrieved from other nodes in the network if able."
tags: [ Data ]
operationId: downloadNetworkStream
parameters:
- in: path
name: cid
@@ -459,6 +507,32 @@ paths:
"500":
description: Well it was bad-bad
"/data/{cid}/network/manifest":
get:
summary: "Download only the dataset manifest from the network to the local node if it's not available locally."
tags: [ Data ]
operationId: downloadNetworkManifest
parameters:
- in: path
name: cid
required: true
schema:
$ref: "#/components/schemas/Cid"
description: "File for which the manifest is to be downloaded."
responses:
"200":
description: Manifest information.
content:
application/json:
schema:
$ref: "#/components/schemas/DataItem"
"400":
description: Invalid CID is specified
"404":
description: Failed to download dataset manifest
"500":
description: Well it was bad-bad
"/space":
get:
summary: "Gets a summary of the storage space allocation of the node."
@@ -491,7 +565,7 @@ paths:
$ref: "#/components/schemas/Slot"
"503":
description: Sales are unavailable
description: Persistence is not enabled
"/sales/slots/{slotId}":
get:
@@ -511,7 +585,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/Slot"
$ref: "#/components/schemas/SlotAgent"
"400":
description: Invalid or missing SlotId
@@ -520,13 +594,13 @@ paths:
description: Host is not in an active sale for the slot
"503":
description: Sales are unavailable
description: Persistence is not enabled
"/sales/availability":
get:
summary: "Returns storage that is for sale"
tags: [ Marketplace ]
operationId: getOfferedStorage
operationId: getAvailabilities
responses:
"200":
description: Retrieved storage availabilities of the node
@@ -535,11 +609,11 @@ paths:
schema:
type: array
items:
$ref: "#/components/schemas/SalesAvailability"
$ref: "#/components/schemas/SalesAvailabilityREAD"
"500":
description: Error getting unused availabilities
"503":
description: Sales are unavailable
description: Persistence is not enabled
post:
summary: "Offers storage for sale"
@@ -564,7 +638,7 @@ paths:
"500":
description: Error reserving availability
"503":
description: Sales are unavailable
description: Persistence is not enabled
"/sales/availability/{id}":
patch:
summary: "Updates availability"
@@ -597,10 +671,10 @@ paths:
"500":
description: Error reserving availability
"503":
description: Sales are unavailable
description: Persistence is not enabled
"/sales/availability/{id}/reservations":
patch:
get:
summary: "Get availability's reservations"
description: Return's list of Reservations for ongoing Storage Requests that the node hosts.
operationId: getReservations
@@ -628,7 +702,7 @@ paths:
"500":
description: Error getting reservations
"503":
description: Sales are unavailable
description: Persistence is not enabled
"/storage/request/{cid}":
post:
@@ -659,7 +733,7 @@ paths:
"404":
description: Request ID not found
"503":
description: Purchasing is unavailable
description: Persistence is not enabled
"/storage/purchases":
get:
@@ -676,7 +750,7 @@ paths:
items:
type: string
"503":
description: Purchasing is unavailable
description: Persistence is not enabled
"/storage/purchases/{id}":
get:
@@ -702,9 +776,9 @@ paths:
"404":
description: Purchase not found
"503":
description: Purchasing is unavailable
description: Persistence is not enabled
"/node/spr":
"/spr":
get:
summary: "Get Node's SPR"
operationId: getSPR
@@ -722,7 +796,7 @@ paths:
"503":
description: Node SPR not ready, try again later
"/node/peerid":
"/peerid":
get:
summary: "Get Node's PeerID"
operationId: getPeerId
@@ -2,9 +2,13 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\Utils\Utils.csproj" />
</ItemGroup>
</Project>
+4 -26
View File
@@ -1,5 +1,6 @@
using System.Security.Cryptography;
using System.Text;
using Utils;
public static class Program
{
@@ -40,32 +41,9 @@ public static class Program
private static string FindCodexPluginFolder()
{
var current = Directory.GetCurrentDirectory();
while (true)
{
var localFolders = Directory.GetDirectories(current);
var projectPluginsFolders = localFolders.Where(l => l.EndsWith(ProjectPluginsFolderName)).ToArray();
if (projectPluginsFolders.Length == 1)
{
return Path.Combine(projectPluginsFolders.Single(), CodexPluginFolderName);
}
var codexPluginFolders = localFolders.Where(l => l.EndsWith(CodexPluginFolderName)).ToArray();
if (codexPluginFolders.Length == 1)
{
return codexPluginFolders.Single();
}
var parent = Directory.GetParent(current);
if (parent == null)
{
var msg = $"Unable to locate '{CodexPluginFolderName}' folder. Travelled up from: '{Directory.GetCurrentDirectory()}'";
Console.WriteLine(msg);
throw new Exception(msg);
}
current = parent.FullName;
}
var folder = Path.Combine(PluginPathUtils.ProjectPluginsDir, "CodexPlugin");
if (!Directory.Exists(folder)) throw new Exception("CodexPlugin folder not found. Expected: " + folder);
return folder;
}
private static string CreateHash(string openApiFile)
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
@@ -28,7 +28,13 @@
public override string ToString()
{
return $"{Eth} Eth";
var weiOnly = Wei % TokensIntExtensions.WeiPerEth;
var tokens = new List<string>();
if (Eth > 0) tokens.Add($"{Eth} Eth");
if (weiOnly > 0) tokens.Add($"{weiOnly} Wei");
return string.Join(" + ", tokens);
}
}
+1 -1
View File
@@ -57,7 +57,7 @@ namespace GethPlugin
protected override NethereumInteraction StartInteraction()
{
var address = StartResult.Container.GetAddress(log, GethContainerRecipe.HttpPortTag);
var address = StartResult.Container.GetAddress(GethContainerRecipe.HttpPortTag);
var account = StartResult.Account;
var creator = new NethereumInteractionCreator(log, address.Host, address.Port, account.PrivateKey);
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
+1 -1
View File
@@ -15,7 +15,7 @@ namespace MetricsPlugin
{
RunningContainer = runningContainer;
log = tools.GetLog();
var address = RunningContainer.GetAddress(log, PrometheusContainerRecipe.PortTag);
var address = RunningContainer.GetAddress(PrometheusContainerRecipe.PortTag);
endpoint = tools
.CreateHttp(address.ToString())
.CreateEndpoint(address, "/api/v1/");
@@ -80,7 +80,7 @@ namespace MetricsPlugin
{
public static string FormatTarget(ILog log, IMetricsScrapeTarget target)
{
var a = target.Container.GetAddress(log, target.MetricsPortTag);
var a = target.Container.GetAddress(target.MetricsPortTag);
var host = a.Host.Replace("http://", "").Replace("https://", "");
return $"{host}:{a.Port}";
}
+1 -1
View File
@@ -2,7 +2,7 @@
This project allows you to write tools and tests that control and interact with container-based applications to form a distributed system in a controlled, reproducible environment.
Dotnet: v7.0
Dotnet: v8.0
Kubernetes: v1.25.4
Dotnet-kubernetes SDK: v10.1.4 https://github.com/kubernetes-client/csharp
Nethereum: v4.14.0
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
+1 -1
View File
@@ -98,7 +98,7 @@ namespace ContinuousTests
{
cancelToken.ThrowIfCancellationRequested();
var address = n.Container.GetAddress(log, CodexContainerRecipe.ApiPortTag);
var address = n.Container.GetAddress(CodexContainerRecipe.ApiPortTag);
log.Log($"Checking {n.Container.Name} @ '{address}'...");
if (EnsureOnline(log, n))
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
+23 -5
View File
@@ -1,28 +1,46 @@
using CodexPlugin;
using DistTestCore;
using NUnit.Framework;
namespace CodexTests
{
public class AutoBootstrapDistTest : CodexDistTest
{
private readonly Dictionary<TestLifecycle, ICodexNode> bootstrapNodes = new Dictionary<TestLifecycle, ICodexNode>();
[SetUp]
public void SetUpBootstrapNode()
{
BootstrapNode = StartCodex(s => s.WithName("BOOTSTRAP"));
var tl = Get();
if (!bootstrapNodes.ContainsKey(tl))
{
bootstrapNodes.Add(tl, StartCodex(s => s.WithName("BOOTSTRAP_" + tl.TestNamespace)));
}
}
[TearDown]
public void TearDownBootstrapNode()
{
BootstrapNode = null;
bootstrapNodes.Remove(Get());
}
protected override void OnCodexSetup(ICodexSetup setup)
{
if (BootstrapNode != null) setup.WithBootstrapNode(BootstrapNode);
var node = BootstrapNode;
if (node != null) setup.WithBootstrapNode(node);
}
protected ICodexNode? BootstrapNode { get; private set; }
protected ICodexNode? BootstrapNode
{
get
{
var tl = Get();
if (bootstrapNodes.TryGetValue(tl, out var node))
{
return node;
}
return null;
}
}
}
}
@@ -97,6 +97,12 @@ namespace CodexTests.BasicTests
purchaseContract.WaitForStorageContractStarted();
var availabilities = hosts.Select(h => h.Marketplace.GetAvailabilities()).ToArray();
if (availabilities.All(h => h.All(a => a.FreeSpace.SizeInBytes == a.TotalSpace.SizeInBytes)))
{
Assert.Fail("Host availabilities were not used.");
}
var request = GetOnChainStorageRequest(contracts, geth);
AssertStorageRequest(request, purchase, contracts, client);
AssertContractSlot(contracts, request, 0);
@@ -107,47 +113,6 @@ namespace CodexTests.BasicTests
Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Finished));
}
[Test]
[Ignore("Integrated into MarketplaceExample to speed up testing.")]
public void CanDownloadContentFromContractCid()
{
var fileSize = 10.MB();
var geth = Ci.StartGethNode(s => s.IsMiner().WithName("disttest-geth"));
var contracts = Ci.StartCodexContracts(geth);
var testFile = CreateFile(fileSize);
var client = StartCodex(s => s
.WithName("Client")
.EnableMarketplace(geth, contracts, m => m
.WithInitial(10.Eth(), 10.Tst())));
var uploadCid = client.UploadFile(testFile);
var purchase = new StoragePurchaseRequest(uploadCid)
{
PricePerSlotPerSecond = 2.TstWei(),
RequiredCollateral = 10.TstWei(),
MinRequiredNumberOfNodes = 5,
NodeFailureTolerance = 2,
ProofProbability = 5,
Duration = TimeSpan.FromMinutes(5),
Expiry = TimeSpan.FromMinutes(4)
};
var purchaseContract = client.Marketplace.RequestStorage(purchase);
var contractCid = purchaseContract.ContentId;
Assert.That(uploadCid.Id, Is.Not.EqualTo(contractCid.Id));
// Download both from client.
testFile.AssertIsEqual(client.DownloadContent(uploadCid));
testFile.AssertIsEqual(client.DownloadContent(contractCid));
// Download both from another node.
var downloader = StartCodex(s => s.WithName("Downloader"));
testFile.AssertIsEqual(downloader.DownloadContent(uploadCid));
testFile.AssertIsEqual(downloader.DownloadContent(contractCid));
}
private TrackedFile CreateFile(ByteSize fileSize)
{
var segmentSize = new ByteSize(fileSize.SizeInBytes / 4);
+38 -3
View File
@@ -1,5 +1,7 @@
using CodexPlugin;
using FileUtils;
using NUnit.Framework;
using System.Diagnostics;
using Utils;
namespace CodexTests.BasicTests
@@ -10,11 +12,44 @@ namespace CodexTests.BasicTests
[Test]
public void OneClientTest()
{
var primary = StartCodex();
var node = StartCodex();
PerformOneClientTest(primary);
PerformOneClientTest(node);
LogNodeStatus(primary);
LogNodeStatus(node);
}
[Test]
public void InterruptUploadTest()
{
var nodes = StartCodex(10);
var tasks = nodes.Select(n => Task<bool>.Run(() => RunInterruptUploadTest(n)));
Task.WaitAll(tasks.ToArray());
Assert.That(tasks.Select(t => t.Result).All(r => r == true));
}
private bool RunInterruptUploadTest(ICodexNode node)
{
var file = GenerateTestFile(300.MB());
var process = StartCurlUploadProcess(node, file);
Thread.Sleep(500);
process.Kill();
Thread.Sleep(1000);
var log = Ci.DownloadLog(node);
return !log.GetLinesContaining("Unhandled exception in async proc, aborting").Any();
}
private Process StartCurlUploadProcess(ICodexNode node, TrackedFile file)
{
var apiAddress = node.Container.GetAddress(CodexContainerRecipe.ApiPortTag);
var codexUrl = $"{apiAddress}/api/codex/v1/data";
var filePath = file.Filename;
return Process.Start("curl", $"-X POST {codexUrl} -H \"Content-Type: application/octet-stream\" -T {filePath}");
}
private void PerformOneClientTest(ICodexNode primary)
+1 -1
View File
@@ -17,7 +17,7 @@ namespace CodexTests.BasicTests
public void PyramidTest()
{
var size = 5.MB();
var numberOfLayers = 4;
var numberOfLayers = 3;
var bottomLayer = StartLayers(numberOfLayers);
@@ -22,24 +22,6 @@ namespace CodexTests.BasicTests
testFile.AssertIsEqual(downloadedFile);
}
[Test]
[CreateTranscript(nameof(SwarmTest))]
public void SwarmTest()
{
var uploader = StartCodex(s => s.WithName("uploader"));
var downloaders = StartCodex(5, s => s.WithName("downloader"));
var file = GenerateTestFile(100.MB());
var cid = uploader.UploadFile(file);
var result = Parallel.ForEach(downloaders, d =>
{
d.DownloadContent(cid);
});
Assert.That(result.IsCompleted);
}
[Test]
public void DownloadingUnknownCidDoesNotCauseCrash()
{
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
@@ -0,0 +1,228 @@
using CodexPlugin;
using FileUtils;
using Logging;
using NUnit.Framework;
using Utils;
namespace CodexTests.DownloadConnectivityTests
{
[TestFixture]
public class MultiswarmTests : AutoBootstrapDistTest
{
[Test]
[Combinatorial]
public void Multiswarm(
[Values(3, 5)] int numFiles,
[Values(5, 20)] int fileSizeMb,
[Values(1)] int uploadersPerFile,
[Values(3)] int downloadersPerFile,
[Values(1)] int maxUploadsPerNode,
[Values(2, 3)] int maxDownloadsPerNode
)
{
var plan = CreateThePlan(numFiles, uploadersPerFile, downloadersPerFile, maxUploadsPerNode, maxDownloadsPerNode);
Assert.That(plan.NodePlans.Count, Is.LessThan(30));
RunThePlan(plan, fileSizeMb);
}
private void RunThePlan(Plan plan, int fileSizeMb)
{
foreach (var filePlan in plan.FilePlans) filePlan.File = GenerateTestFile(fileSizeMb.MB());
var nodes = StartCodex(plan.NodePlans.Count);
for (int i = 0; i < plan.NodePlans.Count; i++) plan.NodePlans[i].Node = nodes[i];
// Upload all files to their nodes.
foreach (var filePlan in plan.FilePlans)
{
foreach (var uploader in filePlan.Uploaders)
{
filePlan.Cid = uploader.Node!.UploadFile(filePlan.File!);
}
}
Thread.Sleep(5000); // Everything is processed and announced.
// Start all downloads (almost) simultaneously.
var tasks = new List<Task>();
foreach (var filePlan in plan.FilePlans)
{
foreach (var downloader in filePlan.Downloaders)
{
tasks.Add(Task.Run(() =>
{
var downloadedFile = downloader.Node!.DownloadContent(filePlan.Cid!);
lock (filePlan.DownloadedFiles)
{
filePlan.DownloadedFiles.Add(downloadedFile);
}
}));
}
}
Task.WaitAll(tasks.ToArray());
// Assert all files are correct.
foreach (var filePlan in plan.FilePlans)
{
foreach (var downloadedFile in filePlan.DownloadedFiles)
{
filePlan.File!.AssertIsEqual(downloadedFile);
}
}
}
private Plan CreateThePlan(int numFiles, int uploadersPerFile, int downloadersPerFile, int maxUploadsPerNode, int maxDownloadsPerNode)
{
var plan = new Plan(numFiles, uploadersPerFile, downloadersPerFile, maxUploadsPerNode, maxDownloadsPerNode);
plan.Initialize();
plan.LogPlan(GetTestLog());
return plan;
}
}
public class FilePlan
{
public FilePlan(int number)
{
Number = number;
}
public int Number { get; }
public TrackedFile? File { get; set; }
public ContentId? Cid { get; set; }
public List<TrackedFile?> DownloadedFiles { get; } = new List<TrackedFile?>();
public List<NodePlan> Uploaders { get; } = new List<NodePlan>();
public List<NodePlan> Downloaders { get; } = new List<NodePlan>();
public override string ToString()
{
return $"FilePlan[{Number}] " +
$"Uploaders:[{string.Join(",", Uploaders.Select(u => u.Number.ToString()))}] " +
$"Downloaders:[{string.Join(",", Downloaders.Select(u => u.Number.ToString()))}]";
}
}
public class NodePlan
{
public NodePlan(int number)
{
Number = number;
}
public int Number { get; }
public ICodexNode? Node { get; set; }
public List<FilePlan> Uploads { get; } = new List<FilePlan>();
public List<FilePlan> Downloads { get; } = new List<FilePlan>();
public bool Contains(FilePlan plan)
{
return Uploads.Contains(plan) || Downloads.Contains(plan);
}
public override string ToString()
{
return $"NodePlan[{Number}] " +
$"Uploads:[{string.Join(",", Uploads.Select(u => u.Number.ToString()))}] " +
$"Downloads:[{string.Join(",", Downloads.Select(u => u.Number.ToString()))}]";
}
}
public class Plan
{
private readonly int numFiles;
private readonly int uploadersPerFile;
private readonly int downloadersPerFile;
private readonly int maxUploadsPerNode;
private readonly int maxDownloadsPerNode;
public Plan(int numFiles, int uploadersPerFile, int downloadersPerFile, int maxUploadsPerNode, int maxDownloadsPerNode)
{
this.numFiles = numFiles;
this.uploadersPerFile = uploadersPerFile;
this.downloadersPerFile = downloadersPerFile;
this.maxUploadsPerNode = maxUploadsPerNode;
this.maxDownloadsPerNode = maxDownloadsPerNode;
}
public List<FilePlan> FilePlans { get; } = new List<FilePlan>();
public List<NodePlan> NodePlans { get; } = new List<NodePlan>();
public void Initialize()
{
for (int i = 0; i < numFiles; i++) FilePlans.Add(new FilePlan(i));
foreach (var filePlan in FilePlans)
{
while (filePlan.Uploaders.Count < uploadersPerFile) AddUploader(filePlan);
while (filePlan.Downloaders.Count < downloadersPerFile) AddDownloader(filePlan);
}
CollectionAssert.AllItemsAreUnique(FilePlans.Select(f => f.Number));
CollectionAssert.AllItemsAreUnique(NodePlans.Select(f => f.Number));
foreach (var filePlan in FilePlans)
{
Assert.That(filePlan.Uploaders.Count, Is.EqualTo(uploadersPerFile));
Assert.That(filePlan.Downloaders.Count, Is.EqualTo(downloadersPerFile));
}
foreach (var nodePlan in NodePlans)
{
Assert.That(nodePlan.Uploads.Count, Is.LessThanOrEqualTo(maxUploadsPerNode));
Assert.That(nodePlan.Downloads.Count, Is.LessThanOrEqualTo(maxDownloadsPerNode));
}
}
public void LogPlan(ILog log)
{
log.Log("The plan:");
log.Log("Input:");
log.Log($"numFiles: {numFiles}");
log.Log($"uploadersPerFile: {uploadersPerFile}");
log.Log($"downloadersPerFile: {downloadersPerFile}");
log.Log($"maxUploadsPerNode: {maxUploadsPerNode}");
log.Log($"maxDownloadsPerNode: {maxDownloadsPerNode}");
log.Log("Setup:");
log.Log($"number of nodes: {NodePlans.Count}");
foreach (var filePlan in FilePlans) log.Log(filePlan.ToString());
foreach (var nodePlan in NodePlans) log.Log(nodePlan.ToString());
}
private void AddDownloader(FilePlan filePlan)
{
var nodePlan = GetOrCreateDownloaderNode(filePlan);
filePlan.Downloaders.Add(nodePlan);
nodePlan.Downloads.Add(filePlan);
}
private void AddUploader(FilePlan filePlan)
{
var nodePlan = GetOrCreateUploaderNode(filePlan);
filePlan.Uploaders.Add(nodePlan);
nodePlan.Uploads.Add(filePlan);
}
private NodePlan GetOrCreateDownloaderNode(FilePlan notIn)
{
var available = NodePlans.Where(n =>
n.Downloads.Count < maxDownloadsPerNode && !n.Contains(notIn)
).ToArray();
if (available.Any()) return RandomUtils.GetOneRandom(available);
var newNodePlan = new NodePlan(NodePlans.Count);
NodePlans.Add(newNodePlan);
return newNodePlan;
}
private NodePlan GetOrCreateUploaderNode(FilePlan notIn)
{
var available = NodePlans.Where(n =>
n.Uploads.Count < maxUploadsPerNode && !n.Contains(notIn)
).ToArray();
if (available.Any()) return RandomUtils.GetOneRandom(available);
var newNodePlan = new NodePlan(NodePlans.Count);
NodePlans.Add(newNodePlan);
return newNodePlan;
}
}
}
@@ -0,0 +1,25 @@
using NUnit.Framework;
using Utils;
namespace CodexTests.DownloadConnectivityTests
{
[TestFixture]
public class SwarmTests : AutoBootstrapDistTest
{
[Test]
[Combinatorial]
[CreateTranscript("swarm_retransmit")]
public void DetectBlockRetransmits(
[Values(1, 5, 10, 20)] int fileSize,
[Values(3, 5, 10, 20)] int numNodes
)
{
var nodes = StartCodex(numNodes);
var file = GenerateTestFile(fileSize.MB());
var cid = nodes[0].UploadFile(file);
var tasks = nodes.Select(n => Task.Run(() => n.DownloadContent(cid))).ToArray();
Task.WaitAll(tasks);
}
}
}
@@ -23,7 +23,7 @@ namespace CodexTests.UtilityTests
private readonly List<EthAccount> hostAccounts = new List<EthAccount>();
private readonly List<ulong> rewardsSeen = new List<ulong>();
private readonly TimeSpan rewarderInterval = TimeSpan.FromMinutes(1);
private readonly List<string> receivedEvents = new List<string>();
private readonly List<ChainEventMessage> receivedEvents = new List<ChainEventMessage>();
[Test]
[DontDownloadLogs]
@@ -73,13 +73,18 @@ namespace CodexTests.UtilityTests
private void AssertEventOccurance(string msg, int expectedCount)
{
Assert.That(receivedEvents.Count(e => e.Contains(msg)), Is.EqualTo(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)
{
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>DistTestCore</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
+2
View File
@@ -22,6 +22,7 @@ namespace DistTestCore
Log = log;
Configuration = configuration;
TimeSet = timeSet;
TestNamespace = testNamespace;
TestStart = DateTime.UtcNow;
entryPoint = new EntryPoint(log, configuration.GetK8sConfiguration(timeSet, this, testNamespace), configuration.GetFileManagerFolder(), timeSet);
@@ -36,6 +37,7 @@ namespace DistTestCore
public TestLog Log { get; }
public Configuration Configuration { get; }
public ITimeSet TimeSet { get; }
public string TestNamespace { get; }
public bool WaitForCleanup { get; }
public CoreInterface CoreInterface { get; }
+2 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
@@ -18,6 +18,7 @@
<ProjectReference Include="..\..\Framework\OverwatchTranscript\OverwatchTranscript.csproj" />
<ProjectReference Include="..\..\Framework\Utils\Utils.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexContractsPlugin\CodexContractsPlugin.csproj" />
<ProjectReference Include="..\..\Tools\TestNetRewarder\TestNetRewarder.csproj" />
</ItemGroup>
</Project>
@@ -19,11 +19,20 @@ namespace FrameworkTests.NethereumWorkflow
{
var start = DateTime.UtcNow.AddDays(-1).AddSeconds(-30);
blocks = new Dictionary<ulong, Block>();
Block? prev = null;
for (ulong i = 0; i < 30; i++)
{
ulong d = 100 + i;
blocks.Add(d, new Block(d, start + TimeSpan.FromSeconds(i * 2)));
var newBlock = new Block(d, start + TimeSpan.FromSeconds(i * 2));
blocks.Add(d, newBlock);
if (prev != null)
{
prev.Next = newBlock;
newBlock.Previous = prev;
}
prev = newBlock;
}
}
@@ -99,23 +108,23 @@ namespace FrameworkTests.NethereumWorkflow
}
[Test]
public void FailsToFindBlockBeforeFrontOfChain()
public void FindsGenesisBlockAtFrontOfChain()
{
var first = blocks.First().Value;
var notFound = finder.GetHighestBlockNumberBefore(first.Time);
var firstNumber = finder.GetHighestBlockNumberBefore(first.Time);
Assert.That(notFound, Is.Null);
Assert.That(firstNumber, Is.EqualTo(first.Number));
}
[Test]
public void FailsToFindBlockAfterTailOfChain()
public void FindsCurrentBlockAtTailOfChain()
{
var last = blocks.Last().Value;
var notFound = finder.GetLowestBlockNumberAfter(last.Time);
var lastNumber = finder.GetLowestBlockNumberAfter(last.Time);
Assert.That(notFound, Is.Null);
Assert.That(lastNumber, Is.EqualTo(last.Number));
}
[Test]
@@ -143,13 +152,27 @@ namespace FrameworkTests.NethereumWorkflow
{
foreach (var pair in blocks)
{
finder.GetHighestBlockNumberBefore(pair.Value.JustBefore);
finder.GetHighestBlockNumberBefore(pair.Value.Time);
finder.GetHighestBlockNumberBefore(pair.Value.JustAfter);
var block = pair.Value;
finder.GetLowestBlockNumberAfter(pair.Value.JustBefore);
finder.GetLowestBlockNumberAfter(pair.Value.Time);
finder.GetLowestBlockNumberAfter(pair.Value.JustAfter);
AssertLink(block.Previous, finder.GetHighestBlockNumberBefore(block.JustBefore));
AssertLink(block, finder.GetHighestBlockNumberBefore(block.Time));
AssertLink(block, finder.GetHighestBlockNumberBefore(block.JustAfter));
AssertLink(block, finder.GetLowestBlockNumberAfter(block.JustBefore));
AssertLink(block, finder.GetLowestBlockNumberAfter(block.Time));
AssertLink(block.Next, finder.GetLowestBlockNumberAfter(block.JustAfter));
}
}
private void AssertLink(Block? expected, ulong? actual)
{
if (expected == null)
{
Assert.That(actual, Is.Null);
}
else
{
Assert.That(expected.Number, Is.EqualTo(actual!.Value));
}
}
}
@@ -167,6 +190,9 @@ namespace FrameworkTests.NethereumWorkflow
public DateTime JustBefore { get { return Time.AddSeconds(-1); } }
public DateTime JustAfter { get { return Time.AddSeconds(1); } }
public Block? Next { get; set; }
public Block? Previous { get; set; }
public override string ToString()
{
return $"[{Number}]";
@@ -1,135 +0,0 @@
using Logging;
using NUnit.Framework;
using OverwatchTranscript;
namespace FrameworkTests.OverwatchTranscript
{
[TestFixture]
public class TranscriptTests
{
private const string TranscriptFilename = "testtranscript.owts";
private const string HeaderKey = "testHeader";
private const string HeaderData = "abcdef";
private const string EventData0 = "12345";
private const string EventData1 = "678";
private const string EventData2 = "90";
private const string EventData3 = "-=";
private readonly DateTime t0 = DateTime.UtcNow;
private readonly DateTime t1 = DateTime.UtcNow.AddMinutes(1);
private readonly DateTime t2 = DateTime.UtcNow.AddMinutes(3);
[Test]
public void WriteAndRun()
{
WriteTranscript();
ReadTranscript();
File.Delete(TranscriptFilename);
}
private void WriteTranscript()
{
var log = new ConsoleLog();
var writer = Transcript.NewWriter(log);
writer.AddHeader(HeaderKey, new TestHeader
{
HeaderData = HeaderData
});
writer.Add(t0, new MyEvent
{
EventData = EventData0
});
writer.Add(t2, new MyEvent
{
EventData = EventData3
});
writer.Add(t1, new MyEvent
{
EventData = EventData1
});
writer.Add(t1, new MyEvent
{
EventData = EventData2
});
if (File.Exists(TranscriptFilename)) File.Delete(TranscriptFilename);
writer.Write(TranscriptFilename);
}
private void ReadTranscript()
{
var reader = Transcript.NewReader(TranscriptFilename);
var header = reader.GetHeader<TestHeader>(HeaderKey);
Assert.That(header.HeaderData, Is.EqualTo(HeaderData));
Assert.That(reader.Header.NumberOfMoments, Is.EqualTo(3));
Assert.That(reader.Header.NumberOfEvents, Is.EqualTo(4));
Assert.That(reader.Header.EarliestUtc, Is.EqualTo(t0));
Assert.That(reader.Header.LatestUtc, Is.EqualTo(t2));
var moments = new List<ActivateMoment>();
var events = new List<ActivateEvent<MyEvent>>();
reader.AddMomentHandler(moments.Add);
reader.AddEventHandler<MyEvent>(events.Add);
Assert.That(moments.Count, Is.EqualTo(0));
Assert.That(events.Count, Is.EqualTo(0));
reader.Next();
Assert.That(moments.Count, Is.EqualTo(1));
Assert.That(events.Count, Is.EqualTo(1));
reader.Next();
Assert.That(moments.Count, Is.EqualTo(2));
Assert.That(events.Count, Is.EqualTo(3));
reader.Next();
Assert.That(moments.Count, Is.EqualTo(3));
Assert.That(events.Count, Is.EqualTo(4));
reader.Next();
Assert.That(moments.Count, Is.EqualTo(3));
Assert.That(events.Count, Is.EqualTo(4));
AssertMoment(moments[0], utc: t0, duration: t1 - t0, index: 0);
AssertMoment(moments[1], utc: t1, duration: t2 - t1, index: 1);
AssertMoment(moments[2], utc: t2, duration: null, index: 2);
AssertEvent(events[0], utc: t0, duration: t1 - t0, index: 0, data: EventData0);
AssertEvent(events[1], utc: t1, duration: t2 - t1, index: 1, data: EventData1);
AssertEvent(events[2], utc: t1, duration: t2 - t1, index: 1, data: EventData2);
AssertEvent(events[3], utc: t2, duration: null, index: 2, data: EventData3);
reader.Close();
}
private void AssertMoment(ActivateMoment m, DateTime utc, TimeSpan? duration, int index)
{
Assert.That(m.Utc, Is.EqualTo(utc));
Assert.That(m.Duration, Is.EqualTo(duration));
Assert.That(m.Index, Is.EqualTo(index));
}
private void AssertEvent(ActivateEvent<MyEvent> e, DateTime utc, TimeSpan? duration, int index, string data)
{
Assert.That(e.Moment.Utc, Is.EqualTo(utc));
Assert.That(e.Moment.Duration, Is.EqualTo(duration));
Assert.That(e.Moment.Index, Is.EqualTo(index));
Assert.That(e.Payload.EventData, Is.EqualTo(data));
}
}
public class TestHeader
{
public string HeaderData { get; set; } = string.Empty;
}
public class MyEvent
{
public string EventData { get; set; } = string.Empty;
}
}
@@ -2,7 +2,7 @@
using NUnit.Framework;
using OverwatchTranscript;
namespace FrameworkTests.OverwatchTranscript
namespace FrameworkTests.OverwatchTranscriptTests
{
[TestFixture]
public class TranscriptLargeTests
@@ -0,0 +1,229 @@
using Logging;
using Newtonsoft.Json;
using NUnit.Framework;
using OverwatchTranscript;
using System.IO.Compression;
namespace FrameworkTests.OverwatchTranscriptTests
{
[TestFixture]
public class TranscriptTests
{
private const string TranscriptFilename = "testtranscript.owts";
private const string HeaderKey = "testHeader";
private const string HeaderData = "abcdef";
private const string EventData0 = "12345";
private const string EventData1 = "678";
private const string EventData2 = "90";
private const string EventData3 = "-=";
private readonly DateTime t0 = DateTime.UtcNow;
private readonly DateTime t1 = DateTime.UtcNow.AddMinutes(1);
private readonly DateTime t2 = DateTime.UtcNow.AddMinutes(3);
[Test]
[Combinatorial]
public void WriteAndRun()
{
WriteTranscript();
AssertFileContent();
ReadTranscript();
File.Delete(TranscriptFilename);
}
private void WriteTranscript()
{
var log = new ConsoleLog();
var writer = Transcript.NewWriter(log);
writer.AddHeader(HeaderKey, new TestHeader
{
HeaderData = HeaderData
});
writer.Add(t0, new MyEvent
{
EventData = EventData0
});
writer.Add(t2, new MyEvent
{
EventData = EventData3
});
writer.Add(t1, new MyEvent
{
EventData = EventData1
});
writer.Add(t1, new MyEvent
{
EventData = EventData2
});
if (File.Exists(TranscriptFilename)) File.Delete(TranscriptFilename);
writer.Write(TranscriptFilename);
}
private void ReadTranscript()
{
var reader = Transcript.NewReader(TranscriptFilename);
var header = reader.GetHeader<TestHeader>(HeaderKey);
Assert.That(header.HeaderData, Is.EqualTo(HeaderData));
Assert.That(reader.Header.NumberOfMoments, Is.EqualTo(3));
Assert.That(reader.Header.NumberOfEvents, Is.EqualTo(4));
Assert.That(reader.Header.EarliestUtc, Is.EqualTo(t0));
Assert.That(reader.Header.LatestUtc, Is.EqualTo(t2));
var moments = new List<ActivateMoment>();
var events = new List<ActivateEvent<MyEvent>>();
reader.AddMomentHandler(moments.Add);
reader.AddEventHandler<MyEvent>(events.Add);
var timeout = 10;
while (moments.Count < 3 && events.Count < 4)
{
Thread.Sleep(10);
reader.Next();
timeout--;
if (timeout == 0) Assert.Fail("Events not received.");
}
reader.Next();
Assert.That(moments.Count, Is.EqualTo(3));
Assert.That(events.Count, Is.EqualTo(4));
AssertMoment(moments[0], utc: t0, duration: t1 - t0, index: 0);
AssertMoment(moments[1], utc: t1, duration: t2 - t1, index: 1);
AssertMoment(moments[2], utc: t2, duration: null, index: 2);
AssertEvent(events, utc: t0, duration: t1 - t0, index: 0, data: EventData0);
AssertEvent(events, utc: t1, duration: t2 - t1, index: 1, data: EventData2);
AssertEvent(events, utc: t1, duration: t2 - t1, index: 1, data: EventData1);
AssertEvent(events, utc: t2, duration: null, index: 2, data: EventData3);
reader.Close();
}
private void AssertMoment(ActivateMoment m, DateTime utc, TimeSpan? duration, int index)
{
Assert.That(m.Utc, Is.EqualTo(utc));
Assert.That(m.Duration, Is.EqualTo(duration));
Assert.That(m.Index, Is.EqualTo(index));
}
private void AssertEvent(List<ActivateEvent<MyEvent>> events, DateTime utc, TimeSpan? duration, int index, string data)
{
var e = events.SingleOrDefault(e => e.Moment.Utc == utc && e.Payload.EventData == data);
if (e == null) Assert.Fail("Event not found");
Assert.That(e!.Moment.Utc, Is.EqualTo(utc));
Assert.That(e!.Moment.Duration, Is.EqualTo(duration));
Assert.That(e!.Moment.Index, Is.EqualTo(index));
Assert.That(e!.Payload.EventData, Is.EqualTo(data));
}
private void AssertFileContent()
{
using var zip = ZipFile.OpenRead(TranscriptFilename);
Assert.That(zip.Entries.Count, Is.EqualTo(2));
foreach (var entry in zip.Entries)
{
if (entry.Name == "transcript.json")
{
var transcript = ZipEntryJson<OverwatchTranscript.OverwatchTranscript>(entry);
AssertTranscript(transcript);
}
else
{
var moments = ZipEntryToMoments(entry);
AssertMoments(moments);
}
}
}
private void AssertTranscript(OverwatchTranscript.OverwatchTranscript transcript)
{
Assert.That(transcript.Header.Common.NumberOfMoments, Is.EqualTo(3));
Assert.That(transcript.Header.Common.NumberOfEvents, Is.EqualTo(4));
Assert.That(transcript.Header.Common.EarliestUtc, Is.EqualTo(t0));
Assert.That(transcript.Header.Common.LatestUtc, Is.EqualTo(t2));
Assert.That(transcript.Header.Entries.Length, Is.EqualTo(1));
Assert.That(transcript.Header.Entries[0].Key, Is.EqualTo(HeaderKey));
Assert.That(transcript.Header.Entries[0].Value, Is.EqualTo("{\"HeaderData\":\"abcdef\"}"));
Assert.That(transcript.MomentReferences.Length, Is.EqualTo(1));
Assert.That(transcript.MomentReferences[0].NumberOfMoments, Is.EqualTo(3));
Assert.That(transcript.MomentReferences[0].NumberOfEvents, Is.EqualTo(4));
Assert.That(transcript.MomentReferences[0].EarliestUtc, Is.EqualTo(t0));
Assert.That(transcript.MomentReferences[0].LatestUtc, Is.EqualTo(t2));
}
private void AssertMoments(OverwatchMoment[] moments)
{
Assert.That(moments.Length, Is.EqualTo(3));
Assert.That(moments[0].Utc, Is.EqualTo(t0));
Assert.That(moments[0].Events.Length, Is.EqualTo(1));
Assert.That(moments[0].Events[0].Type, Is.EqualTo("FrameworkTests.OverwatchTranscriptTests.MyEvent"));
Assert.That(moments[0].Events[0].Payload, Is.EqualTo("{\"EventData\":\"12345\"}"));
Assert.That(moments[1].Utc, Is.EqualTo(t1));
Assert.That(moments[1].Events.Length, Is.EqualTo(2));
Assert.That(moments[1].Events[0].Type, Is.EqualTo("FrameworkTests.OverwatchTranscriptTests.MyEvent"));
Assert.That(moments[1].Events[1].Type, Is.EqualTo("FrameworkTests.OverwatchTranscriptTests.MyEvent"));
// output order is not guaranteed:
var payloads = moments[1].Events.Select(e => e.Payload).ToArray();
CollectionAssert.AreEquivalent(new[]
{
"{\"EventData\":\"90\"}",
"{\"EventData\":\"678\"}"
}, payloads);
Assert.That(moments[2].Utc, Is.EqualTo(t2));
Assert.That(moments[2].Events.Length, Is.EqualTo(1));
Assert.That(moments[2].Events[0].Type, Is.EqualTo("FrameworkTests.OverwatchTranscriptTests.MyEvent"));
Assert.That(moments[2].Events[0].Payload, Is.EqualTo("{\"EventData\":\"-=\"}"));
}
private T ZipEntryJson<T>(ZipArchiveEntry? entry)
{
if (entry == null) Assert.Fail("entry is null");
using var stream = entry!.Open();
using var reader = new StreamReader(stream);
var json = reader.ReadToEnd();
var result = JsonConvert.DeserializeObject<T>(json);
if (result == null) Assert.Fail("didn't deserialize");
return result!;
}
private OverwatchMoment[] ZipEntryToMoments(ZipArchiveEntry? entry)
{
var result = new List<OverwatchMoment>();
if (entry == null) Assert.Fail("entry is null");
using var stream = entry!.Open();
using var reader = new StreamReader(stream);
var line = reader.ReadLine();
while (!string.IsNullOrEmpty(line))
{
var moment = JsonConvert.DeserializeObject<OverwatchMoment>(line);
if (moment != null) result.Add(moment);
line = reader.ReadLine();
}
return result.ToArray();
}
}
public class TestHeader
{
public string HeaderData { get; set; } = string.Empty;
}
public class MyEvent
{
public string EventData { get; set; } = string.Empty;
}
}
@@ -0,0 +1,28 @@
using NUnit.Framework;
using System.Text;
using TestNetRewarder;
namespace FrameworkTests.Utils
{
[TestFixture]
public class EmojiMapsTests
{
private readonly Random random = new Random();
private readonly EmojiMaps maps = new EmojiMaps();
[Test]
public void GeneratesConsistentStrings(
[Values(1, 5, 10, 20)] int inputLength,
[Values(1, 2, 3, 5)] int outLength)
{
var buffer = new byte[inputLength];
random.NextBytes(buffer);
var input = Encoding.ASCII.GetString(buffer);
var out1 = maps.StringToEmojis(input, outLength);
var out2 = maps.StringToEmojis(input, outLength);
Assert.That(out1, Is.EqualTo(out2));
}
}
}
@@ -0,0 +1,334 @@
namespace FrameworkTests.Utils
{
public class Run
{
public Run(int start, int length)
{
Start = start;
Length = length;
}
public int Start { get; }
public int Length { get; private set; }
public bool Includes(int index)
{
return index >= Start && index < (Start + Length);
}
public RunUpdate ExpandToInclude(int index)
{
if (Includes(index)) throw new Exception("Run already includes this index. Run: {ToString()} index: {index}");
if (index == (Start + Length))
{
Length++;
return new RunUpdate();
}
if (index == (Start - 1))
{
return new RunUpdate(
newRuns: [new Run(Start - 1, Length + 1)],
removeRuns: [this]
);
}
throw new Exception($"Run cannot expand to include index. Run: {ToString()} index: {index}");
}
public RunUpdate Unset(int index)
{
if (!Includes(index))
{
return new RunUpdate();
}
if (index == Start)
{
// First index: Replace self with new run at next index, unless empty.
if (Length == 1)
{
return new RunUpdate(
newRuns: Array.Empty<Run>(),
removeRuns: [this]
);
}
return new RunUpdate(
newRuns: [new Run(Start + 1, Length - 1)],
removeRuns: [this]
);
}
if (index == (Start + Length - 1))
{
// Last index: Become one smaller.
Length--;
return new RunUpdate();
}
// Split:
var newRunLength = (Start + Length - 1) - index;
Length = index - Start;
return new RunUpdate(
newRuns: [new Run(index + 1, newRunLength)],
removeRuns: Array.Empty<Run>()
);
}
public void Iterate(Action<int> action)
{
for (var i = 0; i < Length; i++)
{
action(Start + i);
}
}
public override string ToString()
{
return $"[{Start},{Length}]";
}
public override bool Equals(object? obj)
{
return obj is Run run &&
Start == run.Start &&
Length == run.Length;
}
public override int GetHashCode()
{
return HashCode.Combine(Start, Length);
}
public static bool operator ==(Run? obj1, Run? obj2)
{
if (ReferenceEquals(obj1, obj2)) return true;
if (ReferenceEquals(obj1, null)) return false;
if (ReferenceEquals(obj2, null)) return false;
return obj1.Equals(obj2);
}
public static bool operator !=(Run? obj1, Run? obj2) => !(obj1 == obj2);
}
public class RunUpdate
{
public RunUpdate()
: this(Array.Empty<Run>(), Array.Empty<Run>())
{
}
public RunUpdate(Run[] newRuns, Run[] removeRuns)
{
NewRuns = newRuns;
RemoveRuns = removeRuns;
}
public Run[] NewRuns { get; }
public Run[] RemoveRuns { get; }
}
public partial class IndexSet
{
private readonly SortedList<int, Run> runs = new SortedList<int, Run>();
public IndexSet()
{
}
public IndexSet(int[] indices)
{
foreach (var i in indices) Set(i);
}
public static IndexSet FromRunLengthEncoded(int[] rle)
{
var set = new IndexSet();
for (var i = 0; i < rle.Length; i += 2)
{
var start = rle[i];
var length = rle[i + 1];
set.runs.Add(start, new Run(start, length));
}
return set;
}
public bool IsSet(int index)
{
if (runs.ContainsKey(index)) return true;
var run = GetRunAt(index);
if (run == null) return false;
return true;
}
public void Set(int index)
{
if (IsSet(index)) return;
var runBefore = GetRunAt(index - 1);
var runAfter = GetRunExact(index + 1);
if (runBefore == null)
{
if (runAfter == null)
{
CreateNewRun(index);
}
else
{
HandleUpdate(runAfter.ExpandToInclude(index));
}
}
else
{
if (runAfter == null)
{
HandleUpdate(runBefore.ExpandToInclude(index));
}
else
{
// new index will connect runBefore with runAfter. We merge!
HandleUpdate(new RunUpdate(
newRuns: [new Run(runBefore.Start, runBefore.Length + 1 + runAfter.Length)],
removeRuns: [runBefore, runAfter]
));
}
}
}
public void Unset(int index)
{
if (runs.ContainsKey(index))
{
HandleUpdate(runs[index].Unset(index));
}
else
{
var run = GetRunAt(index);
if (run == null) return;
HandleUpdate(run.Unset(index));
}
}
public void Iterate(Action<int> onIndex)
{
foreach (var run in runs.Values)
{
run.Iterate(onIndex);
}
}
public int[] RunLengthEncoded()
{
return Encode().ToArray();
}
public override string ToString()
{
return string.Join("&", runs.Select(r => r.ToString()).ToArray());
}
private IEnumerable<int> Encode()
{
foreach (var pair in runs)
{
yield return pair.Value.Start;
yield return pair.Value.Length;
}
}
private Run? GetRunAt(int index)
{
foreach (var run in runs.Values)
{
if (run.Includes(index)) return run;
}
return null;
}
private Run? GetRunExact(int index)
{
if (runs.ContainsKey(index)) return runs[index];
return null;
}
private void HandleUpdate(RunUpdate runUpdate)
{
foreach (var removeRun in runUpdate.RemoveRuns) runs.Remove(removeRun.Start);
foreach (var newRun in runUpdate.NewRuns) runs.Add(newRun.Start, newRun);
}
private void CreateNewRun(int index)
{
if (runs.ContainsKey(index + 1))
{
var length = runs[index + 1].Length + 1;
runs.Add(index, new Run(index, length));
runs.Remove(index + 1);
}
else
{
runs.Add(index, new Run(index, 1));
}
}
}
public partial class IndexSet
{
public IndexSet Overlap(IndexSet other)
{
var result = new IndexSet();
Iterate(i =>
{
if (other.IsSet(i)) result.Set(i);
});
return result;
}
public IndexSet Merge(IndexSet other)
{
var result = new IndexSet();
Iterate(result.Set);
other.Iterate(result.Set);
return result;
}
public IndexSet Without(IndexSet other)
{
var result = new IndexSet();
Iterate(i =>
{
if (!other.IsSet(i)) result.Set(i);
});
return result;
}
public override bool Equals(object? obj)
{
if (obj is IndexSet set)
{
if (set.runs.Count != runs.Count) return false;
foreach (var pair in runs)
{
if (!set.runs.ContainsKey(pair.Key)) return false;
if (set.runs[pair.Key] != pair.Value) return false;
}
return true;
}
return false;
}
public override int GetHashCode()
{
return HashCode.Combine(runs);
}
public static bool operator ==(IndexSet? obj1, IndexSet? obj2)
{
if (ReferenceEquals(obj1, obj2)) return true;
if (ReferenceEquals(obj1, null)) return false;
if (ReferenceEquals(obj2, null)) return false;
return obj1.Equals(obj2);
}
public static bool operator !=(IndexSet? obj1, IndexSet? obj2) => !(obj1 == obj2);
}
}
@@ -0,0 +1,94 @@
using NUnit.Framework;
namespace FrameworkTests.Utils
{
[TestFixture]
public class RunLengthEncodingLogicalTests
{
[Test]
public void EqualityTest()
{
var setA = new IndexSet([1, 2, 3, 4]);
var setB = new IndexSet([1, 2, 3, 4]);
Assert.That(setA, Is.EqualTo(setB));
Assert.That(setA == setB);
}
[Test]
public void InequalityTest1()
{
var setA = new IndexSet([1, 2, 4, 5]);
var setB = new IndexSet([1, 2, 3, 4]);
Assert.That(setA, Is.Not.EqualTo(setB));
Assert.That(setA != setB);
}
[Test]
public void InequalityTest2()
{
var setA = new IndexSet([1, 2, 3]);
var setB = new IndexSet([1, 2, 3, 4]);
Assert.That(setA, Is.Not.EqualTo(setB));
Assert.That(setA != setB);
}
[Test]
public void InequalityTest3()
{
var setA = new IndexSet([2, 3, 4, 5]);
var setB = new IndexSet([1, 2, 3, 4]);
Assert.That(setA, Is.Not.EqualTo(setB));
Assert.That(setA != setB);
}
[Test]
public void InequalityTest()
{
var setA = new IndexSet([2, 3, 4]);
var setB = new IndexSet([1, 2, 3, 4]);
Assert.That(setA, Is.Not.EqualTo(setB));
Assert.That(setA != setB);
}
[Test]
public void Overlap()
{
var setA = new IndexSet([1, 2, 3, 4, 5, 11, 14]);
var setB = new IndexSet([3, 4, 5, 6, 7, 11, 12, 13]);
var expectedSet = new IndexSet([3, 4, 5, 11]);
var set = setA.Overlap(setB);
Assert.That(set, Is.EqualTo(expectedSet));
}
[Test]
public void Merge()
{
var setA = new IndexSet([1, 2, 3, 4, 5, 11, 14]);
var setB = new IndexSet([3, 4, 5, 6, 7, 11, 12, 13]);
var expectedSet = new IndexSet([1, 2, 3, 4, 5, 6, 7, 11, 12, 13, 14]);
var set = setA.Merge(setB);
Assert.That(set, Is.EqualTo(expectedSet));
}
[Test]
public void Without()
{
var setA = new IndexSet([1, 2, 3, 4, 5, 11, 14]);
var setB = new IndexSet([3, 4, 5, 6, 7, 11, 12, 13]);
var expectedSet = new IndexSet([1, 2, 14]);
var set = setA.Without(setB);
Assert.That(set, Is.EqualTo(expectedSet));
}
}
}
@@ -1,12 +1,40 @@
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using static FrameworkTests.Utils.RunLengthEncodingTests;
namespace FrameworkTests.Utils
{
[TestFixture]
public class RunLengthEncodingRunTests
{
[Test]
public void EqualityTest()
{
var runA = new Run(1, 4);
var runB = new Run(1, 4);
Assert.That(runA, Is.EqualTo(runB));
Assert.That(runA == runB);
}
[Test]
public void InequalityTest1()
{
var runA = new Run(1, 4);
var runB = new Run(1, 5);
Assert.That(runA, Is.Not.EqualTo(runB));
Assert.That(runA != runB);
}
[Test]
public void InequalityTest2()
{
var runA = new Run(1, 4);
var runB = new Run(2, 4);
Assert.That(runA, Is.Not.EqualTo(runB));
Assert.That(runA != runB);
}
[Test]
[Combinatorial]
public void RunIncludes(
@@ -33,23 +61,58 @@ namespace FrameworkTests.Utils
}
[Test]
public void RunExpandToInclude()
public void RunExpandThrowsWhenIndexNotAdjacent()
{
var run = new Run(2, 3);
Assert.That(!run.Includes(1));
Assert.That(run.Includes(2));
Assert.That(run.Includes(4));
Assert.That(!run.Includes(5));
Assert.That(run.ExpandToInclude(1), Is.False);
Assert.That(run.ExpandToInclude(2), Is.False);
Assert.That(run.ExpandToInclude(4), Is.False);
Assert.That(run.ExpandToInclude(6), Is.False);
Assert.That(() => run.ExpandToInclude(0), Throws.TypeOf<Exception>());
Assert.That(() => run.ExpandToInclude(6), Throws.TypeOf<Exception>());
}
Assert.That(run.ExpandToInclude(5), Is.True);
[Test]
public void RunExpandThrowsWhenIndexAlreadyIncluded()
{
var run = new Run(2, 3);
Assert.That(!run.Includes(1));
Assert.That(run.Includes(2));
Assert.That(run.Includes(4));
Assert.That(!run.Includes(5));
Assert.That(() => run.ExpandToInclude(2), Throws.TypeOf<Exception>());
Assert.That(() => run.ExpandToInclude(3), Throws.TypeOf<Exception>());
}
[Test]
public void RunExpandToIncludeAfter()
{
var run = new Run(2, 3);
var update = run.ExpandToInclude(5);
Assert.That(update, Is.Not.Null);
Assert.That(update.NewRuns.Length, Is.EqualTo(0));
Assert.That(update.RemoveRuns.Length, Is.EqualTo(0));
Assert.That(run.Includes(5));
Assert.That(!run.Includes(6));
}
[Test]
public void RunExpandToIncludeBefore()
{
var run = new Run(2, 3);
var update = run.ExpandToInclude(1);
Assert.That(update, Is.Not.Null);
Assert.That(update.NewRuns.Length, Is.EqualTo(1));
Assert.That(update.RemoveRuns.Length, Is.EqualTo(1));
Assert.That(update.RemoveRuns[0], Is.SameAs(run));
Assert.That(update.NewRuns[0].Start, Is.EqualTo(1));
Assert.That(update.NewRuns[0].Length, Is.EqualTo(4));
}
[Test]
public void RunCanUnsetLastIndex()
{
@@ -100,94 +163,9 @@ namespace FrameworkTests.Utils
{
var run = new Run(2, 4);
var seen = new List<int>();
run.Iterate(i => seen.Add(i));
run.Iterate(seen.Add);
CollectionAssert.AreEqual(new[] { 2, 3, 4, 5 }, seen);
}
}
public class Run
{
public Run(int start, int length)
{
Start = start;
Length = length;
}
public int Start { get; }
public int Length { get; private set; }
public bool Includes(int index)
{
return index >= Start && index < (Start + Length);
}
public bool ExpandToInclude(int index)
{
if (index == (Start + Length))
{
Length++;
return true;
}
return false;
}
public RunUpdate Unset(int index)
{
if (!Includes(index))
{
return new RunUpdate();
}
if (index == Start)
{
// First index: Replace self with new run at next index, unless empty.
if (Length == 1)
{
return new RunUpdate(Array.Empty<Run>(), new[] { this });
}
return new RunUpdate(
newRuns: new[] { new Run(Start + 1, Length - 1) },
removeRuns: new[] { this }
);
}
if (index == (Start + Length - 1))
{
// Last index: Become one smaller.
Length--;
return new RunUpdate();
}
// Split:
var newRunLength = (Start + Length - 1) - index;
Length = index - Start;
return new RunUpdate(new[] { new Run(index + 1, newRunLength) }, Array.Empty<Run>());
}
public void Iterate(Action<int> action)
{
for (var i = 0; i < Length; i++)
{
action(Start + i);
}
}
}
public class RunUpdate
{
public RunUpdate()
: this(Array.Empty<Run>(), Array.Empty<Run>())
{
}
public RunUpdate(Run[] newRuns, Run[] removeRuns)
{
NewRuns = newRuns;
RemoveRuns = removeRuns;
}
public Run[] NewRuns { get; }
public Run[] RemoveRuns { get; }
}
}
@@ -1,9 +1,4 @@
using Logging;
using Microsoft.VisualStudio.TestPlatform.Common;
using NuGet.Frameworks;
using NUnit.Framework;
using System.Collections.Concurrent;
using System.Numerics;
using NUnit.Framework;
using Utils;
namespace FrameworkTests.Utils
@@ -119,6 +114,19 @@ namespace FrameworkTests.Utils
}, encoded);
}
[Test]
public void SetIndexBetweenRuns()
{
var set = new IndexSet(new[] {8, 9, 10, 12, 13, 14 });
set.Set(11);
var encoded = set.RunLengthEncoded();
CollectionAssert.AreEqual(new[]
{
8, 7
}, encoded);
}
[Test]
public void SetIndexAfterRun()
{
@@ -201,120 +209,5 @@ namespace FrameworkTests.Utils
all.Sort();
return all.ToArray();
}
public class IndexSet
{
private readonly SortedList<int, Run> runs = new SortedList<int, Run>();
public IndexSet()
{
}
public IndexSet(int[] indices)
{
foreach (var i in indices) Set(i);
}
public static IndexSet FromRunLengthEncoded(int[] rle)
{
var set = new IndexSet();
for (var i = 0; i < rle.Length; i += 2)
{
var start = rle[i];
var length = rle[i + 1];
set.runs.Add(start, new Run(start, length));
}
return set;
}
public bool IsSet(int index)
{
if (runs.ContainsKey(index)) return true;
var run = GetRunBefore(index);
if (run == null) return false;
return run.Includes(index);
}
public void Set(int index)
{
if (runs.ContainsKey(index)) return;
var run = GetRunBefore(index);
if (run == null || !run.ExpandToInclude(index))
{
CreateNewRun(index);
}
}
public void Unset(int index)
{
if (runs.ContainsKey(index))
{
HandleUpdate(runs[index].Unset(index));
}
else
{
var run = GetRunBefore(index);
if (run == null) return;
HandleUpdate(run.Unset(index));
}
}
public void Iterate(Action<int> onIndex)
{
foreach (var run in runs.Values)
{
run.Iterate(onIndex);
}
}
public int[] RunLengthEncoded()
{
return Encode().ToArray();
}
private IEnumerable<int> Encode()
{
foreach (var pair in runs)
{
yield return pair.Value.Start;
yield return pair.Value.Length;
}
}
private Run? GetRunBefore(int index)
{
Run? result = null;
foreach (var pair in runs)
{
if (pair.Key < index) result = pair.Value;
else return result;
}
return result;
}
private void HandleUpdate(RunUpdate runUpdate)
{
foreach (var newRun in runUpdate.NewRuns) runs.Add(newRun.Start, newRun);
foreach (var removeRun in runUpdate.RemoveRuns) runs.Remove(removeRun.Start);
}
private void CreateNewRun(int index)
{
if (runs.ContainsKey(index + 1))
{
var length = runs[index + 1].Length + 1;
runs.Add(index, new Run(index, length));
runs.Remove(index + 1);
}
else
{
runs.Add(index, new Run(index, 1));
}
}
}
}
}
+40
View File
@@ -0,0 +1,40 @@
using Logging;
namespace AutoClient
{
public class App
{
public App(Configuration config)
{
Config = config;
Log = new LogSplitter(
new FileLog(Path.Combine(config.LogPath, "autoclient")),
new ConsoleLog()
);
Generator = CreateGenerator();
CidRepo = new CidRepo(config);
Performance = new Performance(new LogSplitter(
new FileLog(Path.Combine(config.LogPath, "performance")),
new ConsoleLog()
));
}
public Configuration Config { get; }
public ILog Log { get; }
public IFileGenerator Generator { get; }
public CancellationTokenSource Cts { get; } = new CancellationTokenSource();
public CidRepo CidRepo { get; }
public Performance Performance { get; }
private IFileGenerator CreateGenerator()
{
if (Config.FileSizeMb > 0)
{
return new RandomFileGenerator(Config, Log);
}
return new ImageGenerator(Log);
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
+85
View File
@@ -0,0 +1,85 @@
namespace AutoClient
{
public class CidRepo
{
private readonly Random random = new Random();
private readonly object _lock = new object();
private readonly List<CidEntry> entries = new List<CidEntry>();
private readonly Configuration config;
public CidRepo(Configuration config)
{
this.config = config;
}
public void Add(string nodeId, string cid, long knownSize)
{
lock (_lock)
{
entries.Add(new CidEntry(nodeId, cid, knownSize));
}
}
public void AddEncoded(string originalCid, string encodedCid)
{
lock (_lock)
{
var entry = entries.SingleOrDefault(e => e.Cid == originalCid);
if (entry == null) return;
entry.Encoded = encodedCid;
}
}
public string? GetForeignCid(string myNodeId)
{
lock (_lock)
{
while (true)
{
if (!entries.Any()) return null;
var available = entries.Where(e => e.NodeId != myNodeId).ToArray();
if (!available.Any()) return null;
var i = random.Next(0, available.Length);
var entry = available[i];
if (entry.CreatedUtc < (DateTime.UtcNow + TimeSpan.FromMinutes(config.ContractDurationMinutes)))
{
entries.Remove(entry);
}
else
{
return entry.Cid;
}
}
}
}
public long? GetSizeForCid(string cid)
{
lock (_lock)
{
var entry = entries.SingleOrDefault(e => e.Cid == cid);
if (entry == null) return null;
return entry.KnownSize;
}
}
}
public class CidEntry
{
public CidEntry(string nodeId, string cid, long knownSize)
{
NodeId = nodeId;
Cid = cid;
KnownSize = knownSize;
}
public string NodeId { get; }
public string Cid { get; }
public string Encoded { get; set; } = string.Empty;
public long KnownSize { get; }
public DateTime CreatedUtc { get; } = DateTime.UtcNow;
}
}
+57
View File
@@ -0,0 +1,57 @@
using CodexOpenApi;
using Logging;
using Utils;
namespace AutoClient
{
public class CodexUser
{
private readonly App app;
private readonly CodexApi codex;
private readonly HttpClient client;
private readonly Address address;
private readonly List<Purchaser> purchasers = new List<Purchaser>();
private Task starterTask = Task.CompletedTask;
private readonly string nodeId = Guid.NewGuid().ToString();
public CodexUser(App app, CodexApi codex, HttpClient client, Address address)
{
this.app = app;
this.codex = codex;
this.client = client;
this.address = address;
}
public void Start(int index)
{
for (var i = 0; i < app.Config.NumConcurrentPurchases; i++)
{
purchasers.Add(new Purchaser(app, nodeId, new LogPrefixer(app.Log, $"({i}) "), client, address, codex));
}
var delayPerPurchaser =
TimeSpan.FromSeconds(10 * index) +
TimeSpan.FromMinutes(app.Config.ContractDurationMinutes) / app.Config.NumConcurrentPurchases;
starterTask = Task.Run(() => StartPurchasers(delayPerPurchaser));
}
private async Task StartPurchasers(TimeSpan delayPerPurchaser)
{
foreach (var purchaser in purchasers)
{
purchaser.Start();
await Task.Delay(delayPerPurchaser);
}
}
public void Stop()
{
starterTask.Wait();
foreach (var purchaser in purchasers)
{
purchaser.Stop();
}
}
}
}
+6 -9
View File
@@ -4,11 +4,8 @@ namespace AutoClient
{
public class Configuration
{
[Uniform("codex-host", "ch", "CODEXHOST", false, "Codex Host address. (default 'http://localhost')")]
public string CodexHost { get; set; } = "http://localhost";
[Uniform("codex-port", "cp", "CODEXPORT", false, "port number of Codex API. (8080 by default)")]
public int CodexPort { get; set; } = 8080;
[Uniform("codex-endpoints", "ce", "CODEXENDPOINTS", false, "Codex endpoints. Semi-colon separated. (default 'http://localhost:8080')")]
public string CodexEndpoints { get; set; } = "http://localhost:8080";
[Uniform("datapath", "dp", "DATAPATH", false, "Root path where all data files will be saved.")]
public string DataPath { get; set; } = "datapath";
@@ -22,11 +19,11 @@ namespace AutoClient
[Uniform("contract-expiry", "ce", "CONTRACTEXPIRY", false, "contract expiry in minutes. (default 15 minutes)")]
public int ContractExpiryMinutes { get; set; } = 15;
[Uniform("num-hosts", "nh", "NUMHOSTS", false, "Number of hosts for contract. (default 5)")]
public int NumHosts { get; set; } = 5;
[Uniform("num-hosts", "nh", "NUMHOSTS", false, "Number of hosts for contract. (default 10)")]
public int NumHosts { get; set; } = 10;
[Uniform("num-hosts-tolerance", "nt", "NUMTOL", false, "Number of host tolerance for contract. (default 2)")]
public int HostTolerance { get; set; } = 2;
[Uniform("num-hosts-tolerance", "nt", "NUMTOL", false, "Number of host tolerance for contract. (default 5)")]
public int HostTolerance { get; set; } = 5;
[Uniform("price","p", "PRICE", false, "Price of contract. (default 10)")]
public int Price { get; set; } = 10;
+3 -3
View File
@@ -11,16 +11,16 @@ namespace AutoClient
public class ImageGenerator : IFileGenerator
{
private LogSplitter log;
private readonly ILog log;
public ImageGenerator(LogSplitter log)
public ImageGenerator(ILog log)
{
this.log = log;
}
public async Task<string> Generate()
{
log.Log("Fetching random image from picsum.photos...");
log.Debug("Fetching random image from picsum.photos...");
var httpClient = new HttpClient();
var thing = await httpClient.GetStreamAsync("https://picsum.photos/3840/2160");
+65
View File
@@ -0,0 +1,65 @@
using Logging;
namespace AutoClient
{
public class Performance
{
private readonly ILog log;
public Performance(ILog log)
{
this.log = log;
}
public void DownloadFailed(Exception ex)
{
Log($"Download failed: {ex}");
}
public void DownloadSuccessful(long size, TimeSpan time)
{
long milliseconds = Convert.ToInt64(time.TotalMilliseconds);
if (milliseconds < 1) milliseconds = 1;
long bytesPerSecond = 1000 * (size / milliseconds);
Log($"Download successful: {bytesPerSecond} bytes per second");
}
public void StorageContractCancelled()
{
Log("Contract cancelled");
}
public void StorageContractErrored(string error)
{
Log($"Contract errored: {error}");
}
public void StorageContractFinished()
{
Log("Contract finished");
}
public void StorageContractStarted()
{
Log("Contract started");
}
public void UploadFailed(Exception ex)
{
Log($"Upload failed: {ex}");
}
public void UploadSuccessful(long size, TimeSpan time)
{
long milliseconds = Convert.ToInt64(time.TotalMilliseconds);
if (milliseconds < 1) milliseconds = 1;
long bytesPerSecond = 1000 * (size / milliseconds);
Log($"Upload successful: {bytesPerSecond} bytes per second");
}
private void Log(string msg)
{
log.Log(msg);
}
}
}
+61 -45
View File
@@ -1,16 +1,20 @@
using ArgsUniform;
using AutoClient;
using CodexOpenApi;
using Core;
using Logging;
using Utils;
public static class Program
public class Program
{
private readonly App app;
public Program(Configuration config)
{
app = new App(config);
}
public static async Task Main(string[] args)
{
var cts = new CancellationTokenSource();
var cancellationToken = cts.Token;
Console.CancelKeyPress += (sender, args) => cts.Cancel();
var uniformArgs = new ArgsUniform<Configuration>(PrintHelp, args);
@@ -21,58 +25,70 @@ public static class Program
throw new Exception("Number of concurrent purchases must be > 0");
}
var log = new LogSplitter(
new FileLog(Path.Combine(config.LogPath, "autoclient")),
new ConsoleLog()
);
var p = new Program(config);
await p.Run();
}
public async Task Run()
{
var codexUsers = await CreateUsers();
var i = 0;
foreach (var user in codexUsers)
{
user.Start(i);
i++;
}
app.Cts.Token.WaitHandle.WaitOne();
foreach (var user in codexUsers) user.Stop();
app.Log.Log("Done");
}
private async Task<CodexUser[]> CreateUsers()
{
var endpointStrs = app.Config.CodexEndpoints.Split(";", StringSplitOptions.RemoveEmptyEntries);
var result = new List<CodexUser>();
foreach (var e in endpointStrs)
{
result.Add(await CreateUser(e));
}
return result.ToArray();
}
private async Task<CodexUser> CreateUser(string endpoint)
{
var splitIndex = endpoint.LastIndexOf(':');
var host = endpoint.Substring(0, splitIndex);
var port = Convert.ToInt32(endpoint.Substring(splitIndex + 1));
var address = new Address(
host: config.CodexHost,
port: config.CodexPort
host: host,
port: port
);
log.Log($"Start. Address: {address}");
var generator = CreateGenerator(config, log);
var client = new HttpClient();
var codex = new CodexApi(client);
codex.BaseUrl = $"{address.Host}:{address.Port}/api/codex/v1";
await CheckCodex(codex, log);
app.Log.Log($"Checking Codex at {address}...");
await CheckCodex(codex);
app.Log.Log("OK");
var purchasers = new List<Purchaser>();
for (var i = 0; i < config.NumConcurrentPurchases; i++)
{
purchasers.Add(
new Purchaser(new LogPrefixer(log, $"({i}) "), client, address, codex, config, generator, cancellationToken)
);
}
var delayPerPurchaser = TimeSpan.FromMinutes(config.ContractDurationMinutes) / config.NumConcurrentPurchases;
foreach (var purchaser in purchasers)
{
purchaser.Start();
await Task.Delay(delayPerPurchaser);
}
cancellationToken.WaitHandle.WaitOne();
log.Log("Done.");
return new CodexUser(
app,
codex,
client,
address
);
}
private static IFileGenerator CreateGenerator(Configuration config, LogSplitter log)
private async Task CheckCodex(CodexApi codex)
{
if (config.FileSizeMb > 0)
{
return new RandomFileGenerator(config, log);
}
return new ImageGenerator(log);
}
private static async Task CheckCodex(CodexApi codex, ILog log)
{
log.Log("Checking Codex...");
try
{
var info = await codex.GetDebugInfoAsync();
@@ -80,7 +96,7 @@ public static class Program
}
catch (Exception ex)
{
log.Log($"Codex not OK: {ex}");
app.Log.Error($"Codex not OK: {ex}");
throw;
}
}
+147 -55
View File
@@ -8,114 +8,197 @@ namespace AutoClient
{
public class Purchaser
{
private readonly App app;
private readonly string nodeId;
private readonly ILog log;
private readonly HttpClient client;
private readonly Address address;
private readonly CodexApi codex;
private readonly Configuration config;
private readonly IFileGenerator generator;
private readonly CancellationToken ct;
private Task workerTask = Task.CompletedTask;
public Purchaser(ILog log, HttpClient client, Address address, CodexApi codex, Configuration config, IFileGenerator generator, CancellationToken ct)
public Purchaser(App app, string nodeId, ILog log, HttpClient client, Address address, CodexApi codex)
{
this.app = app;
this.nodeId = nodeId;
this.log = log;
this.client = client;
this.address = address;
this.codex = codex;
this.config = config;
this.generator = generator;
this.ct = ct;
}
public void Start()
{
Task.Run(Worker);
workerTask = Task.Run(Worker);
}
public void Stop()
{
workerTask.Wait();
}
private async Task Worker()
{
while (!ct.IsCancellationRequested)
log.Log("Worker started.");
while (!app.Cts.Token.IsCancellationRequested)
{
var pid = await StartNewPurchase();
await WaitTillFinished(pid);
try
{
var pid = await StartNewPurchase();
await WaitTillFinished(pid);
await DownloadForeignCid();
}
catch (Exception ex)
{
log.Error("Worker failed with: " + ex);
await Task.Delay(TimeSpan.FromHours(6));
}
}
}
private async Task DownloadForeignCid()
{
var cid = app.CidRepo.GetForeignCid(nodeId);
if (cid == null) return;
var size = app.CidRepo.GetSizeForCid(cid);
if (size == null) return;
try
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var filename = Guid.NewGuid().ToString().ToLowerInvariant();
{
using var fileStream = File.OpenWrite(filename);
var fileResponse = await codex.DownloadNetworkStreamAsync(cid);
fileResponse.Stream.CopyTo(fileStream);
}
var time = sw.Elapsed;
File.Delete(filename);
app.Performance.DownloadSuccessful(size.Value, time);
}
catch (Exception ex)
{
app.Performance.DownloadFailed(ex);
}
}
private async Task<string> StartNewPurchase()
{
var file = await CreateFile();
var cid = await UploadFile(file);
return await RequestStorage(cid);
try
{
var cid = await UploadFile(file);
return await RequestStorage(cid);
}
finally
{
DeleteFile(file);
}
}
private async Task<string> CreateFile()
{
return await generator.Generate();
return await app.Generator.Generate();
}
private void DeleteFile(string file)
{
try
{
File.Delete(file);
}
catch (Exception exc)
{
app.Log.Error($"Failed to delete file '{file}': {exc}");
}
}
private async Task<ContentId> UploadFile(string filename)
{
// Copied from CodexNode :/
using var fileStream = File.OpenRead(filename);
try
{
var info = new FileInfo(filename);
var sw = System.Diagnostics.Stopwatch.StartNew();
var cid = await UploadStream(fileStream);
var time = sw.Elapsed;
app.Performance.UploadSuccessful(info.Length, time);
app.CidRepo.Add(nodeId, cid.Id, info.Length);
return cid;
}
catch (Exception exc)
{
app.Performance.UploadFailed(exc);
throw;
}
}
log.Log($"Uploading file {filename}...");
var response = await codex.UploadAsync(fileStream, ct);
private async Task<ContentId> UploadStream(FileStream fileStream)
{
log.Debug($"Uploading file...");
var response = await codex.UploadAsync(fileStream, app.Cts.Token);
if (string.IsNullOrEmpty(response)) FrameworkAssert.Fail("Received empty response.");
if (response.StartsWith("Unable to store block")) FrameworkAssert.Fail("Node failed to store block.");
log.Log($"Uploaded file. Received contentId: '{response}'.");
log.Debug($"Uploaded file. Received contentId: '{response}'.");
return new ContentId(response);
}
private async Task<string> RequestStorage(ContentId cid)
{
log.Log("Requesting storage for " + cid.Id);
log.Debug("Requesting storage for " + cid.Id);
var result = await codex.CreateStorageRequestAsync(cid.Id, new StorageRequestCreation()
{
Collateral = config.RequiredCollateral.ToString(),
Duration = (config.ContractDurationMinutes * 60).ToString(),
Expiry = (config.ContractExpiryMinutes * 60).ToString(),
Nodes = config.NumHosts,
Reward = config.Price.ToString(),
Collateral = app.Config.RequiredCollateral.ToString(),
Duration = (app.Config.ContractDurationMinutes * 60).ToString(),
Expiry = (app.Config.ContractExpiryMinutes * 60).ToString(),
Nodes = app.Config.NumHosts,
Reward = app.Config.Price.ToString(),
ProofProbability = "15",
Tolerance = config.HostTolerance
}, ct);
Tolerance = app.Config.HostTolerance
}, app.Cts.Token);
log.Log("Purchase ID: " + result);
log.Debug("Purchase ID: " + result);
var encoded = await GetEncodedCid(result);
app.CidRepo.AddEncoded(cid.Id, encoded);
return result;
}
private async Task<string?> GetPurchaseState(string pid)
private async Task<string> GetEncodedCid(string pid)
{
try
{
// openapi still don't match code.
var str = await client.GetStringAsync($"{address.Host}:{address.Port}/api/codex/v1/storage/purchases/{pid}");
if (string.IsNullOrEmpty(str)) return null;
var sp = JsonConvert.DeserializeObject<StoragePurchase>(str)!;
log.Log($"Purchase {pid} is {sp.State}");
if (!string.IsNullOrEmpty(sp.Error)) log.Log($"Purchase {pid} error is {sp.Error}");
return sp.State;
var sp = (await GetStoragePurchase(pid))!;
return sp.Request.Content.Cid;
}
catch
catch (Exception ex)
{
return null;
log.Error(ex.ToString());
throw;
}
}
private async Task<StoragePurchase?> GetStoragePurchase(string pid)
{
// openapi still don't match code.
var str = await client.GetStringAsync($"{address.Host}:{address.Port}/api/codex/v1/storage/purchases/{pid}");
if (string.IsNullOrEmpty(str)) return null;
return JsonConvert.DeserializeObject<StoragePurchase>(str);
}
private async Task WaitTillFinished(string pid)
{
log.Log("Waiting...");
try
{
var emptyResponseTolerance = 10;
while (true)
while (!app.Cts.Token.IsCancellationRequested)
{
var status = (await GetPurchaseState(pid))?.ToLowerInvariant();
if (string.IsNullOrEmpty(status))
var purchase = await GetStoragePurchase(pid);
if (purchase == null)
{
await FixedShortDelay();
emptyResponseTolerance--;
if (emptyResponseTolerance == 0)
{
@@ -123,19 +206,28 @@ namespace AutoClient
await ExpiryTimeDelay();
return;
}
continue;
}
else
var status = purchase.State.ToLowerInvariant();
if (status.Contains("cancel"))
{
if (status.Contains("cancel") ||
status.Contains("error") ||
status.Contains("finished"))
{
return;
}
if (status.Contains("started"))
{
await FixedDurationDelay();
}
app.Performance.StorageContractCancelled();
return;
}
if (status.Contains("error"))
{
app.Performance.StorageContractErrored(purchase.Error);
return;
}
if (status.Contains("finished"))
{
app.Performance.StorageContractFinished();
return;
}
if (status.Contains("started"))
{
app.Performance.StorageContractStarted();
await FixedDurationDelay();
}
await FixedShortDelay();
@@ -150,17 +242,17 @@ namespace AutoClient
private async Task FixedDurationDelay()
{
await Task.Delay(config.ContractDurationMinutes * 60 * 1000, ct);
await Task.Delay(app.Config.ContractDurationMinutes * 60 * 1000, app.Cts.Token);
}
private async Task ExpiryTimeDelay()
{
await Task.Delay(config.ContractExpiryMinutes * 60 * 1000, ct);
await Task.Delay(app.Config.ContractExpiryMinutes * 60 * 1000, app.Cts.Token);
}
private async Task FixedShortDelay()
{
await Task.Delay(15 * 1000, ct);
await Task.Delay(15 * 1000, app.Cts.Token);
}
}
}
+1 -1
View File
@@ -6,7 +6,7 @@ Can generate random images or random data of a specified size.
## How to run
- dotnet 7.0 and CLI arguments: `dotnet run -- --codex-host=... --codex-port=...`
- dotnet 8.0 and CLI arguments: `dotnet run -- --codex-host=... --codex-port=...`
- docker and env-vars: `codexstorage/codex-autoclient:sha-88daab3`
## Configuration options
+1 -1
View File
@@ -1,5 +1,5 @@
# Variables
ARG BUILDER=mcr.microsoft.com/dotnet/sdk:7.0
ARG BUILDER=mcr.microsoft.com/dotnet/sdk:8.0
ARG IMAGE=${BUILDER}
ARG APP_HOME=/app
+2 -2
View File
@@ -27,9 +27,9 @@ namespace BiblioTech
return channel.Id == Program.Config.AdminChannelId;
}
public ISocketMessageChannel GetAdminChannel()
public async Task SendInAdminChannel(string msg)
{
return adminChannel;
await adminChannel.SendMessageAsync(msg);
}
public void SetAdminChannel(ISocketMessageChannel adminChannel)
+21 -9
View File
@@ -25,16 +25,13 @@ namespace BiblioTech
catch (Exception ex)
{
var msg = "Failed with exception: " + ex;
if (IsInAdminChannel(command))
{
await command.FollowupAsync(msg.Substring(0, Math.Min(1900, msg.Length)));
}
else
{
await command.FollowupAsync("Something failed while trying to do that...", ephemeral: true);
await Program.AdminChecker.GetAdminChannel().SendMessageAsync(msg);
}
Program.Log.Error(msg);
if (!IsInAdminChannel(command))
{
await command.FollowupAsync("Something failed while trying to do that... (error details posted in admin channel)", ephemeral: true);
}
await Program.AdminChecker.SendInAdminChannel(msg);
}
}
@@ -62,5 +59,20 @@ namespace BiblioTech
if (IsSenderAdmin(context.Command) && targetUser != null) return targetUser;
return context.Command.User;
}
protected string Mention(SocketUser user)
{
return Mention(user.Id);
}
protected string Mention(IUser user)
{
return Mention(user.Id);
}
protected string Mention(ulong userId)
{
return $"<@{userId}>";
}
}
}
+2 -1
View File
@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
@@ -12,6 +12,7 @@
<ProjectReference Include="..\..\Framework\ArgsUniform\ArgsUniform.csproj" />
<ProjectReference Include="..\..\Framework\DiscordRewards\DiscordRewards.csproj" />
<ProjectReference Include="..\..\Framework\GethConnector\GethConnector.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexPlugin\CodexPlugin.csproj" />
</ItemGroup>
</Project>
+198
View File
@@ -0,0 +1,198 @@
using CodexOpenApi;
using IdentityModel.Client;
using Utils;
namespace BiblioTech
{
public class CodexCidChecker
{
private static readonly string nl = Environment.NewLine;
private readonly Configuration config;
private CodexApi? currentCodexNode;
public CodexCidChecker(Configuration config)
{
this.config = config;
}
public async Task<CheckResponse> PerformCheck(string cid)
{
if (string.IsNullOrEmpty(config.CodexEndpoint))
{
return new CheckResponse(false, "Codex CID checker is not (yet) available.", "");
}
try
{
var codex = GetCodex();
var nodeCheck = await 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 await PerformCheck(codex, cid);
}
catch (Exception ex)
{
return new CheckResponse(false, "Internal server error", ex.ToString());
}
}
private async Task<CheckResponse> PerformCheck(CodexApi codex, string cid)
{
try
{
var manifest = await codex.DownloadNetworkManifestAsync(cid);
return SuccessMessage(manifest);
}
catch (ApiException apiEx)
{
if (apiEx.StatusCode == 400) return CidFormatInvalid(apiEx.Response);
if (apiEx.StatusCode == 404) return FailedToFetch(apiEx.Response);
return UnexpectedReturnCode(apiEx.Response);
}
catch (Exception ex)
{
return UnexpectedException(ex);
}
}
#region Response formatting
private CheckResponse SuccessMessage(DataItem 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 CodexApi GetCodex()
{
if (currentCodexNode == null) currentCodexNode = CreateCodex();
return currentCodexNode;
}
private async Task<bool> CheckCodex(CodexApi codex)
{
try
{
var info = await currentCodexNode!.GetDebugInfoAsync();
if (info == null || string.IsNullOrEmpty(info.Id)) return false;
return true;
}
catch (Exception e)
{
return false;
}
}
private CodexApi 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(
host: host,
port: port
);
var client = new HttpClient();
if (!string.IsNullOrEmpty(config.CodexEndpointAuth) && config.CodexEndpointAuth.Contains(":"))
{
var tokens = config.CodexEndpointAuth.Split(':');
if (tokens.Length != 2) throw new Exception("Expected '<username>:<password>' in CodexEndpointAuth parameter.");
client.SetBasicAuthentication(tokens[0], tokens[1]);
}
var codex = new CodexApi(client);
codex.BaseUrl = $"{address.Host}:{address.Port}/api/codex/v1";
return codex;
}
#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; }
}
}
@@ -0,0 +1,38 @@
using BiblioTech.Options;
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;
public CheckCidCommand(CodexCidChecker checker)
{
this.checker = checker;
}
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 = await 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}'");
await context.Followup(response.Message);
}
}
}
@@ -28,6 +28,7 @@ namespace BiblioTech.Commands
if (addr == null)
{
await context.Followup($"No address has been set for this user. Please use '/{userAssociateCommand.Name}' to set it first.");
await Program.AdminChecker.SendInAdminChannel($"User {Mention(userId)} used '/{Name}' but address has not been set.");
return;
}
+10 -1
View File
@@ -28,6 +28,7 @@ namespace BiblioTech.Commands
if (addr == null)
{
await context.Followup($"No address has been set for this user. Please use '/{userAssociateCommand.Name}' to set it first.");
await Program.AdminChecker.SendInAdminChannel($"User {Mention(userId)} used '/{Name}' but address has not been set.");
return;
}
@@ -42,9 +43,17 @@ namespace BiblioTech.Commands
mintedTokens = ProcessTokens(contracts, addr, report);
});
var reportLine = string.Join(Environment.NewLine, report);
Program.UserRepo.AddMintEventForUser(userId, addr, sentEth, mintedTokens);
await Program.AdminChecker.SendInAdminChannel($"User {Mention(userId)} used '/{Name}' successfully. ({reportLine})");
await context.Followup(string.Join(Environment.NewLine, report));
await context.Followup(reportLine);
}
private string Format<T>(Transaction<T>? transaction)
{
if (transaction == null) return "-";
return transaction.ToString();
}
private Transaction<TestToken>? ProcessTokens(ICodexContracts contracts, EthAddress addr, List<string> report)
@@ -1,4 +1,8 @@
using BiblioTech.Options;
using Discord;
using GethPlugin;
using k8s.KubeConfigModels;
using NBitcoin.Secp256k1;
namespace BiblioTech.Commands
{
@@ -23,30 +27,56 @@ namespace BiblioTech.Commands
protected override async Task Invoke(CommandContext context)
{
var user = GetUserFromCommand(optionalUser, context);
var data = await ethOption.Parse(context);
if (data == null) return;
var newAddress = await ethOption.Parse(context);
if (newAddress == null) return;
var currentAddress = Program.UserRepo.GetCurrentAddressForUser(user);
if (currentAddress != null && !IsSenderAdmin(context.Command))
{
await context.Followup($"You've already set your Ethereum address to {currentAddress}.");
await Program.AdminChecker.SendInAdminChannel($"User {Mention(user)} used '/{Name}' but already has an address set. ({currentAddress})");
return;
}
var result = Program.UserRepo.AssociateUserWithAddress(user, data);
if (result)
var result = Program.UserRepo.AssociateUserWithAddress(user, newAddress);
switch (result)
{
await context.Followup(new string[]
{
case SetAddressResponse.OK:
await ResponseOK(context, user, newAddress);
break;
case SetAddressResponse.AddressAlreadyInUse:
await ResponseAlreadyUsed(context, user, newAddress);
break;
case SetAddressResponse.CreateUserFailed:
await ResponseCreateUserFailed(context, user);
break;
default:
throw new Exception("Unknown SetAddressResponse mode");
}
}
private async Task ResponseCreateUserFailed(CommandContext context, IUser user)
{
await context.Followup("Internal error. Error details sent to admin.");
await Program.AdminChecker.SendInAdminChannel($"User {Mention(user)} used '/{Name}' but failed to create new user.");
}
private async Task ResponseAlreadyUsed(CommandContext context, IUser user, EthAddress newAddress)
{
await context.Followup("This address is already in use by another user.");
await Program.AdminChecker.SendInAdminChannel($"User {Mention(user)} used '/{Name}' but the provided address is already in use by another user. (address: {newAddress})");
}
private async Task ResponseOK(CommandContext context, IUser user, GethPlugin.EthAddress newAddress)
{
await context.Followup(new string[]
{
"Done! Thank you for joining the test net!",
"By default, the bot will @-mention you with test-net reward related notifications.",
"By default, the bot will @-mention you with test-net related notifications.",
$"You can enable/disable this behavior with the '/{notifyCommand.Name}' command."
});
}
else
{
await context.Followup("That didn't work.");
}
});
await Program.AdminChecker.SendInAdminChannel($"User {Mention(user)} used '/{Name}' successfully. ({newAddress})");
}
}
}
+6
View File
@@ -38,6 +38,12 @@ namespace BiblioTech
[Uniform("no-discord", "nd", "NODISCORD", false, "For debugging: Bypasses all Discord API calls.")]
public int NoDiscord { get; set; } = 0;
[Uniform("codex-endpoint", "ce", "CODEXENDPOINT", false, "Codex endpoint. (default 'http://localhost:8080')")]
public string CodexEndpoint { get; set; } = "http://localhost:8080";
[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; } = "";
public string EndpointsPath => Path.Combine(DataPath, "endpoints");
public string UserDataPath => Path.Combine(DataPath, "users");
public string LogPath => Path.Combine(DataPath, "logs");
+2 -1
View File
@@ -3,7 +3,6 @@ using BiblioTech.Commands;
using BiblioTech.Rewards;
using Discord;
using Discord.WebSocket;
using DiscordRewards;
using Logging;
namespace BiblioTech
@@ -81,6 +80,7 @@ namespace BiblioTech
client = new DiscordSocketClient();
client.Log += ClientLog;
var checker = new CodexCidChecker(Config);
var notifyCommand = new NotifyCommand();
var associateCommand = new UserAssociateCommand(notifyCommand);
var sprCommand = new SprCommand();
@@ -90,6 +90,7 @@ namespace BiblioTech
sprCommand,
associateCommand,
notifyCommand,
new CheckCidCommand(checker),
new AdminCommand(sprCommand, replacement)
);
+36 -11
View File
@@ -1,4 +1,5 @@
using Discord.WebSocket;
using DiscordRewards;
using Logging;
namespace BiblioTech.Rewards
@@ -16,24 +17,17 @@ namespace BiblioTech.Rewards
this.eventsChannel = eventsChannel;
}
public async Task ProcessChainEvents(string[] eventsOverview)
public async Task ProcessChainEvents(ChainEventMessage[] eventsOverview, string[] errors)
{
await SendErrorsToAdminChannel(errors);
if (eventsChannel == null || eventsOverview == null || !eventsOverview.Any()) return;
try
{
await Task.Run(async () =>
{
var users = Program.UserRepo.GetAllUserData();
foreach (var e in eventsOverview)
{
if (!string.IsNullOrEmpty(e))
{
var @event = ApplyReplacements(users, e);
await eventsChannel.SendMessageAsync(@event);
await Task.Delay(3000);
}
}
await SendChainEventsInOrder(eventsOverview, eventsChannel, users);
});
}
catch (Exception ex)
@@ -42,6 +36,37 @@ namespace BiblioTech.Rewards
}
}
private async Task SendErrorsToAdminChannel(string[] errors)
{
try
{
foreach (var error in errors)
{
await Program.AdminChecker.SendInAdminChannel(error);
}
}
catch (Exception exc)
{
log.Error("Failed to send error messages to admin channel. " + exc);
Environment.Exit(1);
}
}
private async Task SendChainEventsInOrder(ChainEventMessage[] eventsOverview, SocketTextChannel eventsChannel, UserData[] users)
{
eventsOverview = eventsOverview.OrderBy(e => e.BlockNumber).ToArray();
foreach (var e in eventsOverview)
{
var msg = e.Message;
if (!string.IsNullOrEmpty(msg))
{
var @event = ApplyReplacements(users, msg);
await eventsChannel.SendMessageAsync(@event);
await Task.Delay(300);
}
}
}
private string ApplyReplacements(UserData[] users, string msg)
{
var result = ApplyUserAddressReplacements(users, msg);
+1 -1
View File
@@ -31,7 +31,7 @@ namespace BiblioTech.Rewards
await ProcessRewards(rewards);
}
await eventsSender.ProcessChainEvents(rewards.EventsOverview);
await eventsSender.ProcessChainEvents(rewards.EventsOverview, rewards.Errors);
}
private async Task ProcessRewards(GiveRewardsCommand rewards)
+6
View File
@@ -10,5 +10,11 @@
public T TokenAmount { get; }
public string TransactionHash { get; }
public override string ToString()
{
if (TokenAmount == null) return "NULL";
return TokenAmount.ToString()!;
}
}
}

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