Merge branch 'chainstate-update'

This commit is contained in:
benbierens 2024-06-21 10:19:32 +02:00
commit 5afab577a7
No known key found for this signature in database
GPG Key ID: 877D2C2E09A22F3A
43 changed files with 1623 additions and 890 deletions

View File

@ -13,9 +13,9 @@ namespace DiscordRewards
public enum CheckType
{
Uninitialized,
FilledSlot,
FinishedSlot,
PostedContract,
StartedContract,
HostFilledSlot,
HostFinishedSlot,
ClientPostedContract,
ClientStartedContract,
}
}

View File

@ -5,6 +5,11 @@
public RewardUsersCommand[] Rewards { get; set; } = Array.Empty<RewardUsersCommand>();
public MarketAverage[] Averages { get; set; } = Array.Empty<MarketAverage>();
public string[] EventsOverview { get; set; } = Array.Empty<string>();
public bool HasAny()
{
return Rewards.Any() || Averages.Any() || EventsOverview.Any();
}
}
public class RewardUsersCommand

View File

@ -11,19 +11,19 @@ namespace DiscordRewards
// Filled any slot
new RewardConfig(1187039439558541498, $"{Tag} successfully filled their first slot!", new CheckConfig
{
Type = CheckType.FilledSlot
Type = CheckType.HostFilledSlot
}),
// Finished any slot
new RewardConfig(1202286165630390339, $"{Tag} successfully finished their first slot!", new CheckConfig
{
Type = CheckType.FinishedSlot
Type = CheckType.HostFinishedSlot
}),
// Finished a sizable slot
new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot! (10mb/5mins for test)", new CheckConfig
{
Type = CheckType.FinishedSlot,
Type = CheckType.HostFinishedSlot,
MinSlotSize = 10.MB(),
MinDuration = TimeSpan.FromMinutes(5.0),
}),
@ -31,19 +31,19 @@ namespace DiscordRewards
// Posted any contract
new RewardConfig(1202286258370383913, $"{Tag} posted their first contract!", new CheckConfig
{
Type = CheckType.PostedContract
Type = CheckType.ClientPostedContract
}),
// Started any contract
new RewardConfig(1202286330873126992, $"A contract created by {Tag} reached Started state for the first time!", new CheckConfig
{
Type = CheckType.StartedContract
Type = CheckType.ClientStartedContract
}),
// Started a sizable contract
new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time! (10mb/5mins for test)", new CheckConfig
{
Type = CheckType.StartedContract,
Type = CheckType.ClientStartedContract,
MinNumberOfHosts = 4,
MinSlotSize = 10.MB(),
MinDuration = TimeSpan.FromMinutes(5.0),

View File

@ -117,6 +117,7 @@ namespace NethereumWorkflow
}
return new BlockInterval(
timeRange: timeRange,
from: fromBlock.Value,
to: toBlock.Value
);

View File

@ -2,7 +2,7 @@
{
public class BlockInterval
{
public BlockInterval(ulong from, ulong to)
public BlockInterval(TimeRange timeRange, ulong from, ulong to)
{
if (from < to)
{
@ -14,10 +14,13 @@
From = to;
To = from;
}
TimeRange = timeRange;
}
public ulong From { get; }
public ulong To { get; }
public TimeRange TimeRange { get; }
public ulong NumberOfBlocks => To - From;
public override string ToString()
{

View File

@ -0,0 +1,68 @@
using CodexContractsPlugin.Marketplace;
using Utils;
namespace CodexContractsPlugin.ChainMonitor
{
public class ChainEvents
{
private ChainEvents(
BlockInterval blockInterval,
Request[] requests,
RequestFulfilledEventDTO[] fulfilled,
RequestCancelledEventDTO[] cancelled,
SlotFilledEventDTO[] slotFilled,
SlotFreedEventDTO[] slotFreed
)
{
BlockInterval = blockInterval;
Requests = requests;
Fulfilled = fulfilled;
Cancelled = cancelled;
SlotFilled = slotFilled;
SlotFreed = slotFreed;
}
public BlockInterval BlockInterval { get; }
public Request[] Requests { get; }
public RequestFulfilledEventDTO[] Fulfilled { get; }
public RequestCancelledEventDTO[] Cancelled { get; }
public SlotFilledEventDTO[] SlotFilled { get; }
public SlotFreedEventDTO[] SlotFreed { get; }
public IHasBlock[] All
{
get
{
var all = new List<IHasBlock>();
all.AddRange(Requests);
all.AddRange(Fulfilled);
all.AddRange(Cancelled);
all.AddRange(SlotFilled);
all.AddRange(SlotFreed);
return all.ToArray();
}
}
public static ChainEvents FromBlockInterval(ICodexContracts contracts, BlockInterval blockInterval)
{
return FromContractEvents(contracts.GetEvents(blockInterval));
}
public static ChainEvents FromTimeRange(ICodexContracts contracts, TimeRange timeRange)
{
return FromContractEvents(contracts.GetEvents(timeRange));
}
public static ChainEvents FromContractEvents(ICodexContractsEvents events)
{
return new ChainEvents(
events.BlockInterval,
events.GetStorageRequests(),
events.GetRequestFulfilledEvents(),
events.GetRequestCancelledEvents(),
events.GetSlotFilledEvents(),
events.GetSlotFreedEvents()
);
}
}
}

View File

@ -0,0 +1,156 @@
using CodexContractsPlugin.Marketplace;
using Logging;
using System.Numerics;
using Utils;
namespace CodexContractsPlugin.ChainMonitor
{
public interface IChainStateChangeHandler
{
void OnNewRequest(IChainStateRequest request);
void OnRequestFinished(IChainStateRequest request);
void OnRequestFulfilled(IChainStateRequest request);
void OnRequestCancelled(IChainStateRequest request);
void OnSlotFilled(IChainStateRequest request, BigInteger slotIndex);
void OnSlotFreed(IChainStateRequest request, BigInteger slotIndex);
}
public class ChainState
{
private readonly List<ChainStateRequest> requests = new List<ChainStateRequest>();
private readonly ILog log;
private readonly ICodexContracts contracts;
private readonly IChainStateChangeHandler handler;
public ChainState(ILog log, ICodexContracts contracts, IChainStateChangeHandler changeHandler, DateTime startUtc)
{
this.log = new LogPrefixer(log, "(ChainState) ");
this.contracts = contracts;
handler = changeHandler;
StartUtc = startUtc;
TotalSpan = new TimeRange(startUtc, startUtc);
}
public TimeRange TotalSpan { get; private set; }
public IChainStateRequest[] Requests => requests.ToArray();
public DateTime StartUtc { get; }
public void Update()
{
Update(DateTime.UtcNow);
}
public void Update(DateTime toUtc)
{
var span = new TimeRange(TotalSpan.To, toUtc);
var events = ChainEvents.FromTimeRange(contracts, span);
Apply(events);
TotalSpan = new TimeRange(TotalSpan.From, span.To);
}
private void Apply(ChainEvents events)
{
if (events.BlockInterval.TimeRange.From < TotalSpan.From)
throw new Exception("Attempt to update ChainState with set of events from before its current record.");
log.Log($"ChainState updating: {events.BlockInterval}");
// Run through each block and apply the events to the state in order.
var span = events.BlockInterval.TimeRange.Duration;
var numBlocks = events.BlockInterval.NumberOfBlocks;
var spanPerBlock = span / numBlocks;
var eventUtc = events.BlockInterval.TimeRange.From;
for (var b = events.BlockInterval.From; b <= events.BlockInterval.To; b++)
{
var blockEvents = events.All.Where(e => e.Block.BlockNumber == b).ToArray();
ApplyEvents(b, blockEvents, eventUtc);
eventUtc += spanPerBlock;
}
}
private void ApplyEvents(ulong blockNumber, IHasBlock[] blockEvents, DateTime eventsUtc)
{
foreach (var e in blockEvents)
{
dynamic d = e;
ApplyEvent(d);
}
ApplyTimeImplicitEvents(blockNumber, eventsUtc);
}
private void ApplyEvent(Request request)
{
if (requests.Any(r => Equal(r.Request.RequestId, request.RequestId)))
throw new Exception("Received NewRequest event for id that already exists.");
var newRequest = new ChainStateRequest(log, request, RequestState.New);
requests.Add(newRequest);
handler.OnNewRequest(newRequest);
}
private void ApplyEvent(RequestFulfilledEventDTO request)
{
var r = FindRequest(request.RequestId);
if (r == null) return;
r.UpdateState(request.Block.BlockNumber, RequestState.Started);
handler.OnRequestFulfilled(r);
}
private void ApplyEvent(RequestCancelledEventDTO request)
{
var r = FindRequest(request.RequestId);
if (r == null) return;
r.UpdateState(request.Block.BlockNumber, RequestState.Cancelled);
handler.OnRequestCancelled(r);
}
private void ApplyEvent(SlotFilledEventDTO request)
{
var r = FindRequest(request.RequestId);
if (r == null) return;
r.Hosts.Add(request.Host, (int)request.SlotIndex);
r.Log($"[{request.Block.BlockNumber}] SlotFilled (host:'{request.Host}', slotIndex:{request.SlotIndex})");
handler.OnSlotFilled(r, request.SlotIndex);
}
private void ApplyEvent(SlotFreedEventDTO request)
{
var r = FindRequest(request.RequestId);
if (r == null) return;
r.Hosts.RemoveHost((int)request.SlotIndex);
r.Log($"[{request.Block.BlockNumber}] SlotFreed (slotIndex:{request.SlotIndex})");
handler.OnSlotFreed(r, request.SlotIndex);
}
private void ApplyTimeImplicitEvents(ulong blockNumber, DateTime eventsUtc)
{
foreach (var r in requests)
{
if (r.State == RequestState.Started
&& r.FinishedUtc < eventsUtc)
{
r.UpdateState(blockNumber, RequestState.Finished);
handler.OnRequestFinished(r);
}
}
}
private ChainStateRequest? FindRequest(byte[] requestId)
{
var r = requests.SingleOrDefault(r => Equal(r.Request.RequestId, requestId));
if (r == null) log.Log("Unable to find request by ID!");
return r;
}
private bool Equal(byte[] a, byte[] b)
{
return a.SequenceEqual(b);
}
}
}

View File

@ -0,0 +1,80 @@
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using Logging;
namespace CodexContractsPlugin.ChainMonitor
{
public interface IChainStateRequest
{
Request Request { get; }
RequestState State { get; }
DateTime ExpiryUtc { get; }
DateTime FinishedUtc { get; }
EthAddress Client { get; }
RequestHosts Hosts { get; }
}
public class ChainStateRequest : IChainStateRequest
{
private readonly ILog log;
public ChainStateRequest(ILog log, Request request, RequestState state)
{
this.log = log;
Request = request;
State = state;
ExpiryUtc = request.Block.Utc + TimeSpan.FromSeconds((double)request.Expiry);
FinishedUtc = request.Block.Utc + TimeSpan.FromSeconds((double)request.Ask.Duration);
Log($"[{request.Block.BlockNumber}] Created as {State}.");
Client = new EthAddress(request.Client);
Hosts = new RequestHosts();
}
public Request Request { get; }
public RequestState State { get; private set; }
public DateTime ExpiryUtc { get; }
public DateTime FinishedUtc { get; }
public EthAddress Client { get; }
public RequestHosts Hosts { get; }
public void UpdateState(ulong blockNumber, RequestState newState)
{
Log($"[{blockNumber}] Transit: {State} -> {newState}");
State = newState;
}
public void Log(string msg)
{
log.Log($"Request '{Request.Id}': {msg}");
}
}
public class RequestHosts
{
private readonly Dictionary<int, EthAddress> hosts = new Dictionary<int, EthAddress>();
public void Add(EthAddress host, int index)
{
hosts.Add(index, host);
}
public void RemoveHost(int index)
{
hosts.Remove(index);
}
public EthAddress? GetHost(int index)
{
if (!hosts.ContainsKey(index)) return null;
return hosts[index];
}
public EthAddress[] GetHosts()
{
return hosts.Values.ToArray();
}
}
}

View File

@ -0,0 +1,31 @@
using System.Numerics;
namespace CodexContractsPlugin.ChainMonitor
{
public class DoNothingChainEventHandler : IChainStateChangeHandler
{
public void OnNewRequest(IChainStateRequest request)
{
}
public void OnRequestCancelled(IChainStateRequest request)
{
}
public void OnRequestFinished(IChainStateRequest request)
{
}
public void OnRequestFulfilled(IChainStateRequest request)
{
}
public void OnSlotFilled(IChainStateRequest request, BigInteger slotIndex)
{
}
public void OnSlotFreed(IChainStateRequest request, BigInteger slotIndex)
{
}
}
}

View File

@ -2,10 +2,8 @@
using GethPlugin;
using Logging;
using Nethereum.ABI;
using Nethereum.Hex.HexTypes;
using Nethereum.Util;
using NethereumWorkflow;
using NethereumWorkflow.BlockUtils;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Utils;
@ -22,13 +20,10 @@ namespace CodexContractsPlugin
TestToken GetTestTokenBalance(IHasEthAddress owner);
TestToken GetTestTokenBalance(EthAddress ethAddress);
Request[] GetStorageRequests(BlockInterval blockRange);
ICodexContractsEvents GetEvents(TimeRange timeRange);
ICodexContractsEvents GetEvents(BlockInterval blockInterval);
EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex);
RequestState GetRequestState(Request request);
RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange);
RequestCancelledEventDTO[] GetRequestCancelledEvents(BlockInterval blockRange);
SlotFilledEventDTO[] GetSlotFilledEvents(BlockInterval blockRange);
SlotFreedEventDTO[] GetSlotFreedEvents(BlockInterval blockRange);
}
[JsonConverter(typeof(StringEnumConverter))]
@ -81,65 +76,14 @@ namespace CodexContractsPlugin
return balance.TstWei();
}
public Request[] GetStorageRequests(BlockInterval blockRange)
public ICodexContractsEvents GetEvents(TimeRange timeRange)
{
var events = gethNode.GetEvents<StorageRequestedEventDTO>(Deployment.MarketplaceAddress, blockRange);
var i = StartInteraction();
return events
.Select(e =>
{
var requestEvent = i.GetRequest(Deployment.MarketplaceAddress, e.Event.RequestId);
var result = requestEvent.ReturnValue1;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.RequestId = e.Event.RequestId;
return result;
})
.ToArray();
return GetEvents(gethNode.ConvertTimeRangeToBlockRange(timeRange));
}
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange)
public ICodexContractsEvents GetEvents(BlockInterval blockInterval)
{
var events = gethNode.GetEvents<RequestFulfilledEventDTO>(Deployment.MarketplaceAddress, blockRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
public RequestCancelledEventDTO[] GetRequestCancelledEvents(BlockInterval blockRange)
{
var events = gethNode.GetEvents<RequestCancelledEventDTO>(Deployment.MarketplaceAddress, blockRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
public SlotFilledEventDTO[] GetSlotFilledEvents(BlockInterval blockRange)
{
var events = gethNode.GetEvents<SlotFilledEventDTO>(Deployment.MarketplaceAddress, blockRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.Host = GetEthAddressFromTransaction(e.Log.TransactionHash);
return result;
}).ToArray();
}
public SlotFreedEventDTO[] GetSlotFreedEvents(BlockInterval blockRange)
{
var events = gethNode.GetEvents<SlotFreedEventDTO>(Deployment.MarketplaceAddress, blockRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
return new CodexContractsEvents(log, gethNode, Deployment, blockInterval);
}
public EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex)
@ -170,17 +114,6 @@ namespace CodexContractsPlugin
return gethNode.Call<RequestStateFunction, RequestState>(Deployment.MarketplaceAddress, func);
}
private BlockTimeEntry GetBlock(ulong number)
{
return gethNode.GetBlockForNumber(number);
}
private EthAddress GetEthAddressFromTransaction(string transactionHash)
{
var transaction = gethNode.GetTransaction(transactionHash);
return new EthAddress(transaction.From);
}
private ContractInteractions StartInteraction()
{
return new ContractInteractions(log, gethNode);

View File

@ -0,0 +1,108 @@
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using Logging;
using Nethereum.Hex.HexTypes;
using NethereumWorkflow.BlockUtils;
using Utils;
namespace CodexContractsPlugin
{
public interface ICodexContractsEvents
{
BlockInterval BlockInterval { get; }
Request[] GetStorageRequests();
RequestFulfilledEventDTO[] GetRequestFulfilledEvents();
RequestCancelledEventDTO[] GetRequestCancelledEvents();
SlotFilledEventDTO[] GetSlotFilledEvents();
SlotFreedEventDTO[] GetSlotFreedEvents();
}
public class CodexContractsEvents : ICodexContractsEvents
{
private readonly ILog log;
private readonly IGethNode gethNode;
private readonly CodexContractsDeployment deployment;
public CodexContractsEvents(ILog log, IGethNode gethNode, CodexContractsDeployment deployment, BlockInterval blockInterval)
{
this.log = log;
this.gethNode = gethNode;
this.deployment = deployment;
BlockInterval = blockInterval;
}
public BlockInterval BlockInterval { get; }
public Request[] GetStorageRequests()
{
var events = gethNode.GetEvents<StorageRequestedEventDTO>(deployment.MarketplaceAddress, BlockInterval);
var i = new ContractInteractions(log, gethNode);
return events
.Select(e =>
{
var requestEvent = i.GetRequest(deployment.MarketplaceAddress, e.Event.RequestId);
var result = requestEvent.ReturnValue1;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.RequestId = e.Event.RequestId;
return result;
})
.ToArray();
}
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents()
{
var events = gethNode.GetEvents<RequestFulfilledEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
public RequestCancelledEventDTO[] GetRequestCancelledEvents()
{
var events = gethNode.GetEvents<RequestCancelledEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
public SlotFilledEventDTO[] GetSlotFilledEvents()
{
var events = gethNode.GetEvents<SlotFilledEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.Host = GetEthAddressFromTransaction(e.Log.TransactionHash);
return result;
}).ToArray();
}
public SlotFreedEventDTO[] GetSlotFreedEvents()
{
var events = gethNode.GetEvents<SlotFreedEventDTO>(deployment.MarketplaceAddress, BlockInterval);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
return result;
}).ToArray();
}
private BlockTimeEntry GetBlock(ulong number)
{
return gethNode.GetBlockForNumber(number);
}
private EthAddress GetEthAddressFromTransaction(string transactionHash)
{
var transaction = gethNode.GetTransaction(transactionHash);
return new EthAddress(transaction.From);
}
}
}

View File

@ -5,35 +5,49 @@ using Newtonsoft.Json;
namespace CodexContractsPlugin.Marketplace
{
public partial class Request : RequestBase
public interface IHasBlock
{
BlockTimeEntry Block { get; set; }
}
public partial class Request : RequestBase, IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
public byte[] RequestId { get; set; }
public EthAddress ClientAddress { get { return new EthAddress(Client); } }
[JsonIgnore]
public string Id
{
get
{
return BitConverter.ToString(RequestId).Replace("-", "").ToLowerInvariant();
}
}
}
public partial class RequestFulfilledEventDTO
public partial class RequestFulfilledEventDTO : IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
}
public partial class RequestCancelledEventDTO
public partial class RequestCancelledEventDTO : IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
}
public partial class SlotFilledEventDTO
public partial class SlotFilledEventDTO : IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
public EthAddress Host { get; set; }
}
public partial class SlotFreedEventDTO
public partial class SlotFreedEventDTO : IHasBlock
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }

