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 public enum CheckType
{ {
Uninitialized, Uninitialized,
FilledSlot, HostFilledSlot,
FinishedSlot, HostFinishedSlot,
PostedContract, ClientPostedContract,
StartedContract, ClientStartedContract,
} }
} }

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
{ {
public class BlockInterval public class BlockInterval
{ {
public BlockInterval(ulong from, ulong to) public BlockInterval(TimeRange timeRange, ulong from, ulong to)
{ {
if (from < to) if (from < to)
{ {
@ -14,10 +14,13 @@
From = to; From = to;
To = from; To = from;
} }
TimeRange = timeRange;
} }
public ulong From { get; } public ulong From { get; }
public ulong To { get; } public ulong To { get; }
public TimeRange TimeRange { get; }
public ulong NumberOfBlocks => To - From;
public override string ToString() 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 GethPlugin;
using Logging; using Logging;
using Nethereum.ABI; using Nethereum.ABI;
using Nethereum.Hex.HexTypes;
using Nethereum.Util; using Nethereum.Util;
using NethereumWorkflow; using NethereumWorkflow;
using NethereumWorkflow.BlockUtils;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using Utils; using Utils;
@ -22,13 +20,10 @@ namespace CodexContractsPlugin
TestToken GetTestTokenBalance(IHasEthAddress owner); TestToken GetTestTokenBalance(IHasEthAddress owner);
TestToken GetTestTokenBalance(EthAddress ethAddress); TestToken GetTestTokenBalance(EthAddress ethAddress);
Request[] GetStorageRequests(BlockInterval blockRange); ICodexContractsEvents GetEvents(TimeRange timeRange);
ICodexContractsEvents GetEvents(BlockInterval blockInterval);
EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex); EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex);
RequestState GetRequestState(Request request); RequestState GetRequestState(Request request);
RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange);
RequestCancelledEventDTO[] GetRequestCancelledEvents(BlockInterval blockRange);
SlotFilledEventDTO[] GetSlotFilledEvents(BlockInterval blockRange);
SlotFreedEventDTO[] GetSlotFreedEvents(BlockInterval blockRange);
} }
[JsonConverter(typeof(StringEnumConverter))] [JsonConverter(typeof(StringEnumConverter))]
@ -81,65 +76,14 @@ namespace CodexContractsPlugin
return balance.TstWei(); return balance.TstWei();
} }
public Request[] GetStorageRequests(BlockInterval blockRange) public ICodexContractsEvents GetEvents(TimeRange timeRange)
{ {
var events = gethNode.GetEvents<StorageRequestedEventDTO>(Deployment.MarketplaceAddress, blockRange); return GetEvents(gethNode.ConvertTimeRangeToBlockRange(timeRange));
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();
} }
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange) public ICodexContractsEvents GetEvents(BlockInterval blockInterval)
{ {
var events = gethNode.GetEvents<RequestFulfilledEventDTO>(Deployment.MarketplaceAddress, blockRange); return new CodexContractsEvents(log, gethNode, Deployment, blockInterval);
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();
} }
public EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex) public EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex)
@ -170,17 +114,6 @@ namespace CodexContractsPlugin
return gethNode.Call<RequestStateFunction, RequestState>(Deployment.MarketplaceAddress, func); 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() private ContractInteractions StartInteraction()
{ {
return new ContractInteractions(log, gethNode); 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 namespace CodexContractsPlugin.Marketplace
{ {
public partial class Request : RequestBase public interface IHasBlock
{
BlockTimeEntry Block { get; set; }
}
public partial class Request : RequestBase, IHasBlock
{ {
[JsonIgnore] [JsonIgnore]
public BlockTimeEntry Block { get; set; } public BlockTimeEntry Block { get; set; }
public byte[] RequestId { get; set; } public byte[] RequestId { get; set; }
public EthAddress ClientAddress { get { return new EthAddress(Client); } } 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] [JsonIgnore]
public BlockTimeEntry Block { get; set; } public BlockTimeEntry Block { get; set; }
} }
public partial class RequestCancelledEventDTO public partial class RequestCancelledEventDTO : IHasBlock
{ {
[JsonIgnore] [JsonIgnore]
public BlockTimeEntry Block { get; set; } public BlockTimeEntry Block { get; set; }
} }
public partial class SlotFilledEventDTO public partial class SlotFilledEventDTO : IHasBlock
{ {
[JsonIgnore] [JsonIgnore]
public BlockTimeEntry Block { get; set; } public BlockTimeEntry Block { get; set; }
public EthAddress Host { get; set; } public EthAddress Host { get; set; }
} }
public partial class SlotFreedEventDTO public partial class SlotFreedEventDTO : IHasBlock
{ {
[JsonIgnore] [JsonIgnore]
public BlockTimeEntry Block { get; set; } public BlockTimeEntry Block { get; set; }

View File

@ -1,11 +1,13 @@
using Core; using Core;
using KubernetesWorkflow; using KubernetesWorkflow;
using KubernetesWorkflow.Types; using KubernetesWorkflow.Types;
using Utils;
namespace CodexDiscordBotPlugin namespace CodexDiscordBotPlugin
{ {
public class CodexDiscordBotPlugin : IProjectPlugin, IHasLogPrefix, IHasMetadata public class CodexDiscordBotPlugin : IProjectPlugin, IHasLogPrefix, IHasMetadata
{ {
private const string ExpectedStartupMessage = "Debug option is set. Discord connection disabled!";
private readonly IPluginTools tools; private readonly IPluginTools tools;
public CodexDiscordBotPlugin(IPluginTools tools) public CodexDiscordBotPlugin(IPluginTools tools)
@ -46,14 +48,59 @@ namespace CodexDiscordBotPlugin
var startupConfig = new StartupConfig(); var startupConfig = new StartupConfig();
startupConfig.NameOverride = config.Name; startupConfig.NameOverride = config.Name;
startupConfig.Add(config); 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) private RunningPod StartRewarderContainer(IStartupWorkflow workflow, RewarderBotStartupConfig config)
{ {
var startupConfig = new StartupConfig(); var startupConfig = new StartupConfig();
startupConfig.NameOverride = config.Name;
startupConfig.Add(config); 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 class DiscordBotContainerRecipe : ContainerRecipeFactory
{ {
public override string AppName => "discordbot-bibliotech"; 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"; public static string RewardsPort = "bot_rewards_port";
@ -33,6 +33,8 @@ namespace CodexDiscordBotPlugin
AddEnvVar("CODEXCONTRACTS_TOKENADDRESS", gethInfo.TokenAddress); AddEnvVar("CODEXCONTRACTS_TOKENADDRESS", gethInfo.TokenAddress);
AddEnvVar("CODEXCONTRACTS_ABI", gethInfo.Abi); AddEnvVar("CODEXCONTRACTS_ABI", gethInfo.Abi);
AddEnvVar("NODISCORD", "1");
AddInternalPortAndVar("REWARDAPIPORT", RewardsPort); AddInternalPortAndVar("REWARDAPIPORT", RewardsPort);
if (!string.IsNullOrEmpty(config.DataPath)) if (!string.IsNullOrEmpty(config.DataPath))

View File

@ -27,8 +27,9 @@
public class RewarderBotStartupConfig 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; DiscordBotHost = discordBotHost;
DiscordBotPort = discordBotPort; DiscordBotPort = discordBotPort;
IntervalMinutes = intervalMinutes; IntervalMinutes = intervalMinutes;
@ -37,9 +38,10 @@
DataPath = dataPath; DataPath = dataPath;
} }
public string Name { get; }
public string DiscordBotHost { get; } public string DiscordBotHost { get; }
public int DiscordBotPort { get; } public int DiscordBotPort { get; }
public string IntervalMinutes { get; } public int IntervalMinutes { get; }
public DateTime HistoryStartUtc { get; } public DateTime HistoryStartUtc { get; }
public DiscordBotGethInfo GethInfo { get; } public DiscordBotGethInfo GethInfo { get; }
public string? DataPath { get; set; } public string? DataPath { get; set; }

View File

@ -7,7 +7,8 @@ namespace CodexDiscordBotPlugin
public class RewarderBotContainerRecipe : ContainerRecipeFactory public class RewarderBotContainerRecipe : ContainerRecipeFactory
{ {
public override string AppName => "discordbot-rewarder"; 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) protected override void Initialize(StartupConfig startupConfig)
{ {
@ -17,7 +18,7 @@ namespace CodexDiscordBotPlugin
AddEnvVar("DISCORDBOTHOST", config.DiscordBotHost); AddEnvVar("DISCORDBOTHOST", config.DiscordBotHost);
AddEnvVar("DISCORDBOTPORT", config.DiscordBotPort.ToString()); AddEnvVar("DISCORDBOTPORT", config.DiscordBotPort.ToString());
AddEnvVar("INTERVALMINUTES", config.IntervalMinutes); AddEnvVar("INTERVALMINUTES", config.IntervalMinutes.ToString());
var offset = new DateTimeOffset(config.HistoryStartUtc); var offset = new DateTimeOffset(config.HistoryStartUtc);
AddEnvVar("CHECKHISTORY", offset.ToUnixTimeSeconds().ToString()); 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, // Custom scripting in the Codex test image will write this variable to a private-key file,
// and pass the correct filename to Codex. // and pass the correct filename to Codex.
AddEnvVar("PRIV_KEY", marketplaceSetup.EthAccount.PrivateKey); var account = marketplaceSetup.EthAccountSetup.GetNew();
Additional(marketplaceSetup.EthAccount); AddEnvVar("PRIV_KEY", account.PrivateKey);
Additional(account);
SetCommandOverride(marketplaceSetup); SetCommandOverride(marketplaceSetup);
if (marketplaceSetup.IsValidator) if (marketplaceSetup.IsValidator)

View File

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

View File

@ -22,22 +22,22 @@ namespace CodexPlugin
public CodexNode CreateOnlineCodexNode(CodexAccess access, CodexNodeGroup group) public CodexNode CreateOnlineCodexNode(CodexAccess access, CodexNodeGroup group)
{ {
var ethAddress = GetEthAddress(access); var ethAccount = GetEthAccount(access);
var marketplaceAccess = GetMarketplaceAccess(access, ethAddress); var marketplaceAccess = GetMarketplaceAccess(access, ethAccount);
return new CodexNode(tools, access, group, marketplaceAccess, ethAddress); 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); 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>(); var ethAccount = access.Container.Containers.Single().Recipe.Additionals.Get<EthAccount>();
if (ethAccount == null) return null; if (ethAccount == null) return null;
return ethAccount.EthAddress; return ethAccount;
} }
public CrashWatcher CreateCrashWatcher(RunningContainer c) public CrashWatcher CreateCrashWatcher(RunningContainer c)

View File

@ -169,7 +169,7 @@ namespace CodexPlugin
public bool IsValidator { get; private set; } public bool IsValidator { get; private set; }
public Ether InitialEth { get; private set; } = 0.Eth(); public Ether InitialEth { get; private set; } = 0.Eth();
public TestToken InitialTestTokens { get; private set; } = 0.Tst(); 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() public IMarketplaceSetup AsStorageNode()
{ {
@ -185,7 +185,7 @@ namespace CodexPlugin
public IMarketplaceSetup WithAccount(EthAccount account) public IMarketplaceSetup WithAccount(EthAccount account)
{ {
EthAccount = account; EthAccountSetup.Pin(account);
return this; return this;
} }
@ -201,10 +201,41 @@ namespace CodexPlugin
var result = "[(clientNode)"; // When marketplace is enabled, being a clientNode is implicit. var result = "[(clientNode)"; // When marketplace is enabled, being a clientNode is implicit.
result += IsStorageNode ? "(storageNode)" : "()"; result += IsStorageNode ? "(storageNode)" : "()";
result += IsValidator ? "(validator)" : "() "; result += IsValidator ? "(validator)" : "() ";
result += $"Address: '{EthAccount.EthAddress}' "; result += $"Address: '{EthAccountSetup}' ";
result += $"{InitialEth.Eth} / {InitialTestTokens}"; result += $"{InitialEth.Eth} / {InitialTestTokens}";
result += "] "; result += "] ";
return 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 Logging;
using Newtonsoft.Json;
using Utils; using Utils;
namespace CodexPlugin namespace CodexPlugin
@ -7,7 +6,7 @@ namespace CodexPlugin
public interface IMarketplaceAccess public interface IMarketplaceAccess
{ {
string MakeStorageAvailable(StorageAvailability availability); string MakeStorageAvailable(StorageAvailability availability);
StoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase); IStoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase);
} }
public class MarketplaceAccess : IMarketplaceAccess public class MarketplaceAccess : IMarketplaceAccess
@ -21,7 +20,7 @@ namespace CodexPlugin
this.codexAccess = codexAccess; this.codexAccess = codexAccess;
} }
public StoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase) public IStoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase)
{ {
purchase.Log(log); purchase.Log(log);
@ -68,7 +67,7 @@ namespace CodexPlugin
throw new NotImplementedException(); throw new NotImplementedException();
} }
public StoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase) public IStoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase)
{ {
Unavailable(); Unavailable();
throw new NotImplementedException(); throw new NotImplementedException();
@ -80,134 +79,4 @@ namespace CodexPlugin
throw new InvalidOperationException(); 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); return new EthAccount(ethAddress, account.PrivateKey);
} }
public override string ToString()
{
return EthAddress.ToString();
}
} }
} }

View File

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

View File

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

View File

@ -1,7 +1,13 @@
using CodexContractsPlugin; using CodexContractsPlugin;
using CodexDiscordBotPlugin; using CodexDiscordBotPlugin;
using CodexPlugin; using CodexPlugin;
using Core;
using DiscordRewards;
using DistTestCore;
using GethPlugin; using GethPlugin;
using KubernetesWorkflow.Types;
using Logging;
using Newtonsoft.Json;
using NUnit.Framework; using NUnit.Framework;
using Utils; using Utils;
@ -10,21 +16,155 @@ namespace CodexTests.UtilityTests
[TestFixture] [TestFixture]
public class DiscordBotTests : AutoBootstrapDistTest 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] [Test]
[Ignore("Used for debugging bots")] [DontDownloadLogs]
public void BotRewardTest() 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 geth = Ci.StartGethNode(s => s.IsMiner().WithName("disttest-geth"));
var contracts = Ci.StartCodexContracts(geth); var contracts = Ci.StartCodexContracts(geth);
var gethInfo = CreateGethInfo(geth, contracts);
// start bot and rewarder var botContainer = StartDiscordBot(gethInfo);
var gethInfo = new DiscordBotGethInfo( 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, host: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Host,
port: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Port, port: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Port,
privKey: geth.StartResult.Account.PrivateKey, privKey: geth.StartResult.Account.PrivateKey,
@ -32,8 +172,12 @@ namespace CodexTests.UtilityTests
tokenAddress: contracts.Deployment.TokenAddress, tokenAddress: contracts.Deployment.TokenAddress,
abi: contracts.Deployment.Abi abi: contracts.Deployment.Abi
); );
}
private RunningContainer StartDiscordBot(DiscordBotGethInfo gethInfo)
{
var bot = Ci.DeployCodexDiscordBot(new DiscordBotStartupConfig( var bot = Ci.DeployCodexDiscordBot(new DiscordBotStartupConfig(
name: "bot", name: "discord-bot",
token: "aaa", token: "aaa",
serverName: "ThatBen's server", serverName: "ThatBen's server",
adminRoleName: "bottest-admins", adminRoleName: "bottest-admins",
@ -42,70 +186,190 @@ namespace CodexTests.UtilityTests
kubeNamespace: "notneeded", kubeNamespace: "notneeded",
gethInfo: gethInfo gethInfo: gethInfo
)); ));
var botContainer = bot.Containers.Single(); return bot.Containers.Single();
Ci.DeployRewarderBot(new RewarderBotStartupConfig( }
//discordBotHost: "http://" + botContainer.GetAddress(GetTestLog(), DiscordBotContainerRecipe.RewardsPort).Host,
//discordBotPort: botContainer.GetAddress(GetTestLog(), DiscordBotContainerRecipe.RewardsPort).Port,
discordBotHost: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Host,
discordBotPort: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Port,
intervalMinutes: "1",
historyStartUtc: GetTestRunTimeRange().From - TimeSpan.FromMinutes(3),
gethInfo: gethInfo,
dataPath: null
));
var numberOfHosts = 3; private void StartHosts(IGethNode geth, ICodexContracts contracts)
for (var i = 0; i < numberOfHosts; i++)
{ {
var seller = StartCodex(s => s var hosts = StartCodex(GetNumberOfLiveHosts(), s => s
.WithName("Seller") .WithName("Host")
.WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn) .WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn)
{ {
ContractClock = CodexLogLevel.Trace, ContractClock = CodexLogLevel.Trace,
}) })
.WithStorageQuota(11.GB()) .WithStorageQuota(Mult(GetMinFileSizePlus(50), GetNumberOfLiveHosts()))
.EnableMarketplace(geth, contracts, m => m .EnableMarketplace(geth, contracts, m => m
.WithAccount(myAccount) .WithInitial(10.Eth(), hostInitialBalance)
.WithInitial(10.Eth(), sellerInitialBalance)
.AsStorageNode() .AsStorageNode()
.AsValidator())); .AsValidator()));
var availability = new StorageAvailability( var availability = new StorageAvailability(
totalSpace: 10.GB(), totalSpace: Mult(GetMinFileSize(), GetNumberOfLiveHosts()),
maxDuration: TimeSpan.FromMinutes(30), maxDuration: TimeSpan.FromMinutes(30),
minPriceForTotalSpace: 1.TstWei(), minPriceForTotalSpace: 1.TstWei(),
maxCollateral: 20.TstWei() maxCollateral: hostInitialBalance
); );
seller.Marketplace.MakeStorageAvailable(availability);
}
var testFile = GenerateTestFile(fileSize); foreach (var host in hosts)
var buyer = StartCodex(s => s
.WithName("Buyer")
.EnableMarketplace(geth, contracts, m => m
.WithAccount(myAccount)
.WithInitial(10.Eth(), buyerInitialBalance)));
var contentId = buyer.UploadFile(testFile);
var purchase = new StoragePurchaseRequest(contentId)
{ {
PricePerSlotPerSecond = 2.TstWei(), hostAccounts.Add(host.EthAccount);
RequiredCollateral = 10.TstWei(), host.Marketplace.MakeStorageAvailable(availability);
MinRequiredNumberOfNodes = 5, }
NodeFailureTolerance = 2, }
ProofProbability = 5,
Duration = TimeSpan.FromMinutes(6),
Expiry = TimeSpan.FromMinutes(5)
};
var purchaseContract = buyer.Marketplace.RequestStorage(purchase); private int GetNumberOfLiveHosts()
{
return Convert.ToInt32(GetNumberOfRequiredHosts()) + 3;
}
purchaseContract.WaitForStorageContractStarted(); private ByteSize Mult(ByteSize size, int mult)
{
return new ByteSize(size.SizeInBytes * mult);
}
purchaseContract.WaitForStorageContractFinished(); private ByteSize GetMinFileSizePlus(int plusMb)
{
return new ByteSize(GetMinFileSize().SizeInBytes + plusMb.MB().SizeInBytes);
}
private ByteSize GetMinFileSize()
{
ulong minSlotSize = 0;
ulong minNumHosts = 0;
foreach (var r in repo.Rewards)
{
var s = Convert.ToUInt64(r.CheckConfig.MinSlotSize.SizeInBytes);
var h = r.CheckConfig.MinNumberOfHosts;
if (s > minSlotSize) minSlotSize = s;
if (h > minNumHosts) minNumHosts = h;
}
var minFileSize = ((minSlotSize + 1024) * minNumHosts);
return new ByteSize(Convert.ToInt64(minFileSize));
}
private uint GetNumberOfRequiredHosts()
{
return Convert.ToUInt32(repo.Rewards.Max(r => r.CheckConfig.MinNumberOfHosts));
}
private TimeSpan GetMinRequiredRequestDuration()
{
return repo.Rewards.Max(r => r.CheckConfig.MinDuration) + TimeSpan.FromSeconds(10);
}
private string IdentifyAccount(string address)
{
if (address == clientAccount.EthAddress.Address) return "Client";
try
{
var index = hostAccounts.FindIndex(a => a.EthAddress.Address == address);
return "Host" + index;
}
catch
{
return "UNKNOWN";
}
}
public class RewardApiCalls
{
private readonly ContainerFileMonitor monitor;
public RewardApiCalls(ILog log, CoreInterface ci, RunningContainer botContainer)
{
monitor = new ContainerFileMonitor(log, ci, botContainer, "/app/datapath/logs/discordbot.log");
}
public void Start(Action<string, GiveRewardsCommand> onCommand)
{
monitor.Start(line => ParseLine(line, onCommand));
}
public void Stop()
{
monitor.Stop();
}
private void ParseLine(string line, Action<string, GiveRewardsCommand> onCommand)
{
try
{
var timestamp = line.Substring(0, 30);
var json = line.Substring(31);
var cmd = JsonConvert.DeserializeObject<GiveRewardsCommand>(json);
if (cmd != null)
{
onCommand(timestamp, cmd);
}
}
catch
{
}
}
}
public class ContainerFileMonitor
{
private readonly ILog log;
private readonly CoreInterface ci;
private readonly RunningContainer botContainer;
private readonly string filePath;
private readonly CancellationTokenSource cts = new CancellationTokenSource();
private readonly List<string> seenLines = new List<string>();
private Task worker = Task.CompletedTask;
private Action<string> onNewLine = c => { };
public ContainerFileMonitor(ILog log, CoreInterface ci, RunningContainer botContainer, string filePath)
{
this.log = log;
this.ci = ci;
this.botContainer = botContainer;
this.filePath = filePath;
}
public void Start(Action<string> onNewLine)
{
this.onNewLine = onNewLine;
worker = Task.Run(Worker);
}
public void Stop()
{
cts.Cancel();
worker.Wait();
}
// did any container crash? that's why it repeats?
private void Worker()
{
while (!cts.IsCancellationRequested)
{
Update();
}
}
private void Update()
{
Thread.Sleep(TimeSpan.FromSeconds(10));
if (cts.IsCancellationRequested) return;
var botLog = ci.ExecuteContainerCommand(botContainer, "cat", filePath);
var lines = botLog.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines)
{
// log.Log("line: " + line);
if (!seenLines.Contains(line))
{
seenLines.Add(line);
onNewLine(line);
}
}
}
} }
} }
} }