View File

@ -1,11 +1,13 @@
using Core;
using KubernetesWorkflow;
using KubernetesWorkflow.Types;
using Utils;
namespace CodexDiscordBotPlugin
{
public class CodexDiscordBotPlugin : IProjectPlugin, IHasLogPrefix, IHasMetadata
{
private const string ExpectedStartupMessage = "Debug option is set. Discord connection disabled!";
private readonly IPluginTools tools;
public CodexDiscordBotPlugin(IPluginTools tools)
@ -46,14 +48,59 @@ namespace CodexDiscordBotPlugin
var startupConfig = new StartupConfig();
startupConfig.NameOverride = config.Name;
startupConfig.Add(config);
return workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig).WaitForOnline();
var pod = workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig).WaitForOnline();
WaitForStartupMessage(workflow, pod);
workflow.CreateCrashWatcher(pod.Containers.Single()).Start();
return pod;
}
private RunningPod StartRewarderContainer(IStartupWorkflow workflow, RewarderBotStartupConfig config)
{
var startupConfig = new StartupConfig();
startupConfig.NameOverride = config.Name;
startupConfig.Add(config);
return workflow.Start(1, new RewarderBotContainerRecipe(), startupConfig).WaitForOnline();
var pod = workflow.Start(1, new RewarderBotContainerRecipe(), startupConfig).WaitForOnline();
workflow.CreateCrashWatcher(pod.Containers.Single()).Start();
return pod;
}
private void WaitForStartupMessage(IStartupWorkflow workflow, RunningPod pod)
{
var finder = new LogLineFinder(ExpectedStartupMessage, workflow);
Time.WaitUntil(() =>
{
finder.FindLine(pod);
return finder.Found;
}, nameof(WaitForStartupMessage));
}
public class LogLineFinder : LogHandler
{
private readonly string message;
private readonly IStartupWorkflow workflow;
public LogLineFinder(string message, IStartupWorkflow workflow)
{
this.message = message;
this.workflow = workflow;
}
public void FindLine(RunningPod pod)
{
Found = false;
foreach (var c in pod.Containers)
{
workflow.DownloadContainerLog(c, this);
if (Found) return;
}
}
public bool Found { get; private set; }
protected override void ProcessLine(string line)
{
if (!Found && line.Contains(message)) Found = true;
}
}
}
}

View File

@ -7,7 +7,7 @@ namespace CodexDiscordBotPlugin
public class DiscordBotContainerRecipe : ContainerRecipeFactory
{
public override string AppName => "discordbot-bibliotech";
public override string Image => "codexstorage/codex-discordbot:sha-8c64352";
public override string Image => "codexstorage/codex-discordbot:sha-22cf82b";
public static string RewardsPort = "bot_rewards_port";
@ -33,6 +33,8 @@ namespace CodexDiscordBotPlugin
AddEnvVar("CODEXCONTRACTS_TOKENADDRESS", gethInfo.TokenAddress);
AddEnvVar("CODEXCONTRACTS_ABI", gethInfo.Abi);
AddEnvVar("NODISCORD", "1");
AddInternalPortAndVar("REWARDAPIPORT", RewardsPort);
if (!string.IsNullOrEmpty(config.DataPath))

View File

@ -27,8 +27,9 @@
public class RewarderBotStartupConfig
{
public RewarderBotStartupConfig(string discordBotHost, int discordBotPort, string intervalMinutes, DateTime historyStartUtc, DiscordBotGethInfo gethInfo, string? dataPath)
public RewarderBotStartupConfig(string name, string discordBotHost, int discordBotPort, int intervalMinutes, DateTime historyStartUtc, DiscordBotGethInfo gethInfo, string? dataPath)
{
Name = name;
DiscordBotHost = discordBotHost;
DiscordBotPort = discordBotPort;
IntervalMinutes = intervalMinutes;
@ -37,9 +38,10 @@
DataPath = dataPath;
}
public string Name { get; }
public string DiscordBotHost { get; }
public int DiscordBotPort { get; }
public string IntervalMinutes { get; }
public int IntervalMinutes { get; }
public DateTime HistoryStartUtc { get; }
public DiscordBotGethInfo GethInfo { get; }
public string? DataPath { get; set; }

View File

@ -7,7 +7,8 @@ namespace CodexDiscordBotPlugin
public class RewarderBotContainerRecipe : ContainerRecipeFactory
{
public override string AppName => "discordbot-rewarder";
public override string Image => "codexstorage/codex-rewarderbot:sha-2ab84e2";
public override string Image => "thatbenbierens/codex-rewardbot:newstate";
//"codexstorage/codex-rewarderbot:sha-12dc7ef";
protected override void Initialize(StartupConfig startupConfig)
{
@ -17,7 +18,7 @@ namespace CodexDiscordBotPlugin
AddEnvVar("DISCORDBOTHOST", config.DiscordBotHost);
AddEnvVar("DISCORDBOTPORT", config.DiscordBotPort.ToString());
AddEnvVar("INTERVALMINUTES", config.IntervalMinutes);
AddEnvVar("INTERVALMINUTES", config.IntervalMinutes.ToString());
var offset = new DateTimeOffset(config.HistoryStartUtc);
AddEnvVar("CHECKHISTORY", offset.ToUnixTimeSeconds().ToString());

View File

@ -109,8 +109,9 @@ namespace CodexPlugin
// Custom scripting in the Codex test image will write this variable to a private-key file,
// and pass the correct filename to Codex.
AddEnvVar("PRIV_KEY", marketplaceSetup.EthAccount.PrivateKey);
Additional(marketplaceSetup.EthAccount);
var account = marketplaceSetup.EthAccountSetup.GetNew();
AddEnvVar("PRIV_KEY", account.PrivateKey);
Additional(account);
SetCommandOverride(marketplaceSetup);
if (marketplaceSetup.IsValidator)

View File

@ -26,13 +26,13 @@ namespace CodexPlugin
CrashWatcher CrashWatcher { get; }
PodInfo GetPodInfo();
ITransferSpeeds TransferSpeeds { get; }
EthAccount EthAccount { get; }
/// <summary>
/// Warning! The node is not usable after this.
/// TODO: Replace with delete-blocks debug call once available in Codex.
/// </summary>
void DeleteRepoFolder();
void Stop(bool waitTillStopped);
}
@ -40,13 +40,13 @@ namespace CodexPlugin
{
private const string UploadFailedMessage = "Unable to store block";
private readonly IPluginTools tools;
private readonly EthAddress? ethAddress;
private readonly EthAccount? ethAccount;
private readonly TransferSpeeds transferSpeeds;
public CodexNode(IPluginTools tools, CodexAccess codexAccess, CodexNodeGroup group, IMarketplaceAccess marketplaceAccess, EthAddress? ethAddress)
public CodexNode(IPluginTools tools, CodexAccess codexAccess, CodexNodeGroup group, IMarketplaceAccess marketplaceAccess, EthAccount? ethAccount)
{
this.tools = tools;
this.ethAddress = ethAddress;
this.ethAccount = ethAccount;
CodexAccess = codexAccess;
Group = group;
Marketplace = marketplaceAccess;
@ -76,8 +76,17 @@ namespace CodexPlugin
{
get
{
if (ethAddress == null) throw new Exception("Marketplace is not enabled for this Codex node. Please start it with the option '.EnableMarketplace(...)' to enable it.");
return ethAddress;
EnsureMarketplace();
return ethAccount!.EthAddress;
}
}
public EthAccount EthAccount
{
get
{
EnsureMarketplace();
return ethAccount!;
}
}
@ -231,6 +240,11 @@ namespace CodexPlugin
CodexAccess.LogDiskSpace("After download");
}
private void EnsureMarketplace()
{
if (ethAccount == null) throw new Exception("Marketplace is not enabled for this Codex node. Please start it with the option '.EnableMarketplace(...)' to enable it.");
}
private void Log(string msg)
{
tools.GetLog().Log($"{GetName()}: {msg}");

View File

@ -22,22 +22,22 @@ namespace CodexPlugin
public CodexNode CreateOnlineCodexNode(CodexAccess access, CodexNodeGroup group)
{
var ethAddress = GetEthAddress(access);
var marketplaceAccess = GetMarketplaceAccess(access, ethAddress);
return new CodexNode(tools, access, group, marketplaceAccess, ethAddress);
var ethAccount = GetEthAccount(access);
var marketplaceAccess = GetMarketplaceAccess(access, ethAccount);
return new CodexNode(tools, access, group, marketplaceAccess, ethAccount);
}
private IMarketplaceAccess GetMarketplaceAccess(CodexAccess codexAccess, EthAddress? ethAddress)
private IMarketplaceAccess GetMarketplaceAccess(CodexAccess codexAccess, EthAccount? ethAccount)
{
if (ethAddress == null) return new MarketplaceUnavailable();
if (ethAccount == null) return new MarketplaceUnavailable();
return new MarketplaceAccess(tools.GetLog(), codexAccess);
}
private EthAddress? GetEthAddress(CodexAccess access)
private EthAccount? GetEthAccount(CodexAccess access)
{
var ethAccount = access.Container.Containers.Single().Recipe.Additionals.Get<EthAccount>();
if (ethAccount == null) return null;
return ethAccount.EthAddress;
return ethAccount;
}
public CrashWatcher CreateCrashWatcher(RunningContainer c)

View File

@ -169,7 +169,7 @@ namespace CodexPlugin
public bool IsValidator { get; private set; }
public Ether InitialEth { get; private set; } = 0.Eth();
public TestToken InitialTestTokens { get; private set; } = 0.Tst();
public EthAccount EthAccount { get; private set; } = EthAccount.GenerateNew();
public EthAccountSetup EthAccountSetup { get; private set; } = new EthAccountSetup();
public IMarketplaceSetup AsStorageNode()
{
@ -185,7 +185,7 @@ namespace CodexPlugin
public IMarketplaceSetup WithAccount(EthAccount account)
{
EthAccount = account;
EthAccountSetup.Pin(account);
return this;
}
@ -201,10 +201,41 @@ namespace CodexPlugin
var result = "[(clientNode)"; // When marketplace is enabled, being a clientNode is implicit.
result += IsStorageNode ? "(storageNode)" : "()";
result += IsValidator ? "(validator)" : "() ";
result += $"Address: '{EthAccount.EthAddress}' ";
result += $"Address: '{EthAccountSetup}' ";
result += $"{InitialEth.Eth} / {InitialTestTokens}";
result += "] ";
return result;
}
}
public class EthAccountSetup
{
private readonly List<EthAccount> accounts = new List<EthAccount>();
private bool pinned = false;
public void Pin(EthAccount account)
{
accounts.Add(account);
pinned = true;
}
public EthAccount GetNew()
{
if (pinned) return accounts.Last();
var a = EthAccount.GenerateNew();
accounts.Add(a);
return a;
}
public EthAccount[] GetAll()
{
return accounts.ToArray();
}
public override string ToString()
{
return string.Join(",", accounts.Select(a => a.ToString()).ToArray());
}
}
}

View File

@ -1,5 +1,4 @@
using Logging;
using Newtonsoft.Json;
using Utils;
namespace CodexPlugin
@ -7,7 +6,7 @@ namespace CodexPlugin
public interface IMarketplaceAccess
{
string MakeStorageAvailable(StorageAvailability availability);
StoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase);
IStoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase);
}
public class MarketplaceAccess : IMarketplaceAccess
@ -21,7 +20,7 @@ namespace CodexPlugin
this.codexAccess = codexAccess;
}
public StoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase)
public IStoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase)
{
purchase.Log(log);
@ -68,7 +67,7 @@ namespace CodexPlugin
throw new NotImplementedException();
}
public StoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase)
public IStoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase)
{
Unavailable();
throw new NotImplementedException();
@ -80,134 +79,4 @@ namespace CodexPlugin
throw new InvalidOperationException();
}
}
public class StoragePurchaseContract
{
private readonly ILog log;
private readonly CodexAccess codexAccess;
private readonly TimeSpan gracePeriod = TimeSpan.FromSeconds(30);
private readonly DateTime contractPendingUtc = DateTime.UtcNow;
private DateTime? contractSubmittedUtc = DateTime.UtcNow;
private DateTime? contractStartedUtc;
private DateTime? contractFinishedUtc;
public StoragePurchaseContract(ILog log, CodexAccess codexAccess, string purchaseId, StoragePurchaseRequest purchase)
{
this.log = log;
this.codexAccess = codexAccess;
PurchaseId = purchaseId;
Purchase = purchase;
ContentId = new ContentId(codexAccess.GetPurchaseStatus(purchaseId).Request.Content.Cid);
}
public string PurchaseId { get; }
public StoragePurchaseRequest Purchase { get; }
public ContentId ContentId { get; }
public TimeSpan? PendingToSubmitted => contractSubmittedUtc - contractPendingUtc;
public TimeSpan? SubmittedToStarted => contractStartedUtc - contractSubmittedUtc;
public TimeSpan? SubmittedToFinished => contractFinishedUtc - contractSubmittedUtc;
public void WaitForStorageContractSubmitted()
{
WaitForStorageContractState(gracePeriod, "submitted", sleep: 200);
contractSubmittedUtc = DateTime.UtcNow;
LogSubmittedDuration();
AssertDuration(PendingToSubmitted, gracePeriod, nameof(PendingToSubmitted));
}
public void WaitForStorageContractStarted()
{
var timeout = Purchase.Expiry + gracePeriod;
WaitForStorageContractState(timeout, "started");
contractStartedUtc = DateTime.UtcNow;
LogStartedDuration();
AssertDuration(SubmittedToStarted, timeout, nameof(SubmittedToStarted));
}
public void WaitForStorageContractFinished()
{
if (!contractStartedUtc.HasValue)
{
WaitForStorageContractStarted();
}
var currentContractTime = DateTime.UtcNow - contractSubmittedUtc!.Value;
var timeout = (Purchase.Duration - currentContractTime) + gracePeriod;
WaitForStorageContractState(timeout, "finished");
contractFinishedUtc = DateTime.UtcNow;
LogFinishedDuration();
AssertDuration(SubmittedToFinished, timeout, nameof(SubmittedToFinished));
}
public StoragePurchase GetPurchaseStatus(string purchaseId)
{
return codexAccess.GetPurchaseStatus(purchaseId);
}
private void WaitForStorageContractState(TimeSpan timeout, string desiredState, int sleep = 1000)
{
var lastState = "";
var waitStart = DateTime.UtcNow;
Log($"Waiting for {Time.FormatDuration(timeout)} to reach state '{desiredState}'.");
while (lastState != desiredState)
{
var purchaseStatus = codexAccess.GetPurchaseStatus(PurchaseId);
var statusJson = JsonConvert.SerializeObject(purchaseStatus);
if (purchaseStatus != null && purchaseStatus.State != lastState)
{
lastState = purchaseStatus.State;
log.Debug("Purchase status: " + statusJson);
}
Thread.Sleep(sleep);
if (lastState == "errored")
{
FrameworkAssert.Fail("Contract errored: " + statusJson);
}
if (DateTime.UtcNow - waitStart > timeout)
{
FrameworkAssert.Fail($"Contract did not reach '{desiredState}' within {Time.FormatDuration(timeout)} timeout. {statusJson}");
}
}
}
private void LogSubmittedDuration()
{
Log($"Pending to Submitted in {Time.FormatDuration(PendingToSubmitted)} " +
$"( < {Time.FormatDuration(gracePeriod)})");
}
private void LogStartedDuration()
{
Log($"Submitted to Started in {Time.FormatDuration(SubmittedToStarted)} " +
$"( < {Time.FormatDuration(Purchase.Expiry + gracePeriod)})");
}
private void LogFinishedDuration()
{
Log($"Submitted to Finished in {Time.FormatDuration(SubmittedToFinished)} " +
$"( < {Time.FormatDuration(Purchase.Duration + gracePeriod)})");
}
private void AssertDuration(TimeSpan? span, TimeSpan max, string message)
{
if (span == null) throw new ArgumentNullException(nameof(MarketplaceAccess) + ": " + message + " (IsNull)");
if (span.Value.TotalDays >= max.TotalSeconds)
{
throw new Exception(nameof(MarketplaceAccess) +
$": Duration out of range. Max: {Time.FormatDuration(max)} but was: {Time.FormatDuration(span.Value)} " +
message);
}
}
private void Log(string msg)
{
log.Log($"[{PurchaseId}] {msg}");
}
}
}