View File

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

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,6 +41,32 @@ namespace BiblioTech
public async Task MainAsync(string[] args) public async Task MainAsync(string[] args)
{ {
Log.Log("Starting Codex Discord Bot..."); Log.Log("Starting Codex Discord Bot...");
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) =>
{
options.ListenAnyIP(Config.RewardApiPort);
});
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
Log.Log("Running...");
await app.RunAsync();
await Task.Delay(-1);
}
private async Task StartDiscordBot()
{
client = new DiscordSocketClient(); client = new DiscordSocketClient();
client.Log += ClientLog; client.Log += ClientLog;
@ -60,19 +86,6 @@ namespace BiblioTech
await client.LoginAsync(TokenType.Bot, Config.ApplicationToken); await client.LoginAsync(TokenType.Bot, Config.ApplicationToken);
await client.StartAsync(); await client.StartAsync();
AdminChecker = new AdminChecker(); AdminChecker = new AdminChecker();
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel((context, options) =>
{
options.ListenAnyIP(Config.RewardApiPort);
});
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
Log.Log("Running...");
await app.RunAsync();
await Task.Delay(-1);
} }
private static void PrintHelp() private static void PrintHelp()

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); 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 r.State == RequestState.Failed
); );
} }
public string EntireString()
{
return JsonConvert.SerializeObject(StorageRequests);
}
public HistoricState()
{
}
public HistoricState(StorageRequest[] requests)
{
storageRequests.AddRange(requests);
}
} }
public class StorageRequest 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,122 +1,71 @@
using CodexContractsPlugin.Marketplace; using CodexContractsPlugin.ChainMonitor;
using DiscordRewards; using DiscordRewards;
using Logging;
using System.Numerics; using System.Numerics;
namespace TestNetRewarder 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(); var intervals = GetInsightCounts(config);
if (!intervalCounts.Any()) return Array.Empty<MarketAverage>();
UpdateBuffer(chainState, intervalCounts.Max()); foreach (var i in intervals)
var result = intervalCounts {
.Select(GenerateMarketAverage) buffers.Add(new MarketBuffer(
.Where(a => a != null) config.Interval * i
.Cast<MarketAverage>() ));
.ToArray();
if (!result.Any()) result = Array.Empty<MarketAverage>();
return result;
} }
private void UpdateBuffer(ChainState chainState, int maxNumberOfIntervals) this.log = log;
{
buffer.Add(chainState);
while (buffer.Count > maxNumberOfIntervals)
{
buffer.RemoveAt(0);
}
} }
private MarketAverage? GenerateMarketAverage(int numberOfIntervals) public MarketAverage[] GetAverages()
{ {
var states = SelectStates(numberOfIntervals); foreach (var b in buffers) b.Update();
return CreateAverage(states);
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 try
{ {
return new MarketAverage var tokens = config.MarketInsights.Split(';').ToArray();
{
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();
return tokens.Select(t => Convert.ToInt32(t)).ToArray(); return tokens.Select(t => Convert.ToInt32(t)).ToArray();
} }
catch (Exception ex) 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 CodexContractsPlugin;
using GethPlugin; using CodexContractsPlugin.ChainMonitor;
using Logging; using Logging;
using Newtonsoft.Json;
using Utils; using Utils;
namespace TestNetRewarder namespace TestNetRewarder
{ {
public class Processor public class Processor : ITimeSegmentHandler
{ {
private static readonly HistoricState historicState = new HistoricState(); private readonly RequestBuilder builder;
private static readonly RewardRepo rewardRepo = new RewardRepo(); private readonly RewardChecker rewardChecker;
private static readonly MarketTracker marketTracker = new MarketTracker(); private readonly MarketTracker marketTracker;
private readonly BufferLogger bufferLogger;
private readonly ChainState chainState;
private readonly BotClient client;
private readonly ILog log; 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; 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 try
{ {
var blockRange = connector.GethNode.ConvertTimeRangeToBlockRange(timeRange); chainState.Update(timeRange.To);
if (!IsNewBlockRange(blockRange))
{
log.Log($"Block range {blockRange} was previously processed. Skipping...");
return;
}
var chainState = new ChainState(historicState, connector.CodexContracts, blockRange); var averages = marketTracker.GetAverages();
await ProcessChainState(chainState); var lines = RemoveFirstLine(bufferLogger.Get());
var request = builder.Build(averages, lines);
if (request.HasAny())
{
await client.SendRewards(request);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
log.Error("Exception processing time segment: " + ex); var msg = "Exception processing time segment: " + ex;
log.Error(msg);
bufferLogger.Error(msg);
throw; throw;
} }
} }
private bool IsNewBlockRange(BlockInterval blockRange) private string[] RemoveFirstLine(string[] lines)
{ {
if (lastBlockRange == null || //if (!lines.Any()) return Array.Empty<string>();
lastBlockRange.From != blockRange.From || //return lines.Skip(1).ToArray();
lastBlockRange.To != blockRange.To) return lines;
{
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);
} }
} }
} }

View File

@ -6,10 +6,10 @@ namespace TestNetRewarder
{ {
public class Program public class Program
{ {
public static Configuration Config { get; private set; } = null!; public static CancellationToken CancellationToken;
public static ILog Log { get; private set; } = null!; private static Configuration Config = null!;
public static CancellationToken CancellationToken { get; private set; } private static ILog Log = null!;
public static BotClient BotClient { get; private set; } = null!; private static BotClient BotClient = null!;
private static Processor processor = null!; private static Processor processor = null!;
private static DateTime lastCheck = DateTime.MinValue; private static DateTime lastCheck = DateTime.MinValue;
@ -27,8 +27,11 @@ namespace TestNetRewarder
new ConsoleLog() new ConsoleLog()
); );
var connector = GethConnector.GethConnector.Initialize(Log);
if (connector == null) throw new Exception("Invalid Geth information");
BotClient = new BotClient(Config, Log); BotClient = new BotClient(Config, Log);
processor = new Processor(Log); processor = new Processor(Config, BotClient, connector.CodexContracts, Log);
EnsurePath(Config.DataPath); EnsurePath(Config.DataPath);
EnsurePath(Config.LogPath); EnsurePath(Config.LogPath);
@ -41,12 +44,12 @@ namespace TestNetRewarder
EnsureGethOnline(); EnsureGethOnline();
Log.Log("Starting TestNet Rewarder..."); Log.Log("Starting TestNet Rewarder...");
var segmenter = new TimeSegmenter(Log, Config); var segmenter = new TimeSegmenter(Log, Config, processor);
while (!CancellationToken.IsCancellationRequested) while (!CancellationToken.IsCancellationRequested)
{ {
await EnsureBotOnline(); await EnsureBotOnline();
await segmenter.WaitForNextSegment(processor.ProcessTimeSegment); await segmenter.ProcessNextSegment();
await Task.Delay(100, CancellationToken); 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 namespace TestNetRewarder
{ {
public interface ITimeSegmentHandler
{
Task OnNewSegment(TimeRange timeRange);
}
public class TimeSegmenter public class TimeSegmenter
{ {
private readonly ILog log; private readonly ILog log;
private readonly ITimeSegmentHandler handler;
private readonly TimeSpan segmentSize; 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.log = log;
this.handler = handler;
if (configuration.IntervalMinutes < 0) configuration.IntervalMinutes = 1; 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; 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)); 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 = latest + segmentSize;
var end = start + segmentSize; var waited = await WaitUntilTimeSegmentInPast(end);
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);
if (Program.CancellationToken.IsCancellationRequested) return; if (Program.CancellationToken.IsCancellationRequested) return;
var postfix = "(Catching up...)"; var postfix = "(Catching up...)";
if (waited) postfix = "(Real-time)"; if (waited) postfix = "(Real-time)";
log.Log($"Time segment [{latest} to {end}] {postfix}");
log.Log($"Time segment [{start} to {end}] {postfix}"); var range = new TimeRange(latest, end);
var range = new TimeRange(start, end); latest = end;
start = end;
await onSegment(range); await handler.OnNewSegment(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;
} }
} }
} }