View File

@ -0,0 +1,146 @@
using Logging;
using Newtonsoft.Json;
using Utils;
namespace CodexPlugin
{
public interface IStoragePurchaseContract
{
string PurchaseId { get; }
StoragePurchaseRequest Purchase { get; }
ContentId ContentId { get; }
void WaitForStorageContractSubmitted();
void WaitForStorageContractStarted();
void WaitForStorageContractFinished();
}
public class StoragePurchaseContract : IStoragePurchaseContract
{
private readonly ILog log;
private readonly CodexAccess codexAccess;
private readonly TimeSpan gracePeriod = TimeSpan.FromSeconds(30);
private readonly DateTime contractPendingUtc = DateTime.UtcNow;
private DateTime? contractSubmittedUtc = DateTime.UtcNow;
private DateTime? contractStartedUtc;
private DateTime? contractFinishedUtc;
public StoragePurchaseContract(ILog log, CodexAccess codexAccess, string purchaseId, StoragePurchaseRequest purchase)
{
this.log = log;
this.codexAccess = codexAccess;
PurchaseId = purchaseId;
Purchase = purchase;
ContentId = new ContentId(codexAccess.GetPurchaseStatus(purchaseId).Request.Content.Cid);
}
public string PurchaseId { get; }
public StoragePurchaseRequest Purchase { get; }
public ContentId ContentId { get; }
public TimeSpan? PendingToSubmitted => contractSubmittedUtc - contractPendingUtc;
public TimeSpan? SubmittedToStarted => contractStartedUtc - contractSubmittedUtc;
public TimeSpan? SubmittedToFinished => contractFinishedUtc - contractSubmittedUtc;
public void WaitForStorageContractSubmitted()
{
WaitForStorageContractState(gracePeriod, "submitted", sleep: 200);
contractSubmittedUtc = DateTime.UtcNow;
LogSubmittedDuration();
AssertDuration(PendingToSubmitted, gracePeriod, nameof(PendingToSubmitted));
}
public void WaitForStorageContractStarted()
{
var timeout = Purchase.Expiry + gracePeriod;
WaitForStorageContractState(timeout, "started");
contractStartedUtc = DateTime.UtcNow;
LogStartedDuration();
AssertDuration(SubmittedToStarted, timeout, nameof(SubmittedToStarted));
}
public void WaitForStorageContractFinished()
{
if (!contractStartedUtc.HasValue)
{
WaitForStorageContractStarted();
}
var currentContractTime = DateTime.UtcNow - contractSubmittedUtc!.Value;
var timeout = (Purchase.Duration - currentContractTime) + gracePeriod;
WaitForStorageContractState(timeout, "finished");
contractFinishedUtc = DateTime.UtcNow;
LogFinishedDuration();
AssertDuration(SubmittedToFinished, timeout, nameof(SubmittedToFinished));
}
public StoragePurchase GetPurchaseStatus(string purchaseId)
{
return codexAccess.GetPurchaseStatus(purchaseId);
}
private void WaitForStorageContractState(TimeSpan timeout, string desiredState, int sleep = 1000)
{
var lastState = "";
var waitStart = DateTime.UtcNow;
Log($"Waiting for {Time.FormatDuration(timeout)} to reach state '{desiredState}'.");
while (lastState != desiredState)
{
var purchaseStatus = codexAccess.GetPurchaseStatus(PurchaseId);
var statusJson = JsonConvert.SerializeObject(purchaseStatus);
if (purchaseStatus != null && purchaseStatus.State != lastState)
{
lastState = purchaseStatus.State;
log.Debug("Purchase status: " + statusJson);
}
Thread.Sleep(sleep);
if (lastState == "errored")
{
FrameworkAssert.Fail("Contract errored: " + statusJson);
}
if (DateTime.UtcNow - waitStart > timeout)
{
FrameworkAssert.Fail($"Contract did not reach '{desiredState}' within {Time.FormatDuration(timeout)} timeout. {statusJson}");
}
}
}
private void LogSubmittedDuration()
{
Log($"Pending to Submitted in {Time.FormatDuration(PendingToSubmitted)} " +
$"( < {Time.FormatDuration(gracePeriod)})");
}
private void LogStartedDuration()
{
Log($"Submitted to Started in {Time.FormatDuration(SubmittedToStarted)} " +
$"( < {Time.FormatDuration(Purchase.Expiry + gracePeriod)})");
}
private void LogFinishedDuration()
{
Log($"Submitted to Finished in {Time.FormatDuration(SubmittedToFinished)} " +
$"( < {Time.FormatDuration(Purchase.Duration + gracePeriod)})");
}
private void AssertDuration(TimeSpan? span, TimeSpan max, string message)
{
if (span == null) throw new ArgumentNullException(nameof(MarketplaceAccess) + ": " + message + " (IsNull)");
if (span.Value.TotalDays >= max.TotalSeconds)
{
throw new Exception(nameof(MarketplaceAccess) +
$": Duration out of range. Max: {Time.FormatDuration(max)} but was: {Time.FormatDuration(span.Value)} " +
message);
}
}
private void Log(string msg)
{
log.Log($"[{PurchaseId}] {msg}");
}
}
}

View File

@ -24,5 +24,10 @@ namespace GethPlugin
return new EthAccount(ethAddress, account.PrivateKey);
}
public override string ToString()
{
return EthAddress.ToString();
}
}
}

View File

@ -65,8 +65,8 @@ namespace CodexTests.BasicTests
MinRequiredNumberOfNodes = 5,
NodeFailureTolerance = 2,
ProofProbability = 5,
Duration = TimeSpan.FromMinutes(5),
Expiry = TimeSpan.FromMinutes(4)
Duration = TimeSpan.FromMinutes(6),
Expiry = TimeSpan.FromMinutes(5)
};
var purchaseContract = client.Marketplace.RequestStorage(purchase);
@ -129,8 +129,8 @@ namespace CodexTests.BasicTests
{
Time.Retry(() =>
{
var blockRange = geth.ConvertTimeRangeToBlockRange(GetTestRunTimeRange());
var slotFilledEvents = contracts.GetSlotFilledEvents(blockRange);
var events = contracts.GetEvents(GetTestRunTimeRange());
var slotFilledEvents = events.GetSlotFilledEvents();
var msg = $"SlotFilledEvents: {slotFilledEvents.Length} - NumSlots: {purchase.MinRequiredNumberOfNodes}";
Debug(msg);
@ -147,7 +147,8 @@ namespace CodexTests.BasicTests
private Request GetOnChainStorageRequest(ICodexContracts contracts, IGethNode geth)
{
var requests = contracts.GetStorageRequests(geth.ConvertTimeRangeToBlockRange(GetTestRunTimeRange()));
var events = contracts.GetEvents(GetTestRunTimeRange());
var requests = events.GetStorageRequests();
Assert.That(requests.Length, Is.EqualTo(1));
return requests.Single();
}

View File

@ -13,11 +13,13 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Framework\DiscordRewards\DiscordRewards.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexContractsPlugin\CodexContractsPlugin.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexDiscordBotPlugin\CodexDiscordBotPlugin.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\CodexPlugin\CodexPlugin.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\GethPlugin\GethPlugin.csproj" />
<ProjectReference Include="..\..\ProjectPlugins\MetricsPlugin\MetricsPlugin.csproj" />
<ProjectReference Include="..\..\Tools\TestNetRewarder\TestNetRewarder.csproj" />
<ProjectReference Include="..\DistTestCore\DistTestCore.csproj" />
</ItemGroup>

View File

@ -1,7 +1,13 @@
using CodexContractsPlugin;
using CodexDiscordBotPlugin;
using CodexPlugin;
using Core;
using DiscordRewards;
using DistTestCore;
using GethPlugin;
using KubernetesWorkflow.Types;
using Logging;
using Newtonsoft.Json;
using NUnit.Framework;
using Utils;
@ -10,21 +16,155 @@ namespace CodexTests.UtilityTests
[TestFixture]
public class DiscordBotTests : AutoBootstrapDistTest
{
private readonly RewardRepo repo = new RewardRepo();
private readonly TestToken hostInitialBalance = 3000000.TstWei();
private readonly TestToken clientInitialBalance = 1000000000.TstWei();
private readonly EthAccount clientAccount = EthAccount.GenerateNew();
private readonly List<EthAccount> hostAccounts = new List<EthAccount>();
private readonly List<ulong> rewardsSeen = new List<ulong>();
private readonly TimeSpan rewarderInterval = TimeSpan.FromMinutes(1);
private readonly List<string> receivedEvents = new List<string>();
private readonly List<MarketAverage> receivedAverages = new List<MarketAverage>();
[Test]
[Ignore("Used for debugging bots")]
[DontDownloadLogs]
public void BotRewardTest()
{
var myAccount = EthAccount.GenerateNew();
var sellerInitialBalance = 234.TstWei();
var buyerInitialBalance = 100000.TstWei();
var fileSize = 11.MB();
var geth = Ci.StartGethNode(s => s.IsMiner().WithName("disttest-geth"));
var contracts = Ci.StartCodexContracts(geth);
var gethInfo = CreateGethInfo(geth, contracts);
// start bot and rewarder
var gethInfo = new DiscordBotGethInfo(
var botContainer = StartDiscordBot(gethInfo);
var rewarderContainer = StartRewarderBot(gethInfo, botContainer);
StartHosts(geth, contracts);
var client = StartClient(geth, contracts);
var apiCalls = new RewardApiCalls(GetTestLog(), Ci, botContainer);
apiCalls.Start(OnCommand);
var purchaseContract = ClientPurchasesStorage(client);
purchaseContract.WaitForStorageContractStarted();
purchaseContract.WaitForStorageContractFinished();
Thread.Sleep(rewarderInterval * 3);
apiCalls.Stop();
AssertEventOccurance("Created as New.", 1);
AssertEventOccurance("SlotFilled", Convert.ToInt32(GetNumberOfRequiredHosts()));
AssertEventOccurance("Transit: New -> Started", 1);
AssertEventOccurance("Transit: Started -> Finished", 1);
AssertMarketAverage();
foreach (var r in repo.Rewards)
{
var seen = rewardsSeen.Any(s => r.RoleId == s);
Log($"{Lookup(r.RoleId)} = {seen}");
}
Assert.That(repo.Rewards.All(r => rewardsSeen.Contains(r.RoleId)));
}
private string Lookup(ulong rewardId)
{
var reward = repo.Rewards.Single(r => r.RoleId == rewardId);
return $"({rewardId})'{reward.Message}'";
}
private void AssertEventOccurance(string msg, int expectedCount)
{
Assert.That(receivedEvents.Count(e => e.Contains(msg)), Is.EqualTo(expectedCount),
$"Event '{msg}' did not occure correct number of times.");
}
private void AssertMarketAverage()
{
Assert.That(receivedAverages.Count, Is.EqualTo(1));
var a = receivedAverages.Single();
Assert.That(a.NumberOfFinished, Is.EqualTo(1));
Assert.That(a.TimeRangeSeconds, Is.EqualTo(5760));
Assert.That(a.Price, Is.EqualTo(2.0f).Within(0.1f));
Assert.That(a.Size, Is.EqualTo(GetMinFileSize().SizeInBytes).Within(1.0f));
Assert.That(a.Duration, Is.EqualTo(GetMinRequiredRequestDuration().TotalSeconds).Within(1.0f));
Assert.That(a.Collateral, Is.EqualTo(10.0f).Within(0.1f));
Assert.That(a.ProofProbability, Is.EqualTo(5.0f).Within(0.1f));
}
private void OnCommand(string timestamp, GiveRewardsCommand call)
{
Log($"<API call {timestamp}>");
receivedAverages.AddRange(call.Averages);
foreach (var a in call.Averages)
{
Log("\tAverage: " + JsonConvert.SerializeObject(a));
}
receivedEvents.AddRange(call.EventsOverview);
foreach (var e in call.EventsOverview)
{
Log("\tEvent: " + e);
}
foreach (var r in call.Rewards)
{
var reward = repo.Rewards.Single(a => a.RoleId == r.RewardId);
if (r.UserAddresses.Any()) rewardsSeen.Add(reward.RoleId);
foreach (var address in r.UserAddresses)
{
var user = IdentifyAccount(address);
Log("\tReward: " + user + ": " + reward.Message);
}
}
Log($"</API call>");
}
private IStoragePurchaseContract ClientPurchasesStorage(ICodexNode client)
{
var testFile = GenerateTestFile(GetMinFileSize());
var contentId = client.UploadFile(testFile);
var purchase = new StoragePurchaseRequest(contentId)
{
PricePerSlotPerSecond = 2.TstWei(),
RequiredCollateral = 10.TstWei(),
MinRequiredNumberOfNodes = GetNumberOfRequiredHosts(),
NodeFailureTolerance = 2,
ProofProbability = 5,
Duration = GetMinRequiredRequestDuration(),
Expiry = GetMinRequiredRequestDuration() - TimeSpan.FromMinutes(1)
};
return client.Marketplace.RequestStorage(purchase);
}
private ICodexNode StartClient(IGethNode geth, ICodexContracts contracts)
{
var node = StartCodex(s => s
.WithName("Client")
.EnableMarketplace(geth, contracts, m => m
.WithAccount(clientAccount)
.WithInitial(10.Eth(), clientInitialBalance)));
Log($"Client {node.EthAccount.EthAddress}");
return node;
}
private RunningPod StartRewarderBot(DiscordBotGethInfo gethInfo, RunningContainer botContainer)
{
return Ci.DeployRewarderBot(new RewarderBotStartupConfig(
name: "rewarder-bot",
discordBotHost: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Host,
discordBotPort: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Port,
intervalMinutes: Convert.ToInt32(Math.Round(rewarderInterval.TotalMinutes)),
historyStartUtc: DateTime.UtcNow,
gethInfo: gethInfo,
dataPath: null
));
}
private DiscordBotGethInfo CreateGethInfo(IGethNode geth, ICodexContracts contracts)
{
return new DiscordBotGethInfo(
host: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Host,
port: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Port,
privKey: geth.StartResult.Account.PrivateKey,
@ -32,8 +172,12 @@ namespace CodexTests.UtilityTests
tokenAddress: contracts.Deployment.TokenAddress,
abi: contracts.Deployment.Abi
);
}
private RunningContainer StartDiscordBot(DiscordBotGethInfo gethInfo)
{
var bot = Ci.DeployCodexDiscordBot(new DiscordBotStartupConfig(
name: "bot",
name: "discord-bot",
token: "aaa",
serverName: "ThatBen's server",
adminRoleName: "bottest-admins",
@ -42,70 +186,190 @@ namespace CodexTests.UtilityTests
kubeNamespace: "notneeded",
gethInfo: gethInfo
));
var botContainer = bot.Containers.Single();
Ci.DeployRewarderBot(new RewarderBotStartupConfig(
//discordBotHost: "http://" + botContainer.GetAddress(GetTestLog(), DiscordBotContainerRecipe.RewardsPort).Host,
//discordBotPort: botContainer.GetAddress(GetTestLog(), DiscordBotContainerRecipe.RewardsPort).Port,
discordBotHost: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Host,
discordBotPort: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Port,
intervalMinutes: "1",
historyStartUtc: GetTestRunTimeRange().From - TimeSpan.FromMinutes(3),
gethInfo: gethInfo,
dataPath: null
));
return bot.Containers.Single();
}
var numberOfHosts = 3;
private void StartHosts(IGethNode geth, ICodexContracts contracts)
{
var hosts = StartCodex(GetNumberOfLiveHosts(), s => s
.WithName("Host")
.WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn)
{
ContractClock = CodexLogLevel.Trace,
})
.WithStorageQuota(Mult(GetMinFileSizePlus(50), GetNumberOfLiveHosts()))
.EnableMarketplace(geth, contracts, m => m
.WithInitial(10.Eth(), hostInitialBalance)
.AsStorageNode()
.AsValidator()));
for (var i = 0; i < numberOfHosts; i++)
var availability = new StorageAvailability(
totalSpace: Mult(GetMinFileSize(), GetNumberOfLiveHosts()),
maxDuration: TimeSpan.FromMinutes(30),
minPriceForTotalSpace: 1.TstWei(),
maxCollateral: hostInitialBalance
);
foreach (var host in hosts)
{
var seller = StartCodex(s => s
.WithName("Seller")
.WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn)
{
ContractClock = CodexLogLevel.Trace,
})
.WithStorageQuota(11.GB())
.EnableMarketplace(geth, contracts, m => m
.WithAccount(myAccount)
.WithInitial(10.Eth(), sellerInitialBalance)
.AsStorageNode()
.AsValidator()));
hostAccounts.Add(host.EthAccount);
host.Marketplace.MakeStorageAvailable(availability);
}
}
var availability = new StorageAvailability(
totalSpace: 10.GB(),
maxDuration: TimeSpan.FromMinutes(30),
minPriceForTotalSpace: 1.TstWei(),
maxCollateral: 20.TstWei()
);
seller.Marketplace.MakeStorageAvailable(availability);
private int GetNumberOfLiveHosts()
{
return Convert.ToInt32(GetNumberOfRequiredHosts()) + 3;
}
private ByteSize Mult(ByteSize size, int mult)
{
return new ByteSize(size.SizeInBytes * mult);
}
private ByteSize GetMinFileSizePlus(int plusMb)
{
return new ByteSize(GetMinFileSize().SizeInBytes + plusMb.MB().SizeInBytes);
}
private ByteSize GetMinFileSize()
{
ulong minSlotSize = 0;
ulong minNumHosts = 0;
foreach (var r in repo.Rewards)
{
var s = Convert.ToUInt64(r.CheckConfig.MinSlotSize.SizeInBytes);
var h = r.CheckConfig.MinNumberOfHosts;
if (s > minSlotSize) minSlotSize = s;
if (h > minNumHosts) minNumHosts = h;
}
var testFile = GenerateTestFile(fileSize);
var minFileSize = ((minSlotSize + 1024) * minNumHosts);
return new ByteSize(Convert.ToInt64(minFileSize));
}
var buyer = StartCodex(s => s
.WithName("Buyer")
.EnableMarketplace(geth, contracts, m => m
.WithAccount(myAccount)
.WithInitial(10.Eth(), buyerInitialBalance)));
private uint GetNumberOfRequiredHosts()
{
return Convert.ToUInt32(repo.Rewards.Max(r => r.CheckConfig.MinNumberOfHosts));
}
var contentId = buyer.UploadFile(testFile);
private TimeSpan GetMinRequiredRequestDuration()
{
return repo.Rewards.Max(r => r.CheckConfig.MinDuration) + TimeSpan.FromSeconds(10);
}
var purchase = new StoragePurchaseRequest(contentId)
private string IdentifyAccount(string address)
{
if (address == clientAccount.EthAddress.Address) return "Client";
try
{
PricePerSlotPerSecond = 2.TstWei(),
RequiredCollateral = 10.TstWei(),
MinRequiredNumberOfNodes = 5,
NodeFailureTolerance = 2,
ProofProbability = 5,
Duration = TimeSpan.FromMinutes(6),
Expiry = TimeSpan.FromMinutes(5)
};
var index = hostAccounts.FindIndex(a => a.EthAddress.Address == address);
return "Host" + index;
}
catch
{
return "UNKNOWN";
}
}
var purchaseContract = buyer.Marketplace.RequestStorage(purchase);
public class RewardApiCalls
{
private readonly ContainerFileMonitor monitor;
purchaseContract.WaitForStorageContractStarted();
public RewardApiCalls(ILog log, CoreInterface ci, RunningContainer botContainer)
{
monitor = new ContainerFileMonitor(log, ci, botContainer, "/app/datapath/logs/discordbot.log");
}
purchaseContract.WaitForStorageContractFinished();
public void Start(Action<string, GiveRewardsCommand> onCommand)
{
monitor.Start(line => ParseLine(line, onCommand));
}
public void Stop()
{
monitor.Stop();
}
private void ParseLine(string line, Action<string, GiveRewardsCommand> onCommand)
{
try
{
var timestamp = line.Substring(0, 30);
var json = line.Substring(31);
var cmd = JsonConvert.DeserializeObject<GiveRewardsCommand>(json);
if (cmd != null)
{
onCommand(timestamp, cmd);
}
}
catch
{
}
}
}
public class ContainerFileMonitor
{
private readonly ILog log;
private readonly CoreInterface ci;
private readonly RunningContainer botContainer;
private readonly string filePath;
private readonly CancellationTokenSource cts = new CancellationTokenSource();
private readonly List<string> seenLines = new List<string>();
private Task worker = Task.CompletedTask;
private Action<string> onNewLine = c => { };
public ContainerFileMonitor(ILog log, CoreInterface ci, RunningContainer botContainer, string filePath)
{
this.log = log;
this.ci = ci;
this.botContainer = botContainer;
this.filePath = filePath;
}
public void Start(Action<string> onNewLine)
{
this.onNewLine = onNewLine;
worker = Task.Run(Worker);
}
public void Stop()
{
cts.Cancel();
worker.Wait();
}
// did any container crash? that's why it repeats?
private void Worker()
{
while (!cts.IsCancellationRequested)
{
Update();
}
}
private void Update()
{
Thread.Sleep(TimeSpan.FromSeconds(10));
if (cts.IsCancellationRequested) return;
var botLog = ci.ExecuteContainerCommand(botContainer, "cat", filePath);
var lines = botLog.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
// log.Log("line: " + line);
if (!seenLines.Contains(line))
{
seenLines.Add(line);
onNewLine(line);
}
}
}
}
}
}

View File

@ -35,28 +35,12 @@ namespace BiblioTech
[Uniform("mint-tt", "mt", "MINTTT", true, "Amount of TSTWEI minted by the mint command.")]
public BigInteger MintTT { get; set; } = 1073741824;
public string EndpointsPath
{
get
{
return Path.Combine(DataPath, "endpoints");
}
}
[Uniform("no-discord", "nd", "NODISCORD", false, "For debugging: Bypasses all Discord API calls.")]
public int NoDiscord { get; set; } = 0;
public string UserDataPath
{
get
{
return Path.Combine(DataPath, "users");
}
}
public string LogPath
{
get
{
return Path.Combine(DataPath, "logs");
}
}
public string EndpointsPath => Path.Combine(DataPath, "endpoints");
public string UserDataPath => Path.Combine(DataPath, "users");
public string LogPath => Path.Combine(DataPath, "logs");
public bool DebugNoDiscord => NoDiscord == 1;
}
}

View File

@ -0,0 +1,24 @@
using BiblioTech.Rewards;
using DiscordRewards;
using Logging;
using Newtonsoft.Json;
namespace BiblioTech
{
public class LoggingRoleDriver : IDiscordRoleDriver
{
private readonly ILog log;
public LoggingRoleDriver(ILog log)
{
this.log = log;
}
public async Task GiveRewards(GiveRewardsCommand rewards)
{
await Task.CompletedTask;
log.Log(JsonConvert.SerializeObject(rewards, Formatting.None));
}
}
}

View File

@ -41,25 +41,15 @@ namespace BiblioTech
public async Task MainAsync(string[] args)
{
Log.Log("Starting Codex Discord Bot...");
client = new DiscordSocketClient();
client.Log += ClientLog;
var notifyCommand = new NotifyCommand();
var associateCommand = new UserAssociateCommand(notifyCommand);
var sprCommand = new SprCommand();
var handler = new CommandHandler(client,
new GetBalanceCommand(associateCommand),
new MintCommand(associateCommand),
sprCommand,
associateCommand,
notifyCommand,
new AdminCommand(sprCommand),
new MarketCommand()
);
await client.LoginAsync(TokenType.Bot, Config.ApplicationToken);
await client.StartAsync();
AdminChecker = new AdminChecker();
if (Config.DebugNoDiscord)
{
Log.Log("Debug option is set. Discord connection disabled!");
RoleDriver = new LoggingRoleDriver(Log);
}
else
{
await StartDiscordBot();
}
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel((context, options) =>
@ -75,6 +65,29 @@ namespace BiblioTech
await Task.Delay(-1);
}
private async Task StartDiscordBot()
{
client = new DiscordSocketClient();
client.Log += ClientLog;
var notifyCommand = new NotifyCommand();
var associateCommand = new UserAssociateCommand(notifyCommand);
var sprCommand = new SprCommand();
var handler = new CommandHandler(client,
new GetBalanceCommand(associateCommand),
new MintCommand(associateCommand),
sprCommand,
associateCommand,
notifyCommand,
new AdminCommand(sprCommand),
new MarketCommand()
);
await client.LoginAsync(TokenType.Bot, Config.ApplicationToken);
await client.StartAsync();
AdminChecker = new AdminChecker();
}
private static void PrintHelp()
{
Log.Log("BiblioTech - Codex Discord Bot");

View File

@ -0,0 +1,41 @@
using Logging;
namespace TestNetRewarder
{
public class BufferLogger : ILog
{
private readonly List<string> lines = new List<string>();
public void AddStringReplace(string from, string to)
{
throw new NotImplementedException();
}
public LogFile CreateSubfile(string ext = "log")
{
throw new NotImplementedException();
}
public void Debug(string message = "", int skipFrames = 0)
{
lines.Add(message);
}
public void Error(string message)
{
lines.Add($"Error: {message}");
}
public void Log(string message)
{
lines.Add(message);
}
public string[] Get()
{
var result = lines.ToArray();
lines.Clear();
return result;
}
}
}

View File

@ -0,0 +1,45 @@
using CodexContractsPlugin.ChainMonitor;
using System.Numerics;
namespace TestNetRewarder
{
public class ChainChangeMux : IChainStateChangeHandler
{
private readonly IChainStateChangeHandler[] handlers;
public ChainChangeMux(params IChainStateChangeHandler[] handlers)
{
this.handlers = handlers;
}
public void OnNewRequest(IChainStateRequest request)
{
foreach (var handler in handlers) handler.OnNewRequest(request);
}
public void OnRequestCancelled(IChainStateRequest request)
{
foreach (var handler in handlers) handler.OnRequestCancelled(request);
}
public void OnRequestFinished(IChainStateRequest request)
{
foreach (var handler in handlers) handler.OnRequestFinished(request);
}
public void OnRequestFulfilled(IChainStateRequest request)
{
foreach (var handler in handlers) handler.OnRequestFulfilled(request);
}
public void OnSlotFilled(IChainStateRequest request, BigInteger slotIndex)
{
foreach (var handler in handlers) handler.OnSlotFilled(request, slotIndex);
}
public void OnSlotFreed(IChainStateRequest request, BigInteger slotIndex)
{
foreach (var handler in handlers) handler.OnSlotFreed(request, slotIndex);
}
}
}

View File

@ -1,151 +0,0 @@
using CodexContractsPlugin;
using CodexContractsPlugin.Marketplace;
using NethereumWorkflow.BlockUtils;
using Newtonsoft.Json;
using Utils;
namespace TestNetRewarder
{
public class ChainState
{
private readonly HistoricState historicState;
private readonly string[] colorIcons = new[]
{
"🔴",
"🟠",
"🟡",
"🟢",
"🔵",
"🟣",
"🟤",
"⚫",
"⚪",
"🟥",
"🟧",
"🟨",
"🟩",
"🟦",
"🟪",
"🟫",
"⬛",
"⬜",
"🔶",
"🔷"
};
public ChainState(HistoricState historicState, ICodexContracts contracts, BlockInterval blockRange)
{
this.historicState = historicState;
NewRequests = contracts.GetStorageRequests(blockRange);
historicState.CleanUpOldRequests();
historicState.ProcessNewRequests(NewRequests);
historicState.UpdateStorageRequests(contracts);
StartedRequests = historicState.StorageRequests.Where(r => r.RecentlyStarted).ToArray();
FinishedRequests = historicState.StorageRequests.Where(r => r.RecentlyFinished).ToArray();
RequestFulfilledEvents = contracts.GetRequestFulfilledEvents(blockRange);
RequestCancelledEvents = contracts.GetRequestCancelledEvents(blockRange);
SlotFilledEvents = contracts.GetSlotFilledEvents(blockRange);
SlotFreedEvents = contracts.GetSlotFreedEvents(blockRange);
}
public Request[] NewRequests { get; }
public StorageRequest[] AllRequests => historicState.StorageRequests;
public StorageRequest[] StartedRequests { get; private set; }
public StorageRequest[] FinishedRequests { get; private set; }
public RequestFulfilledEventDTO[] RequestFulfilledEvents { get; }
public RequestCancelledEventDTO[] RequestCancelledEvents { get; }
public SlotFilledEventDTO[] SlotFilledEvents { get; }
public SlotFreedEventDTO[] SlotFreedEvents { get; }
public string[] GenerateOverview()
{
var entries = new List<StringBlockNumberPair>();
entries.AddRange(NewRequests.Select(ToPair));
entries.AddRange(RequestFulfilledEvents.Select(ToPair));
entries.AddRange(RequestCancelledEvents.Select(ToPair));
entries.AddRange(SlotFilledEvents.Select(ToPair));
entries.AddRange(SlotFreedEvents.Select(ToPair));
entries.AddRange(FinishedRequests.Select(ToPair));
entries.Sort(new StringUtcComparer());
return entries.Select(ToLine).ToArray();
}
private StringBlockNumberPair ToPair(Request r)
{
return new StringBlockNumberPair("NewRequest", JsonConvert.SerializeObject(r), r.Block, r.RequestId);
}
public StringBlockNumberPair ToPair(StorageRequest r)
{
return new StringBlockNumberPair("FinishedRequest", JsonConvert.SerializeObject(r), r.Request.Block, r.Request.RequestId);
}
private StringBlockNumberPair ToPair(RequestFulfilledEventDTO r)
{
return new StringBlockNumberPair("Fulfilled", JsonConvert.SerializeObject(r), r.Block, r.RequestId);
}
private StringBlockNumberPair ToPair(RequestCancelledEventDTO r)
{
return new StringBlockNumberPair("Cancelled", JsonConvert.SerializeObject(r), r.Block, r.RequestId);
}
private StringBlockNumberPair ToPair(SlotFilledEventDTO r)
{
return new StringBlockNumberPair("SlotFilled", JsonConvert.SerializeObject(r), r.Block, r.RequestId);
}
private StringBlockNumberPair ToPair(SlotFreedEventDTO r)
{
return new StringBlockNumberPair("SlotFreed", JsonConvert.SerializeObject(r), r.Block, r.RequestId);
}
private string ToLine(StringBlockNumberPair pair)
{
var nl = Environment.NewLine;
var colorIcon = GetColorIcon(pair.RequestId);
return $"{colorIcon} {pair.Block} ({pair.Name}){nl}" +
$"```json{nl}" +
$"{pair.Str}{nl}" +
$"```";
}
private string GetColorIcon(byte[] requestId)
{
var index = requestId[0] % colorIcons.Length;
return colorIcons[index];
}
public class StringBlockNumberPair
{
public StringBlockNumberPair(string name, string str, BlockTimeEntry block, byte[] requestId)
{
Name = name;
Str = str;
Block = block;
RequestId = requestId;
}
public string Name { get; }
public string Str { get; }
public BlockTimeEntry Block { get; }
public byte[] RequestId { get; }
}
public class StringUtcComparer : IComparer<StringBlockNumberPair>
{
public int Compare(StringBlockNumberPair? x, StringBlockNumberPair? y)
{
if (x == null && y == null) return 0;
if (x == null) return 1;
if (y == null) return -1;
return x.Block.BlockNumber.CompareTo(y.Block.BlockNumber);
}
}
}
}

View File

@ -1,141 +0,0 @@
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using NethereumWorkflow;
using Utils;
namespace TestNetRewarder
{
public interface ICheck
{
EthAddress[] Check(ChainState state);
}
public class FilledAnySlotCheck : ICheck
{
public EthAddress[] Check(ChainState state)
{
return state.SlotFilledEvents.Select(e => e.Host).ToArray();
}
}
public class FinishedSlotCheck : ICheck
{
private readonly ByteSize minSize;
private readonly TimeSpan minDuration;
public FinishedSlotCheck(ByteSize minSize, TimeSpan minDuration)
{
this.minSize = minSize;
this.minDuration = minDuration;
}
public EthAddress[] Check(ChainState state)
{
return state.FinishedRequests
.Where(r =>
MeetsSizeRequirement(r) &&
MeetsDurationRequirement(r))
.SelectMany(r => r.Hosts)
.ToArray();
}
private bool MeetsSizeRequirement(StorageRequest r)
{
var slotSize = r.Request.Ask.SlotSize.ToDecimal();
decimal min = minSize.SizeInBytes;
return slotSize >= min;
}
private bool MeetsDurationRequirement(StorageRequest r)
{
var duration = TimeSpan.FromSeconds((double)r.Request.Ask.Duration);
return duration >= minDuration;
}
}
public class PostedContractCheck : ICheck
{
private readonly ulong minNumberOfHosts;
private readonly ByteSize minSlotSize;
private readonly TimeSpan minDuration;
public PostedContractCheck(ulong minNumberOfHosts, ByteSize minSlotSize, TimeSpan minDuration)
{
this.minNumberOfHosts = minNumberOfHosts;
this.minSlotSize = minSlotSize;
this.minDuration = minDuration;
}
public EthAddress[] Check(ChainState state)
{
return state.NewRequests
.Where(r =>
MeetsNumSlotsRequirement(r) &&
MeetsSizeRequirement(r) &&
MeetsDurationRequirement(r))
.Select(r => r.ClientAddress)
.ToArray();
}
private bool MeetsNumSlotsRequirement(Request r)
{
return r.Ask.Slots >= minNumberOfHosts;
}
private bool MeetsSizeRequirement(Request r)
{
var slotSize = r.Ask.SlotSize.ToDecimal();
decimal min = minSlotSize.SizeInBytes;
return slotSize >= min;
}
private bool MeetsDurationRequirement(Request r)
{
var duration = TimeSpan.FromSeconds((double)r.Ask.Duration);
return duration >= minDuration;
}
}
public class StartedContractCheck : ICheck
{
private readonly ulong minNumberOfHosts;
private readonly ByteSize minSlotSize;
private readonly TimeSpan minDuration;
public StartedContractCheck(ulong minNumberOfHosts, ByteSize minSlotSize, TimeSpan minDuration)
{
this.minNumberOfHosts = minNumberOfHosts;
this.minSlotSize = minSlotSize;
this.minDuration = minDuration;
}
public EthAddress[] Check(ChainState state)
{
return state.StartedRequests
.Where(r =>
MeetsNumSlotsRequirement(r) &&
MeetsSizeRequirement(r) &&
MeetsDurationRequirement(r))
.Select(r => r.Request.ClientAddress)
.ToArray();
}
private bool MeetsNumSlotsRequirement(StorageRequest r)
{
return r.Request.Ask.Slots >= minNumberOfHosts;
}
private bool MeetsSizeRequirement(StorageRequest r)
{
var slotSize = r.Request.Ask.SlotSize.ToDecimal();
decimal min = minSlotSize.SizeInBytes;
return slotSize >= min;
}
private bool MeetsDurationRequirement(StorageRequest r)
{
var duration = TimeSpan.FromSeconds((double)r.Request.Ask.Duration);
return duration >= minDuration;
}
}
}

View File

@ -40,5 +40,14 @@ namespace TestNetRewarder
return TimeSpan.FromMinutes(IntervalMinutes);
}
}
public DateTime HistoryStartUtc
{
get
{
if (CheckHistoryTimestamp == 0) throw new Exception("'check-history' unix timestamp is required. Set it to the start/launch moment of the testnet.");
return DateTimeOffset.FromUnixTimeSeconds(CheckHistoryTimestamp).UtcDateTime;
}
}
}
}

View File

@ -29,6 +29,20 @@ namespace TestNetRewarder
r.State == RequestState.Failed
);
}
public string EntireString()
{
return JsonConvert.SerializeObject(StorageRequests);
}
public HistoricState()
{
}
public HistoricState(StorageRequest[] requests)
{
storageRequests.AddRange(requests);
}
}
public class StorageRequest

View File

@ -0,0 +1,74 @@
using CodexContractsPlugin.ChainMonitor;
using CodexContractsPlugin.Marketplace;
using DiscordRewards;
using System.Numerics;
namespace TestNetRewarder
{
public class MarketBuffer
{
private readonly List<IChainStateRequest> requests = new List<IChainStateRequest>();
private readonly TimeSpan bufferSpan;
public MarketBuffer(TimeSpan bufferSpan)
{
this.bufferSpan = bufferSpan;
}
public void Add(IChainStateRequest request)
{
requests.Add(request);
}
public void Update()
{
var now = DateTime.UtcNow;
requests.RemoveAll(r => (now - r.FinishedUtc) > bufferSpan);
}
public MarketAverage? GetAverage()
{
if (requests.Count == 0) return null;
return new MarketAverage
{
NumberOfFinished = requests.Count,
TimeRangeSeconds = (int)bufferSpan.TotalSeconds,
Price = Average(s => s.Request.Ask.Reward),
Duration = Average(s => s.Request.Ask.Duration),
Size = Average(s => GetTotalSize(s.Request.Ask)),
Collateral = Average(s => s.Request.Ask.Collateral),
ProofProbability = Average(s => s.Request.Ask.ProofProbability)
};
}
private float Average(Func<IChainStateRequest, BigInteger> getValue)
{
return Average(s =>
{
var value = getValue(s);
return (int)value;
});
}
private float Average(Func<IChainStateRequest, int> getValue)
{
var sum = 0.0f;
float count = requests.Count;
foreach (var r in requests)
{
sum += getValue(r);
}
if (count < 1.0f) return 0.0f;
return sum / count;
}
private int GetTotalSize(Ask ask)
{
var nSlots = Convert.ToInt32(ask.Slots);
var slotSize = (int)ask.SlotSize;
return nSlots * slotSize;
}
}
}

View File

@ -1,124 +1,73 @@
using CodexContractsPlugin.Marketplace;
using CodexContractsPlugin.ChainMonitor;
using DiscordRewards;
using Logging;
using System.Numerics;
namespace TestNetRewarder
{
public class MarketTracker
public class MarketTracker : IChainStateChangeHandler
{
private readonly List<ChainState> buffer = new List<ChainState>();
private readonly List<MarketBuffer> buffers = new List<MarketBuffer>();
private readonly ILog log;
public MarketAverage[] ProcessChainState(ChainState chainState)
public MarketTracker(Configuration config, ILog log)
{
var intervalCounts = GetInsightCounts();
if (!intervalCounts.Any()) return Array.Empty<MarketAverage>();
var intervals = GetInsightCounts(config);
UpdateBuffer(chainState, intervalCounts.Max());
var result = intervalCounts
.Select(GenerateMarketAverage)
.Where(a => a != null)
.Cast<MarketAverage>()
.ToArray();
if (!result.Any()) result = Array.Empty<MarketAverage>();
return result;
}
private void UpdateBuffer(ChainState chainState, int maxNumberOfIntervals)
{
buffer.Add(chainState);
while (buffer.Count > maxNumberOfIntervals)
foreach (var i in intervals)
{
buffer.RemoveAt(0);
buffers.Add(new MarketBuffer(
config.Interval * i
));
}
this.log = log;
}
private MarketAverage? GenerateMarketAverage(int numberOfIntervals)
public MarketAverage[] GetAverages()
{
var states = SelectStates(numberOfIntervals);
return CreateAverage(states);
foreach (var b in buffers) b.Update();
return buffers.Select(b => b.GetAverage()).Where(a => a != null).Cast<MarketAverage>().ToArray();
}
private ChainState[] SelectStates(int numberOfIntervals)
public void OnNewRequest(IChainStateRequest request)
{
if (numberOfIntervals < 1) return Array.Empty<ChainState>();
if (numberOfIntervals > buffer.Count) return Array.Empty<ChainState>();
return buffer.TakeLast(numberOfIntervals).ToArray();
}
private MarketAverage? CreateAverage(ChainState[] states)
public void OnRequestFinished(IChainStateRequest request)
{
foreach (var b in buffers) b.Add(request);
}
public void OnRequestFulfilled(IChainStateRequest request)
{
}
public void OnRequestCancelled(IChainStateRequest request)
{
}
public void OnSlotFilled(IChainStateRequest request, BigInteger slotIndex)
{
}
public void OnSlotFreed(IChainStateRequest request, BigInteger slotIndex)
{
}
private int[] GetInsightCounts(Configuration config)
{
try
{
return new MarketAverage
{
NumberOfFinished = CountNumberOfFinishedRequests(states),
TimeRangeSeconds = GetTotalTimeRange(states),
Price = Average(states, s => s.Request.Ask.Reward),
Duration = Average(states, s => s.Request.Ask.Duration),
Size = Average(states, s => GetTotalSize(s.Request.Ask)),
Collateral = Average(states, s => s.Request.Ask.Collateral),
ProofProbability = Average(states, s => s.Request.Ask.ProofProbability)
};
}
catch (Exception ex)
{
Program.Log.Error($"Exception in CreateAverage: {ex}");
return null;
}
}
private int GetTotalSize(Ask ask)
{
var nSlots = Convert.ToInt32(ask.Slots);
var slotSize = Convert.ToInt32(ask.SlotSize);
return nSlots * slotSize;
}
private float Average(ChainState[] states, Func<StorageRequest, BigInteger> getValue)
{
return Average(states, s => Convert.ToInt32(getValue(s)));
}
private float Average(ChainState[] states, Func<StorageRequest, int> getValue)
{
var sum = 0.0f;
var count = 0.0f;
foreach (var state in states)
{
foreach (var finishedRequest in state.FinishedRequests)
{
sum += getValue(finishedRequest);
count++;
}
}
if (count < 1.0f) return 0.0f;
return sum / count;
}
private int GetTotalTimeRange(ChainState[] states)
{
return Convert.ToInt32((Program.Config.Interval * states.Length).TotalSeconds);
}
private int CountNumberOfFinishedRequests(ChainState[] states)
{
return states.Sum(s => s.FinishedRequests.Length);
}
private int[] GetInsightCounts()
{
try
{
var tokens = Program.Config.MarketInsights.Split(';').ToArray();
var tokens = config.MarketInsights.Split(';').ToArray();
return tokens.Select(t => Convert.ToInt32(t)).ToArray();
}
catch (Exception ex)
{
Program.Log.Error($"Exception when parsing MarketInsights config parameters: {ex}");
log.Error($"Exception when parsing MarketInsights config parameters: {ex}");
}
return Array.Empty<int>();
return Array.Empty<int>();
}
}
}

View File

@ -1,146 +1,67 @@
using DiscordRewards;
using GethPlugin;
using CodexContractsPlugin;
using CodexContractsPlugin.ChainMonitor;
using Logging;
using Newtonsoft.Json;
using Utils;
namespace TestNetRewarder
{
public class Processor
public class Processor : ITimeSegmentHandler
{
private static readonly HistoricState historicState = new HistoricState();
private static readonly RewardRepo rewardRepo = new RewardRepo();
private static readonly MarketTracker marketTracker = new MarketTracker();
private readonly RequestBuilder builder;
private readonly RewardChecker rewardChecker;
private readonly MarketTracker marketTracker;
private readonly BufferLogger bufferLogger;
private readonly ChainState chainState;
private readonly BotClient client;
private readonly ILog log;
private BlockInterval? lastBlockRange;
public Processor(ILog log)
public Processor(Configuration config, BotClient client, ICodexContracts contracts, ILog log)
{
this.client = client;
this.log = log;
builder = new RequestBuilder();
rewardChecker = new RewardChecker(builder);
marketTracker = new MarketTracker(config, log);
bufferLogger = new BufferLogger();
var handler = new ChainChangeMux(
rewardChecker.Handler,
marketTracker
);
chainState = new ChainState(new LogSplitter(log, bufferLogger), contracts, handler, config.HistoryStartUtc);
}
public async Task ProcessTimeSegment(TimeRange timeRange)
public async Task OnNewSegment(TimeRange timeRange)
{
var connector = GethConnector.GethConnector.Initialize(log);
if (connector == null) throw new Exception("Invalid Geth information");
try
{
var blockRange = connector.GethNode.ConvertTimeRangeToBlockRange(timeRange);
if (!IsNewBlockRange(blockRange))
{
log.Log($"Block range {blockRange} was previously processed. Skipping...");
return;
}
chainState.Update(timeRange.To);
var chainState = new ChainState(historicState, connector.CodexContracts, blockRange);
await ProcessChainState(chainState);
var averages = marketTracker.GetAverages();
var lines = RemoveFirstLine(bufferLogger.Get());
var request = builder.Build(averages, lines);
if (request.HasAny())
{
await client.SendRewards(request);
}
}
catch (Exception ex)
{
log.Error("Exception processing time segment: " + ex);
var msg = "Exception processing time segment: " + ex;
log.Error(msg);
bufferLogger.Error(msg);
throw;
}
}
private bool IsNewBlockRange(BlockInterval blockRange)
private string[] RemoveFirstLine(string[] lines)
{
if (lastBlockRange == null ||
lastBlockRange.From != blockRange.From ||
lastBlockRange.To != blockRange.To)
{
lastBlockRange = blockRange;
return true;
}
return false;
}
private async Task ProcessChainState(ChainState chainState)
{
var outgoingRewards = new List<RewardUsersCommand>();
foreach (var reward in rewardRepo.Rewards)
{
ProcessReward(outgoingRewards, reward, chainState);
}
var marketAverages = GetMarketAverages(chainState);
var eventsOverview = GenerateEventsOverview(chainState);
log.Log($"Found {outgoingRewards.Count} rewards. " +
$"Found {marketAverages.Length} market averages. " +
$"Found {eventsOverview.Length} events.");
if (outgoingRewards.Any() || marketAverages.Any() || eventsOverview.Any())
{
if (!await SendRewardsCommand(outgoingRewards, marketAverages, eventsOverview))
{
log.Error("Failed to send reward command.");
}
}
}
private string[] GenerateEventsOverview(ChainState chainState)
{
return chainState.GenerateOverview();
}
private MarketAverage[] GetMarketAverages(ChainState chainState)
{
return marketTracker.ProcessChainState(chainState);
}
private async Task<bool> SendRewardsCommand(List<RewardUsersCommand> outgoingRewards, MarketAverage[] marketAverages, string[] eventsOverview)
{
var cmd = new GiveRewardsCommand
{
Rewards = outgoingRewards.ToArray(),
Averages = marketAverages.ToArray(),
EventsOverview = eventsOverview
};
log.Debug("Sending rewards: " + JsonConvert.SerializeObject(cmd));
return await Program.BotClient.SendRewards(cmd);
}
private void ProcessReward(List<RewardUsersCommand> outgoingRewards, RewardConfig reward, ChainState chainState)
{
var winningAddresses = PerformCheck(reward, chainState);
foreach (var win in winningAddresses)
{
log.Log($"Address '{win.Address}' wins '{reward.Message}'");
}
if (winningAddresses.Any())
{
outgoingRewards.Add(new RewardUsersCommand
{
RewardId = reward.RoleId,
UserAddresses = winningAddresses.Select(a => a.Address).ToArray()
});
}
}
private EthAddress[] PerformCheck(RewardConfig reward, ChainState chainState)
{
var check = GetCheck(reward.CheckConfig);
return check.Check(chainState).Distinct().ToArray();
}
private ICheck GetCheck(CheckConfig config)
{
switch (config.Type)
{
case CheckType.FilledSlot:
return new FilledAnySlotCheck();
case CheckType.FinishedSlot:
return new FinishedSlotCheck(config.MinSlotSize, config.MinDuration);
case CheckType.PostedContract:
return new PostedContractCheck(config.MinNumberOfHosts, config.MinSlotSize, config.MinDuration);
case CheckType.StartedContract:
return new StartedContractCheck(config.MinNumberOfHosts, config.MinSlotSize, config.MinDuration);
}
throw new Exception("Unknown check type: " + config.Type);
//if (!lines.Any()) return Array.Empty<string>();
//return lines.Skip(1).ToArray();
return lines;
}
}
}

View File

@ -6,10 +6,10 @@ namespace TestNetRewarder
{
public class Program
{
public static Configuration Config { get; private set; } = null!;
public static ILog Log { get; private set; } = null!;
public static CancellationToken CancellationToken { get; private set; }
public static BotClient BotClient { get; private set; } = null!;
public static CancellationToken CancellationToken;
private static Configuration Config = null!;
private static ILog Log = null!;
private static BotClient BotClient = null!;
private static Processor processor = null!;
private static DateTime lastCheck = DateTime.MinValue;
@ -27,8 +27,11 @@ namespace TestNetRewarder
new ConsoleLog()
);
var connector = GethConnector.GethConnector.Initialize(Log);
if (connector == null) throw new Exception("Invalid Geth information");
BotClient = new BotClient(Config, Log);
processor = new Processor(Log);
processor = new Processor(Config, BotClient, connector.CodexContracts, Log);
EnsurePath(Config.DataPath);
EnsurePath(Config.LogPath);
@ -41,12 +44,12 @@ namespace TestNetRewarder
EnsureGethOnline();
Log.Log("Starting TestNet Rewarder...");
var segmenter = new TimeSegmenter(Log, Config);
var segmenter = new TimeSegmenter(Log, Config, processor);
while (!CancellationToken.IsCancellationRequested)
{
await EnsureBotOnline();
await segmenter.WaitForNextSegment(processor.ProcessTimeSegment);
await segmenter.ProcessNextSegment();
await Task.Delay(100, CancellationToken);
}
}

View File

@ -0,0 +1,40 @@
using DiscordRewards;
using GethPlugin;
namespace TestNetRewarder
{
public class RequestBuilder : IRewardGiver
{
private readonly Dictionary<ulong, List<EthAddress>> rewards = new Dictionary<ulong, List<EthAddress>>();
public void Give(RewardConfig reward, EthAddress receiver)
{
if (rewards.ContainsKey(reward.RoleId))
{
rewards[reward.RoleId].Add(receiver);
}
else
{
rewards.Add(reward.RoleId, new List<EthAddress> { receiver });
}
}
public GiveRewardsCommand Build(MarketAverage[] marketAverages, string[] lines)
{
var result = new GiveRewardsCommand
{
Rewards = rewards.Select(p => new RewardUsersCommand
{
RewardId = p.Key,
UserAddresses = p.Value.Select(v => v.Address).ToArray()
}).ToArray(),
Averages = marketAverages,
EventsOverview = lines
};
rewards.Clear();
return result;
}
}
}

View File

@ -0,0 +1,98 @@
using CodexContractsPlugin.ChainMonitor;
using DiscordRewards;
using GethPlugin;
using NethereumWorkflow;
using System.Numerics;
namespace TestNetRewarder
{
public interface IRewardGiver
{
void Give(RewardConfig reward, EthAddress receiver);
}
public class RewardCheck : IChainStateChangeHandler
{
private readonly RewardConfig reward;
private readonly IRewardGiver giver;
public RewardCheck(RewardConfig reward, IRewardGiver giver)
{
this.reward = reward;
this.giver = giver;
}
public void OnNewRequest(IChainStateRequest request)
{
if (MeetsRequirements(CheckType.ClientPostedContract, request))
{
GiveReward(reward, request.Client);
}
}
public void OnRequestCancelled(IChainStateRequest request)
{
}
public void OnRequestFinished(IChainStateRequest request)
{
if (MeetsRequirements(CheckType.HostFinishedSlot, request))
{
foreach (var host in request.Hosts.GetHosts())
{
GiveReward(reward, host);
}
}
}
public void OnRequestFulfilled(IChainStateRequest request)
{
if (MeetsRequirements(CheckType.ClientStartedContract, request))
{
GiveReward(reward, request.Client);
}
}
public void OnSlotFilled(IChainStateRequest request, BigInteger slotIndex)
{
if (MeetsRequirements(CheckType.HostFilledSlot, request))
{
var host = request.Hosts.GetHost((int)slotIndex);
if (host != null)
{
GiveReward(reward, host);
}
}
}
public void OnSlotFreed(IChainStateRequest request, BigInteger slotIndex)
{
}
private void GiveReward(RewardConfig reward, EthAddress receiver)
{
giver.Give(reward, receiver);
}
private bool MeetsRequirements(CheckType type, IChainStateRequest request)
{
return
reward.CheckConfig.Type == type &&
MeetsDurationRequirement(request) &&
MeetsSizeRequirement(request);
}
private bool MeetsSizeRequirement(IChainStateRequest r)
{
var slotSize = r.Request.Ask.SlotSize.ToDecimal();
decimal min = reward.CheckConfig.MinSlotSize.SizeInBytes;
return slotSize >= min;
}
private bool MeetsDurationRequirement(IChainStateRequest r)
{
var duration = TimeSpan.FromSeconds((double)r.Request.Ask.Duration);
return duration >= reward.CheckConfig.MinDuration;
}
}
}

View File

@ -0,0 +1,17 @@
using CodexContractsPlugin.ChainMonitor;
using DiscordRewards;
namespace TestNetRewarder
{
public class RewardChecker
{
public RewardChecker(IRewardGiver giver)
{
var repo = new RewardRepo();
var checks = repo.Rewards.Select(r => new RewardCheck(r, giver)).ToArray();
Handler = new ChainChangeMux(checks);
}
public IChainStateChangeHandler Handler { get; }
}
}

View File

@ -3,51 +3,60 @@ using Utils;
namespace TestNetRewarder
{
public interface ITimeSegmentHandler
{
Task OnNewSegment(TimeRange timeRange);
}
public class TimeSegmenter
{
private readonly ILog log;
private readonly ITimeSegmentHandler handler;
private readonly TimeSpan segmentSize;
private DateTime start;
private DateTime latest;
public TimeSegmenter(ILog log, Configuration configuration)
public TimeSegmenter(ILog log, Configuration configuration, ITimeSegmentHandler handler)
{
this.log = log;
this.handler = handler;
if (configuration.IntervalMinutes < 0) configuration.IntervalMinutes = 1;
if (configuration.CheckHistoryTimestamp == 0) throw new Exception("'check-history' unix timestamp is required. Set it to the start/launch moment of the testnet.");
segmentSize = configuration.Interval;
start = DateTimeOffset.FromUnixTimeSeconds(configuration.CheckHistoryTimestamp).UtcDateTime;
latest = configuration.HistoryStartUtc;
log.Log("Starting time segments at " + start);
log.Log("Starting time segments at " + latest);
log.Log("Segment size: " + Time.FormatDuration(segmentSize));
}
public async Task WaitForNextSegment(Func<TimeRange, Task> onSegment)
public async Task ProcessNextSegment()
{
var now = DateTime.UtcNow;
var end = start + segmentSize;
var waited = false;
if (end > now)
{
// Wait for the entire time segment to be in the past.
var delay = end - now;
waited = true;
log.Log($"Waiting till time segment is in the past... {Time.FormatDuration(delay)}");
await Task.Delay(delay, Program.CancellationToken);
}
await Task.Delay(TimeSpan.FromSeconds(3), Program.CancellationToken);
var end = latest + segmentSize;
var waited = await WaitUntilTimeSegmentInPast(end);
if (Program.CancellationToken.IsCancellationRequested) return;
var postfix = "(Catching up...)";
if (waited) postfix = "(Real-time)";
log.Log($"Time segment [{latest} to {end}] {postfix}");
var range = new TimeRange(latest, end);
latest = end;
log.Log($"Time segment [{start} to {end}] {postfix}");
var range = new TimeRange(start, end);
start = end;
await handler.OnNewSegment(range);
}
await onSegment(range);
private async Task<bool> WaitUntilTimeSegmentInPast(DateTime end)
{
await Task.Delay(TimeSpan.FromSeconds(3), Program.CancellationToken);
var now = DateTime.UtcNow;
while (end > now)
{
var delay = (end - now) + TimeSpan.FromSeconds(3);
await Task.Delay(delay, Program.CancellationToken);
return true;
}
return false;
}
}
}