diff --git a/Framework/DiscordRewards/CheckConfig.cs b/Framework/DiscordRewards/CheckConfig.cs index 9c4fccb0..34425cea 100644 --- a/Framework/DiscordRewards/CheckConfig.cs +++ b/Framework/DiscordRewards/CheckConfig.cs @@ -13,9 +13,9 @@ namespace DiscordRewards public enum CheckType { Uninitialized, - FilledSlot, - FinishedSlot, - PostedContract, - StartedContract, + HostFilledSlot, + HostFinishedSlot, + ClientPostedContract, + ClientStartedContract, } } diff --git a/Framework/DiscordRewards/GiveRewardsCommand.cs b/Framework/DiscordRewards/GiveRewardsCommand.cs index 48dabcca..dffb4ede 100644 --- a/Framework/DiscordRewards/GiveRewardsCommand.cs +++ b/Framework/DiscordRewards/GiveRewardsCommand.cs @@ -5,6 +5,11 @@ public RewardUsersCommand[] Rewards { get; set; } = Array.Empty(); public MarketAverage[] Averages { get; set; } = Array.Empty(); public string[] EventsOverview { get; set; } = Array.Empty(); + + public bool HasAny() + { + return Rewards.Any() || Averages.Any() || EventsOverview.Any(); + } } public class RewardUsersCommand diff --git a/Framework/DiscordRewards/RewardRepo.cs b/Framework/DiscordRewards/RewardRepo.cs index 51ac3fcb..28f6173a 100644 --- a/Framework/DiscordRewards/RewardRepo.cs +++ b/Framework/DiscordRewards/RewardRepo.cs @@ -11,19 +11,19 @@ namespace DiscordRewards // Filled any slot new RewardConfig(1187039439558541498, $"{Tag} successfully filled their first slot!", new CheckConfig { - Type = CheckType.FilledSlot + Type = CheckType.HostFilledSlot }), // Finished any slot new RewardConfig(1202286165630390339, $"{Tag} successfully finished their first slot!", new CheckConfig { - Type = CheckType.FinishedSlot + Type = CheckType.HostFinishedSlot }), // Finished a sizable slot new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot! (10mb/5mins for test)", new CheckConfig { - Type = CheckType.FinishedSlot, + Type = CheckType.HostFinishedSlot, MinSlotSize = 10.MB(), MinDuration = TimeSpan.FromMinutes(5.0), }), @@ -31,19 +31,19 @@ namespace DiscordRewards // Posted any contract new RewardConfig(1202286258370383913, $"{Tag} posted their first contract!", new CheckConfig { - Type = CheckType.PostedContract + Type = CheckType.ClientPostedContract }), // Started any contract new RewardConfig(1202286330873126992, $"A contract created by {Tag} reached Started state for the first time!", new CheckConfig { - Type = CheckType.StartedContract + Type = CheckType.ClientStartedContract }), // Started a sizable contract new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time! (10mb/5mins for test)", new CheckConfig { - Type = CheckType.StartedContract, + Type = CheckType.ClientStartedContract, MinNumberOfHosts = 4, MinSlotSize = 10.MB(), MinDuration = TimeSpan.FromMinutes(5.0), diff --git a/Framework/NethereumWorkflow/NethereumInteraction.cs b/Framework/NethereumWorkflow/NethereumInteraction.cs index f965c35a..01f26bf5 100644 --- a/Framework/NethereumWorkflow/NethereumInteraction.cs +++ b/Framework/NethereumWorkflow/NethereumInteraction.cs @@ -117,6 +117,7 @@ namespace NethereumWorkflow } return new BlockInterval( + timeRange: timeRange, from: fromBlock.Value, to: toBlock.Value ); diff --git a/Framework/Utils/BlockInterval.cs b/Framework/Utils/BlockInterval.cs index 79229bfa..64430488 100644 --- a/Framework/Utils/BlockInterval.cs +++ b/Framework/Utils/BlockInterval.cs @@ -2,7 +2,7 @@ { public class BlockInterval { - public BlockInterval(ulong from, ulong to) + public BlockInterval(TimeRange timeRange, ulong from, ulong to) { if (from < to) { @@ -14,10 +14,13 @@ From = to; To = from; } + TimeRange = timeRange; } public ulong From { get; } public ulong To { get; } + public TimeRange TimeRange { get; } + public ulong NumberOfBlocks => To - From; public override string ToString() { diff --git a/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainEvents.cs b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainEvents.cs new file mode 100644 index 00000000..25d08513 --- /dev/null +++ b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainEvents.cs @@ -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(); + 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() + ); + } + } +} diff --git a/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainState.cs b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainState.cs new file mode 100644 index 00000000..bc284eb2 --- /dev/null +++ b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainState.cs @@ -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 requests = new List(); + 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); + } + } +} diff --git a/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainStateRequest.cs b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainStateRequest.cs new file mode 100644 index 00000000..d908d193 --- /dev/null +++ b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/ChainStateRequest.cs @@ -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 hosts = new Dictionary(); + + 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(); + } + } +} diff --git a/ProjectPlugins/CodexContractsPlugin/ChainMonitor/DoNothingChainEventHandler.cs b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/DoNothingChainEventHandler.cs new file mode 100644 index 00000000..8a3709cc --- /dev/null +++ b/ProjectPlugins/CodexContractsPlugin/ChainMonitor/DoNothingChainEventHandler.cs @@ -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) + { + } + } +} diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs index 197d4f99..58fbb29b 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsAccess.cs @@ -2,10 +2,8 @@ using GethPlugin; using Logging; using Nethereum.ABI; -using Nethereum.Hex.HexTypes; using Nethereum.Util; using NethereumWorkflow; -using NethereumWorkflow.BlockUtils; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Utils; @@ -22,13 +20,10 @@ namespace CodexContractsPlugin TestToken GetTestTokenBalance(IHasEthAddress owner); TestToken GetTestTokenBalance(EthAddress ethAddress); - Request[] GetStorageRequests(BlockInterval blockRange); + ICodexContractsEvents GetEvents(TimeRange timeRange); + ICodexContractsEvents GetEvents(BlockInterval blockInterval); EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex); RequestState GetRequestState(Request request); - RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange); - RequestCancelledEventDTO[] GetRequestCancelledEvents(BlockInterval blockRange); - SlotFilledEventDTO[] GetSlotFilledEvents(BlockInterval blockRange); - SlotFreedEventDTO[] GetSlotFreedEvents(BlockInterval blockRange); } [JsonConverter(typeof(StringEnumConverter))] @@ -81,65 +76,14 @@ namespace CodexContractsPlugin return balance.TstWei(); } - public Request[] GetStorageRequests(BlockInterval blockRange) + public ICodexContractsEvents GetEvents(TimeRange timeRange) { - var events = gethNode.GetEvents(Deployment.MarketplaceAddress, blockRange); - var i = StartInteraction(); - return events - .Select(e => - { - var requestEvent = i.GetRequest(Deployment.MarketplaceAddress, e.Event.RequestId); - var result = requestEvent.ReturnValue1; - result.Block = GetBlock(e.Log.BlockNumber.ToUlong()); - result.RequestId = e.Event.RequestId; - return result; - }) - .ToArray(); + return GetEvents(gethNode.ConvertTimeRangeToBlockRange(timeRange)); } - public RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange) + public ICodexContractsEvents GetEvents(BlockInterval blockInterval) { - var events = gethNode.GetEvents(Deployment.MarketplaceAddress, blockRange); - return events.Select(e => - { - var result = e.Event; - result.Block = GetBlock(e.Log.BlockNumber.ToUlong()); - return result; - }).ToArray(); - } - - public RequestCancelledEventDTO[] GetRequestCancelledEvents(BlockInterval blockRange) - { - var events = gethNode.GetEvents(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(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(Deployment.MarketplaceAddress, blockRange); - return events.Select(e => - { - var result = e.Event; - result.Block = GetBlock(e.Log.BlockNumber.ToUlong()); - return result; - }).ToArray(); + return new CodexContractsEvents(log, gethNode, Deployment, blockInterval); } public EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex) @@ -170,17 +114,6 @@ namespace CodexContractsPlugin return gethNode.Call(Deployment.MarketplaceAddress, func); } - private BlockTimeEntry GetBlock(ulong number) - { - return gethNode.GetBlockForNumber(number); - } - - private EthAddress GetEthAddressFromTransaction(string transactionHash) - { - var transaction = gethNode.GetTransaction(transactionHash); - return new EthAddress(transaction.From); - } - private ContractInteractions StartInteraction() { return new ContractInteractions(log, gethNode); diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs new file mode 100644 index 00000000..cf7b2d74 --- /dev/null +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsEvents.cs @@ -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(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(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(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(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(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); + } + } +} diff --git a/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs b/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs index 68427b60..43311430 100644 --- a/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs +++ b/ProjectPlugins/CodexContractsPlugin/Marketplace/Customizations.cs @@ -5,35 +5,49 @@ using Newtonsoft.Json; namespace CodexContractsPlugin.Marketplace { - public partial class Request : RequestBase + public interface IHasBlock + { + BlockTimeEntry Block { get; set; } + } + + public partial class Request : RequestBase, IHasBlock { [JsonIgnore] public BlockTimeEntry Block { get; set; } public byte[] RequestId { get; set; } public EthAddress ClientAddress { get { return new EthAddress(Client); } } + + [JsonIgnore] + public string Id + { + get + { + return BitConverter.ToString(RequestId).Replace("-", "").ToLowerInvariant(); + } + } } - public partial class RequestFulfilledEventDTO + public partial class RequestFulfilledEventDTO : IHasBlock { [JsonIgnore] public BlockTimeEntry Block { get; set; } } - public partial class RequestCancelledEventDTO + public partial class RequestCancelledEventDTO : IHasBlock { [JsonIgnore] public BlockTimeEntry Block { get; set; } } - public partial class SlotFilledEventDTO + public partial class SlotFilledEventDTO : IHasBlock { [JsonIgnore] public BlockTimeEntry Block { get; set; } public EthAddress Host { get; set; } } - public partial class SlotFreedEventDTO + public partial class SlotFreedEventDTO : IHasBlock { [JsonIgnore] public BlockTimeEntry Block { get; set; } diff --git a/ProjectPlugins/CodexDiscordBotPlugin/CodexDiscordBotPlugin.cs b/ProjectPlugins/CodexDiscordBotPlugin/CodexDiscordBotPlugin.cs index f347805a..556ef152 100644 --- a/ProjectPlugins/CodexDiscordBotPlugin/CodexDiscordBotPlugin.cs +++ b/ProjectPlugins/CodexDiscordBotPlugin/CodexDiscordBotPlugin.cs @@ -1,11 +1,13 @@ using Core; using KubernetesWorkflow; using KubernetesWorkflow.Types; +using Utils; namespace CodexDiscordBotPlugin { public class CodexDiscordBotPlugin : IProjectPlugin, IHasLogPrefix, IHasMetadata { + private const string ExpectedStartupMessage = "Debug option is set. Discord connection disabled!"; private readonly IPluginTools tools; public CodexDiscordBotPlugin(IPluginTools tools) @@ -46,14 +48,59 @@ namespace CodexDiscordBotPlugin var startupConfig = new StartupConfig(); startupConfig.NameOverride = config.Name; startupConfig.Add(config); - return workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig).WaitForOnline(); + var pod = workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig).WaitForOnline(); + WaitForStartupMessage(workflow, pod); + workflow.CreateCrashWatcher(pod.Containers.Single()).Start(); + return pod; } private RunningPod StartRewarderContainer(IStartupWorkflow workflow, RewarderBotStartupConfig config) { var startupConfig = new StartupConfig(); + startupConfig.NameOverride = config.Name; startupConfig.Add(config); - return workflow.Start(1, new RewarderBotContainerRecipe(), startupConfig).WaitForOnline(); + var pod = workflow.Start(1, new RewarderBotContainerRecipe(), startupConfig).WaitForOnline(); + workflow.CreateCrashWatcher(pod.Containers.Single()).Start(); + return pod; + } + + private void WaitForStartupMessage(IStartupWorkflow workflow, RunningPod pod) + { + var finder = new LogLineFinder(ExpectedStartupMessage, workflow); + Time.WaitUntil(() => + { + finder.FindLine(pod); + return finder.Found; + }, nameof(WaitForStartupMessage)); + } + + public class LogLineFinder : LogHandler + { + private readonly string message; + private readonly IStartupWorkflow workflow; + + public LogLineFinder(string message, IStartupWorkflow workflow) + { + this.message = message; + this.workflow = workflow; + } + + public void FindLine(RunningPod pod) + { + Found = false; + foreach (var c in pod.Containers) + { + workflow.DownloadContainerLog(c, this); + if (Found) return; + } + } + + public bool Found { get; private set; } + + protected override void ProcessLine(string line) + { + if (!Found && line.Contains(message)) Found = true; + } } } } diff --git a/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotContainerRecipe.cs b/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotContainerRecipe.cs index 9a2e3fed..86ee1e25 100644 --- a/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotContainerRecipe.cs +++ b/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotContainerRecipe.cs @@ -7,7 +7,7 @@ namespace CodexDiscordBotPlugin public class DiscordBotContainerRecipe : ContainerRecipeFactory { public override string AppName => "discordbot-bibliotech"; - public override string Image => "codexstorage/codex-discordbot:sha-8c64352"; + public override string Image => "codexstorage/codex-discordbot:sha-22cf82b"; public static string RewardsPort = "bot_rewards_port"; @@ -33,6 +33,8 @@ namespace CodexDiscordBotPlugin AddEnvVar("CODEXCONTRACTS_TOKENADDRESS", gethInfo.TokenAddress); AddEnvVar("CODEXCONTRACTS_ABI", gethInfo.Abi); + AddEnvVar("NODISCORD", "1"); + AddInternalPortAndVar("REWARDAPIPORT", RewardsPort); if (!string.IsNullOrEmpty(config.DataPath)) diff --git a/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotStartupConfig.cs b/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotStartupConfig.cs index bbef0b1a..e77552e5 100644 --- a/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotStartupConfig.cs +++ b/ProjectPlugins/CodexDiscordBotPlugin/DiscordBotStartupConfig.cs @@ -27,8 +27,9 @@ public class RewarderBotStartupConfig { - public RewarderBotStartupConfig(string discordBotHost, int discordBotPort, string intervalMinutes, DateTime historyStartUtc, DiscordBotGethInfo gethInfo, string? dataPath) + public RewarderBotStartupConfig(string name, string discordBotHost, int discordBotPort, int intervalMinutes, DateTime historyStartUtc, DiscordBotGethInfo gethInfo, string? dataPath) { + Name = name; DiscordBotHost = discordBotHost; DiscordBotPort = discordBotPort; IntervalMinutes = intervalMinutes; @@ -37,9 +38,10 @@ DataPath = dataPath; } + public string Name { get; } public string DiscordBotHost { get; } public int DiscordBotPort { get; } - public string IntervalMinutes { get; } + public int IntervalMinutes { get; } public DateTime HistoryStartUtc { get; } public DiscordBotGethInfo GethInfo { get; } public string? DataPath { get; set; } diff --git a/ProjectPlugins/CodexDiscordBotPlugin/RewarderBotContainerRecipe.cs b/ProjectPlugins/CodexDiscordBotPlugin/RewarderBotContainerRecipe.cs index 816c910d..2d11cbd4 100644 --- a/ProjectPlugins/CodexDiscordBotPlugin/RewarderBotContainerRecipe.cs +++ b/ProjectPlugins/CodexDiscordBotPlugin/RewarderBotContainerRecipe.cs @@ -7,7 +7,8 @@ namespace CodexDiscordBotPlugin public class RewarderBotContainerRecipe : ContainerRecipeFactory { public override string AppName => "discordbot-rewarder"; - public override string Image => "codexstorage/codex-rewarderbot:sha-2ab84e2"; + public override string Image => "thatbenbierens/codex-rewardbot:newstate"; + //"codexstorage/codex-rewarderbot:sha-12dc7ef"; protected override void Initialize(StartupConfig startupConfig) { @@ -17,7 +18,7 @@ namespace CodexDiscordBotPlugin AddEnvVar("DISCORDBOTHOST", config.DiscordBotHost); AddEnvVar("DISCORDBOTPORT", config.DiscordBotPort.ToString()); - AddEnvVar("INTERVALMINUTES", config.IntervalMinutes); + AddEnvVar("INTERVALMINUTES", config.IntervalMinutes.ToString()); var offset = new DateTimeOffset(config.HistoryStartUtc); AddEnvVar("CHECKHISTORY", offset.ToUnixTimeSeconds().ToString()); diff --git a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs index f1183779..564e81a1 100644 --- a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs +++ b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs @@ -109,8 +109,9 @@ namespace CodexPlugin // Custom scripting in the Codex test image will write this variable to a private-key file, // and pass the correct filename to Codex. - AddEnvVar("PRIV_KEY", marketplaceSetup.EthAccount.PrivateKey); - Additional(marketplaceSetup.EthAccount); + var account = marketplaceSetup.EthAccountSetup.GetNew(); + AddEnvVar("PRIV_KEY", account.PrivateKey); + Additional(account); SetCommandOverride(marketplaceSetup); if (marketplaceSetup.IsValidator) diff --git a/ProjectPlugins/CodexPlugin/CodexNode.cs b/ProjectPlugins/CodexPlugin/CodexNode.cs index fd6d1f70..c5372f65 100644 --- a/ProjectPlugins/CodexPlugin/CodexNode.cs +++ b/ProjectPlugins/CodexPlugin/CodexNode.cs @@ -26,13 +26,13 @@ namespace CodexPlugin CrashWatcher CrashWatcher { get; } PodInfo GetPodInfo(); ITransferSpeeds TransferSpeeds { get; } + EthAccount EthAccount { get; } /// /// Warning! The node is not usable after this. /// TODO: Replace with delete-blocks debug call once available in Codex. /// void DeleteRepoFolder(); - void Stop(bool waitTillStopped); } @@ -40,13 +40,13 @@ namespace CodexPlugin { private const string UploadFailedMessage = "Unable to store block"; private readonly IPluginTools tools; - private readonly EthAddress? ethAddress; + private readonly EthAccount? ethAccount; private readonly TransferSpeeds transferSpeeds; - public CodexNode(IPluginTools tools, CodexAccess codexAccess, CodexNodeGroup group, IMarketplaceAccess marketplaceAccess, EthAddress? ethAddress) + public CodexNode(IPluginTools tools, CodexAccess codexAccess, CodexNodeGroup group, IMarketplaceAccess marketplaceAccess, EthAccount? ethAccount) { this.tools = tools; - this.ethAddress = ethAddress; + this.ethAccount = ethAccount; CodexAccess = codexAccess; Group = group; Marketplace = marketplaceAccess; @@ -76,8 +76,17 @@ namespace CodexPlugin { get { - if (ethAddress == null) throw new Exception("Marketplace is not enabled for this Codex node. Please start it with the option '.EnableMarketplace(...)' to enable it."); - return ethAddress; + EnsureMarketplace(); + return ethAccount!.EthAddress; + } + } + + public EthAccount EthAccount + { + get + { + EnsureMarketplace(); + return ethAccount!; } } @@ -231,6 +240,11 @@ namespace CodexPlugin CodexAccess.LogDiskSpace("After download"); } + private void EnsureMarketplace() + { + if (ethAccount == null) throw new Exception("Marketplace is not enabled for this Codex node. Please start it with the option '.EnableMarketplace(...)' to enable it."); + } + private void Log(string msg) { tools.GetLog().Log($"{GetName()}: {msg}"); diff --git a/ProjectPlugins/CodexPlugin/CodexNodeFactory.cs b/ProjectPlugins/CodexPlugin/CodexNodeFactory.cs index 18483de6..425d489d 100644 --- a/ProjectPlugins/CodexPlugin/CodexNodeFactory.cs +++ b/ProjectPlugins/CodexPlugin/CodexNodeFactory.cs @@ -22,22 +22,22 @@ namespace CodexPlugin public CodexNode CreateOnlineCodexNode(CodexAccess access, CodexNodeGroup group) { - var ethAddress = GetEthAddress(access); - var marketplaceAccess = GetMarketplaceAccess(access, ethAddress); - return new CodexNode(tools, access, group, marketplaceAccess, ethAddress); + var ethAccount = GetEthAccount(access); + var marketplaceAccess = GetMarketplaceAccess(access, ethAccount); + return new CodexNode(tools, access, group, marketplaceAccess, ethAccount); } - private IMarketplaceAccess GetMarketplaceAccess(CodexAccess codexAccess, EthAddress? ethAddress) + private IMarketplaceAccess GetMarketplaceAccess(CodexAccess codexAccess, EthAccount? ethAccount) { - if (ethAddress == null) return new MarketplaceUnavailable(); + if (ethAccount == null) return new MarketplaceUnavailable(); return new MarketplaceAccess(tools.GetLog(), codexAccess); } - private EthAddress? GetEthAddress(CodexAccess access) + private EthAccount? GetEthAccount(CodexAccess access) { var ethAccount = access.Container.Containers.Single().Recipe.Additionals.Get(); if (ethAccount == null) return null; - return ethAccount.EthAddress; + return ethAccount; } public CrashWatcher CreateCrashWatcher(RunningContainer c) diff --git a/ProjectPlugins/CodexPlugin/CodexSetup.cs b/ProjectPlugins/CodexPlugin/CodexSetup.cs index 04aaf427..44454c77 100644 --- a/ProjectPlugins/CodexPlugin/CodexSetup.cs +++ b/ProjectPlugins/CodexPlugin/CodexSetup.cs @@ -169,7 +169,7 @@ namespace CodexPlugin public bool IsValidator { get; private set; } public Ether InitialEth { get; private set; } = 0.Eth(); public TestToken InitialTestTokens { get; private set; } = 0.Tst(); - public EthAccount EthAccount { get; private set; } = EthAccount.GenerateNew(); + public EthAccountSetup EthAccountSetup { get; private set; } = new EthAccountSetup(); public IMarketplaceSetup AsStorageNode() { @@ -185,7 +185,7 @@ namespace CodexPlugin public IMarketplaceSetup WithAccount(EthAccount account) { - EthAccount = account; + EthAccountSetup.Pin(account); return this; } @@ -201,10 +201,41 @@ namespace CodexPlugin var result = "[(clientNode)"; // When marketplace is enabled, being a clientNode is implicit. result += IsStorageNode ? "(storageNode)" : "()"; result += IsValidator ? "(validator)" : "() "; - result += $"Address: '{EthAccount.EthAddress}' "; + result += $"Address: '{EthAccountSetup}' "; result += $"{InitialEth.Eth} / {InitialTestTokens}"; result += "] "; return result; } } + + public class EthAccountSetup + { + private readonly List accounts = new List(); + 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()); + } + } } diff --git a/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs b/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs index 6f5e47e7..11f3b60e 100644 --- a/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs +++ b/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs @@ -1,5 +1,4 @@ using Logging; -using Newtonsoft.Json; using Utils; namespace CodexPlugin @@ -7,7 +6,7 @@ namespace CodexPlugin public interface IMarketplaceAccess { string MakeStorageAvailable(StorageAvailability availability); - StoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase); + IStoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase); } public class MarketplaceAccess : IMarketplaceAccess @@ -21,7 +20,7 @@ namespace CodexPlugin this.codexAccess = codexAccess; } - public StoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase) + public IStoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase) { purchase.Log(log); @@ -68,7 +67,7 @@ namespace CodexPlugin throw new NotImplementedException(); } - public StoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase) + public IStoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase) { Unavailable(); throw new NotImplementedException(); @@ -80,134 +79,4 @@ namespace CodexPlugin throw new InvalidOperationException(); } } - - public class StoragePurchaseContract - { - private readonly ILog log; - private readonly CodexAccess codexAccess; - private readonly TimeSpan gracePeriod = TimeSpan.FromSeconds(30); - private readonly DateTime contractPendingUtc = DateTime.UtcNow; - private DateTime? contractSubmittedUtc = DateTime.UtcNow; - private DateTime? contractStartedUtc; - private DateTime? contractFinishedUtc; - - public StoragePurchaseContract(ILog log, CodexAccess codexAccess, string purchaseId, StoragePurchaseRequest purchase) - { - this.log = log; - this.codexAccess = codexAccess; - PurchaseId = purchaseId; - Purchase = purchase; - - ContentId = new ContentId(codexAccess.GetPurchaseStatus(purchaseId).Request.Content.Cid); - } - - public string PurchaseId { get; } - public StoragePurchaseRequest Purchase { get; } - public ContentId ContentId { get; } - - public TimeSpan? PendingToSubmitted => contractSubmittedUtc - contractPendingUtc; - public TimeSpan? SubmittedToStarted => contractStartedUtc - contractSubmittedUtc; - public TimeSpan? SubmittedToFinished => contractFinishedUtc - contractSubmittedUtc; - - public void WaitForStorageContractSubmitted() - { - WaitForStorageContractState(gracePeriod, "submitted", sleep: 200); - contractSubmittedUtc = DateTime.UtcNow; - LogSubmittedDuration(); - AssertDuration(PendingToSubmitted, gracePeriod, nameof(PendingToSubmitted)); - } - - public void WaitForStorageContractStarted() - { - var timeout = Purchase.Expiry + gracePeriod; - - WaitForStorageContractState(timeout, "started"); - contractStartedUtc = DateTime.UtcNow; - LogStartedDuration(); - AssertDuration(SubmittedToStarted, timeout, nameof(SubmittedToStarted)); - } - - public void WaitForStorageContractFinished() - { - if (!contractStartedUtc.HasValue) - { - WaitForStorageContractStarted(); - } - var currentContractTime = DateTime.UtcNow - contractSubmittedUtc!.Value; - var timeout = (Purchase.Duration - currentContractTime) + gracePeriod; - WaitForStorageContractState(timeout, "finished"); - contractFinishedUtc = DateTime.UtcNow; - LogFinishedDuration(); - AssertDuration(SubmittedToFinished, timeout, nameof(SubmittedToFinished)); - } - - public StoragePurchase GetPurchaseStatus(string purchaseId) - { - return codexAccess.GetPurchaseStatus(purchaseId); - } - - private void WaitForStorageContractState(TimeSpan timeout, string desiredState, int sleep = 1000) - { - var lastState = ""; - var waitStart = DateTime.UtcNow; - - Log($"Waiting for {Time.FormatDuration(timeout)} to reach state '{desiredState}'."); - while (lastState != desiredState) - { - var purchaseStatus = codexAccess.GetPurchaseStatus(PurchaseId); - var statusJson = JsonConvert.SerializeObject(purchaseStatus); - if (purchaseStatus != null && purchaseStatus.State != lastState) - { - lastState = purchaseStatus.State; - log.Debug("Purchase status: " + statusJson); - } - - Thread.Sleep(sleep); - - if (lastState == "errored") - { - FrameworkAssert.Fail("Contract errored: " + statusJson); - } - - if (DateTime.UtcNow - waitStart > timeout) - { - FrameworkAssert.Fail($"Contract did not reach '{desiredState}' within {Time.FormatDuration(timeout)} timeout. {statusJson}"); - } - } - } - - private void LogSubmittedDuration() - { - Log($"Pending to Submitted in {Time.FormatDuration(PendingToSubmitted)} " + - $"( < {Time.FormatDuration(gracePeriod)})"); - } - - private void LogStartedDuration() - { - Log($"Submitted to Started in {Time.FormatDuration(SubmittedToStarted)} " + - $"( < {Time.FormatDuration(Purchase.Expiry + gracePeriod)})"); - } - - private void LogFinishedDuration() - { - Log($"Submitted to Finished in {Time.FormatDuration(SubmittedToFinished)} " + - $"( < {Time.FormatDuration(Purchase.Duration + gracePeriod)})"); - } - - private void AssertDuration(TimeSpan? span, TimeSpan max, string message) - { - if (span == null) throw new ArgumentNullException(nameof(MarketplaceAccess) + ": " + message + " (IsNull)"); - if (span.Value.TotalDays >= max.TotalSeconds) - { - throw new Exception(nameof(MarketplaceAccess) + - $": Duration out of range. Max: {Time.FormatDuration(max)} but was: {Time.FormatDuration(span.Value)} " + - message); - } - } - - private void Log(string msg) - { - log.Log($"[{PurchaseId}] {msg}"); - } - } } diff --git a/ProjectPlugins/CodexPlugin/StoragePurchaseContract.cs b/ProjectPlugins/CodexPlugin/StoragePurchaseContract.cs new file mode 100644 index 00000000..34462310 --- /dev/null +++ b/ProjectPlugins/CodexPlugin/StoragePurchaseContract.cs @@ -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}"); + } + } +} diff --git a/ProjectPlugins/GethPlugin/EthAccount.cs b/ProjectPlugins/GethPlugin/EthAccount.cs index 9f1318b7..60bf40d8 100644 --- a/ProjectPlugins/GethPlugin/EthAccount.cs +++ b/ProjectPlugins/GethPlugin/EthAccount.cs @@ -24,5 +24,10 @@ namespace GethPlugin return new EthAccount(ethAddress, account.PrivateKey); } + + public override string ToString() + { + return EthAddress.ToString(); + } } } diff --git a/Tests/CodexTests/BasicTests/MarketplaceTests.cs b/Tests/CodexTests/BasicTests/MarketplaceTests.cs index 756f8637..3f45469d 100644 --- a/Tests/CodexTests/BasicTests/MarketplaceTests.cs +++ b/Tests/CodexTests/BasicTests/MarketplaceTests.cs @@ -65,8 +65,8 @@ namespace CodexTests.BasicTests MinRequiredNumberOfNodes = 5, NodeFailureTolerance = 2, ProofProbability = 5, - Duration = TimeSpan.FromMinutes(5), - Expiry = TimeSpan.FromMinutes(4) + Duration = TimeSpan.FromMinutes(6), + Expiry = TimeSpan.FromMinutes(5) }; var purchaseContract = client.Marketplace.RequestStorage(purchase); @@ -129,8 +129,8 @@ namespace CodexTests.BasicTests { Time.Retry(() => { - var blockRange = geth.ConvertTimeRangeToBlockRange(GetTestRunTimeRange()); - var slotFilledEvents = contracts.GetSlotFilledEvents(blockRange); + var events = contracts.GetEvents(GetTestRunTimeRange()); + var slotFilledEvents = events.GetSlotFilledEvents(); var msg = $"SlotFilledEvents: {slotFilledEvents.Length} - NumSlots: {purchase.MinRequiredNumberOfNodes}"; Debug(msg); @@ -147,7 +147,8 @@ namespace CodexTests.BasicTests private Request GetOnChainStorageRequest(ICodexContracts contracts, IGethNode geth) { - var requests = contracts.GetStorageRequests(geth.ConvertTimeRangeToBlockRange(GetTestRunTimeRange())); + var events = contracts.GetEvents(GetTestRunTimeRange()); + var requests = events.GetStorageRequests(); Assert.That(requests.Length, Is.EqualTo(1)); return requests.Single(); } diff --git a/Tests/CodexTests/CodexTests.csproj b/Tests/CodexTests/CodexTests.csproj index 495caf6f..4a9a3edb 100644 --- a/Tests/CodexTests/CodexTests.csproj +++ b/Tests/CodexTests/CodexTests.csproj @@ -13,11 +13,13 @@ + + diff --git a/Tests/CodexTests/UtilityTests/DiscordBotTests.cs b/Tests/CodexTests/UtilityTests/DiscordBotTests.cs index de3c5916..c6b6b41f 100644 --- a/Tests/CodexTests/UtilityTests/DiscordBotTests.cs +++ b/Tests/CodexTests/UtilityTests/DiscordBotTests.cs @@ -1,7 +1,13 @@ using CodexContractsPlugin; using CodexDiscordBotPlugin; using CodexPlugin; +using Core; +using DiscordRewards; +using DistTestCore; using GethPlugin; +using KubernetesWorkflow.Types; +using Logging; +using Newtonsoft.Json; using NUnit.Framework; using Utils; @@ -10,21 +16,155 @@ namespace CodexTests.UtilityTests [TestFixture] public class DiscordBotTests : AutoBootstrapDistTest { + private readonly RewardRepo repo = new RewardRepo(); + private readonly TestToken hostInitialBalance = 3000000.TstWei(); + private readonly TestToken clientInitialBalance = 1000000000.TstWei(); + private readonly EthAccount clientAccount = EthAccount.GenerateNew(); + private readonly List hostAccounts = new List(); + private readonly List rewardsSeen = new List(); + private readonly TimeSpan rewarderInterval = TimeSpan.FromMinutes(1); + private readonly List receivedEvents = new List(); + private readonly List receivedAverages = new List(); + [Test] - [Ignore("Used for debugging bots")] + [DontDownloadLogs] public void BotRewardTest() { - var myAccount = EthAccount.GenerateNew(); - - var sellerInitialBalance = 234.TstWei(); - var buyerInitialBalance = 100000.TstWei(); - var fileSize = 11.MB(); - var geth = Ci.StartGethNode(s => s.IsMiner().WithName("disttest-geth")); var contracts = Ci.StartCodexContracts(geth); + var gethInfo = CreateGethInfo(geth, contracts); - // start bot and rewarder - var gethInfo = new DiscordBotGethInfo( + var botContainer = StartDiscordBot(gethInfo); + var rewarderContainer = StartRewarderBot(gethInfo, botContainer); + + StartHosts(geth, contracts); + var client = StartClient(geth, contracts); + + var apiCalls = new RewardApiCalls(GetTestLog(), Ci, botContainer); + apiCalls.Start(OnCommand); + + var purchaseContract = ClientPurchasesStorage(client); + purchaseContract.WaitForStorageContractStarted(); + purchaseContract.WaitForStorageContractFinished(); + Thread.Sleep(rewarderInterval * 3); + + apiCalls.Stop(); + + AssertEventOccurance("Created as New.", 1); + AssertEventOccurance("SlotFilled", Convert.ToInt32(GetNumberOfRequiredHosts())); + AssertEventOccurance("Transit: New -> Started", 1); + AssertEventOccurance("Transit: Started -> Finished", 1); + + AssertMarketAverage(); + + foreach (var r in repo.Rewards) + { + var seen = rewardsSeen.Any(s => r.RoleId == s); + + Log($"{Lookup(r.RoleId)} = {seen}"); + } + + Assert.That(repo.Rewards.All(r => rewardsSeen.Contains(r.RoleId))); + } + + private string Lookup(ulong rewardId) + { + var reward = repo.Rewards.Single(r => r.RoleId == rewardId); + return $"({rewardId})'{reward.Message}'"; + } + + private void AssertEventOccurance(string msg, int expectedCount) + { + Assert.That(receivedEvents.Count(e => e.Contains(msg)), Is.EqualTo(expectedCount), + $"Event '{msg}' did not occure correct number of times."); + } + + private void AssertMarketAverage() + { + Assert.That(receivedAverages.Count, Is.EqualTo(1)); + var a = receivedAverages.Single(); + + Assert.That(a.NumberOfFinished, Is.EqualTo(1)); + Assert.That(a.TimeRangeSeconds, Is.EqualTo(5760)); + Assert.That(a.Price, Is.EqualTo(2.0f).Within(0.1f)); + Assert.That(a.Size, Is.EqualTo(GetMinFileSize().SizeInBytes).Within(1.0f)); + Assert.That(a.Duration, Is.EqualTo(GetMinRequiredRequestDuration().TotalSeconds).Within(1.0f)); + Assert.That(a.Collateral, Is.EqualTo(10.0f).Within(0.1f)); + Assert.That(a.ProofProbability, Is.EqualTo(5.0f).Within(0.1f)); + } + + private void OnCommand(string timestamp, GiveRewardsCommand call) + { + Log($""); + 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($""); + } + + private IStoragePurchaseContract ClientPurchasesStorage(ICodexNode client) + { + var testFile = GenerateTestFile(GetMinFileSize()); + var contentId = client.UploadFile(testFile); + var purchase = new StoragePurchaseRequest(contentId) + { + PricePerSlotPerSecond = 2.TstWei(), + RequiredCollateral = 10.TstWei(), + MinRequiredNumberOfNodes = GetNumberOfRequiredHosts(), + NodeFailureTolerance = 2, + ProofProbability = 5, + Duration = GetMinRequiredRequestDuration(), + Expiry = GetMinRequiredRequestDuration() - TimeSpan.FromMinutes(1) + }; + + return client.Marketplace.RequestStorage(purchase); + } + + private ICodexNode StartClient(IGethNode geth, ICodexContracts contracts) + { + var node = StartCodex(s => s + .WithName("Client") + .EnableMarketplace(geth, contracts, m => m + .WithAccount(clientAccount) + .WithInitial(10.Eth(), clientInitialBalance))); + + Log($"Client {node.EthAccount.EthAddress}"); + return node; + } + + private RunningPod StartRewarderBot(DiscordBotGethInfo gethInfo, RunningContainer botContainer) + { + return Ci.DeployRewarderBot(new RewarderBotStartupConfig( + name: "rewarder-bot", + discordBotHost: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Host, + discordBotPort: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Port, + intervalMinutes: Convert.ToInt32(Math.Round(rewarderInterval.TotalMinutes)), + historyStartUtc: DateTime.UtcNow, + gethInfo: gethInfo, + dataPath: null + )); + } + + private DiscordBotGethInfo CreateGethInfo(IGethNode geth, ICodexContracts contracts) + { + return new DiscordBotGethInfo( host: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Host, port: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Port, privKey: geth.StartResult.Account.PrivateKey, @@ -32,8 +172,12 @@ namespace CodexTests.UtilityTests tokenAddress: contracts.Deployment.TokenAddress, abi: contracts.Deployment.Abi ); + } + + private RunningContainer StartDiscordBot(DiscordBotGethInfo gethInfo) + { var bot = Ci.DeployCodexDiscordBot(new DiscordBotStartupConfig( - name: "bot", + name: "discord-bot", token: "aaa", serverName: "ThatBen's server", adminRoleName: "bottest-admins", @@ -42,70 +186,190 @@ namespace CodexTests.UtilityTests kubeNamespace: "notneeded", gethInfo: gethInfo )); - var botContainer = bot.Containers.Single(); - Ci.DeployRewarderBot(new RewarderBotStartupConfig( - //discordBotHost: "http://" + botContainer.GetAddress(GetTestLog(), DiscordBotContainerRecipe.RewardsPort).Host, - //discordBotPort: botContainer.GetAddress(GetTestLog(), DiscordBotContainerRecipe.RewardsPort).Port, - discordBotHost: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Host, - discordBotPort: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Port, - intervalMinutes: "1", - historyStartUtc: GetTestRunTimeRange().From - TimeSpan.FromMinutes(3), - gethInfo: gethInfo, - dataPath: null - )); + return bot.Containers.Single(); + } - var numberOfHosts = 3; + private void StartHosts(IGethNode geth, ICodexContracts contracts) + { + var hosts = StartCodex(GetNumberOfLiveHosts(), s => s + .WithName("Host") + .WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn) + { + ContractClock = CodexLogLevel.Trace, + }) + .WithStorageQuota(Mult(GetMinFileSizePlus(50), GetNumberOfLiveHosts())) + .EnableMarketplace(geth, contracts, m => m + .WithInitial(10.Eth(), hostInitialBalance) + .AsStorageNode() + .AsValidator())); - for (var i = 0; i < numberOfHosts; i++) + var availability = new StorageAvailability( + totalSpace: Mult(GetMinFileSize(), GetNumberOfLiveHosts()), + maxDuration: TimeSpan.FromMinutes(30), + minPriceForTotalSpace: 1.TstWei(), + maxCollateral: hostInitialBalance + ); + + foreach (var host in hosts) { - var seller = StartCodex(s => s - .WithName("Seller") - .WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn) - { - ContractClock = CodexLogLevel.Trace, - }) - .WithStorageQuota(11.GB()) - .EnableMarketplace(geth, contracts, m => m - .WithAccount(myAccount) - .WithInitial(10.Eth(), sellerInitialBalance) - .AsStorageNode() - .AsValidator())); + hostAccounts.Add(host.EthAccount); + host.Marketplace.MakeStorageAvailable(availability); + } + } - var availability = new StorageAvailability( - totalSpace: 10.GB(), - maxDuration: TimeSpan.FromMinutes(30), - minPriceForTotalSpace: 1.TstWei(), - maxCollateral: 20.TstWei() - ); - seller.Marketplace.MakeStorageAvailable(availability); + private int GetNumberOfLiveHosts() + { + return Convert.ToInt32(GetNumberOfRequiredHosts()) + 3; + } + + private ByteSize Mult(ByteSize size, int mult) + { + return new ByteSize(size.SizeInBytes * mult); + } + + private ByteSize GetMinFileSizePlus(int plusMb) + { + return new ByteSize(GetMinFileSize().SizeInBytes + plusMb.MB().SizeInBytes); + } + + private ByteSize GetMinFileSize() + { + ulong minSlotSize = 0; + ulong minNumHosts = 0; + foreach (var r in repo.Rewards) + { + var s = Convert.ToUInt64(r.CheckConfig.MinSlotSize.SizeInBytes); + var h = r.CheckConfig.MinNumberOfHosts; + if (s > minSlotSize) minSlotSize = s; + if (h > minNumHosts) minNumHosts = h; } - var testFile = GenerateTestFile(fileSize); + var minFileSize = ((minSlotSize + 1024) * minNumHosts); + return new ByteSize(Convert.ToInt64(minFileSize)); + } - var buyer = StartCodex(s => s - .WithName("Buyer") - .EnableMarketplace(geth, contracts, m => m - .WithAccount(myAccount) - .WithInitial(10.Eth(), buyerInitialBalance))); + private uint GetNumberOfRequiredHosts() + { + return Convert.ToUInt32(repo.Rewards.Max(r => r.CheckConfig.MinNumberOfHosts)); + } - var contentId = buyer.UploadFile(testFile); + private TimeSpan GetMinRequiredRequestDuration() + { + return repo.Rewards.Max(r => r.CheckConfig.MinDuration) + TimeSpan.FromSeconds(10); + } - var purchase = new StoragePurchaseRequest(contentId) + private string IdentifyAccount(string address) + { + if (address == clientAccount.EthAddress.Address) return "Client"; + try { - PricePerSlotPerSecond = 2.TstWei(), - RequiredCollateral = 10.TstWei(), - MinRequiredNumberOfNodes = 5, - NodeFailureTolerance = 2, - ProofProbability = 5, - Duration = TimeSpan.FromMinutes(6), - Expiry = TimeSpan.FromMinutes(5) - }; + var index = hostAccounts.FindIndex(a => a.EthAddress.Address == address); + return "Host" + index; + } + catch + { + return "UNKNOWN"; + } + } - var purchaseContract = buyer.Marketplace.RequestStorage(purchase); + public class RewardApiCalls + { + private readonly ContainerFileMonitor monitor; - purchaseContract.WaitForStorageContractStarted(); + public RewardApiCalls(ILog log, CoreInterface ci, RunningContainer botContainer) + { + monitor = new ContainerFileMonitor(log, ci, botContainer, "/app/datapath/logs/discordbot.log"); + } - purchaseContract.WaitForStorageContractFinished(); + public void Start(Action onCommand) + { + monitor.Start(line => ParseLine(line, onCommand)); + } + + public void Stop() + { + monitor.Stop(); + } + + private void ParseLine(string line, Action onCommand) + { + try + { + var timestamp = line.Substring(0, 30); + var json = line.Substring(31); + + var cmd = JsonConvert.DeserializeObject(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 seenLines = new List(); + private Task worker = Task.CompletedTask; + private Action 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 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); + } + } + } } } } diff --git a/Tools/BiblioTech/Configuration.cs b/Tools/BiblioTech/Configuration.cs index 296bf42f..6f27fd1f 100644 --- a/Tools/BiblioTech/Configuration.cs +++ b/Tools/BiblioTech/Configuration.cs @@ -35,28 +35,12 @@ namespace BiblioTech [Uniform("mint-tt", "mt", "MINTTT", true, "Amount of TSTWEI minted by the mint command.")] public BigInteger MintTT { get; set; } = 1073741824; - public string EndpointsPath - { - get - { - return Path.Combine(DataPath, "endpoints"); - } - } + [Uniform("no-discord", "nd", "NODISCORD", false, "For debugging: Bypasses all Discord API calls.")] + public int NoDiscord { get; set; } = 0; - public string UserDataPath - { - get - { - return Path.Combine(DataPath, "users"); - } - } - - public string LogPath - { - get - { - return Path.Combine(DataPath, "logs"); - } - } + public string EndpointsPath => Path.Combine(DataPath, "endpoints"); + public string UserDataPath => Path.Combine(DataPath, "users"); + public string LogPath => Path.Combine(DataPath, "logs"); + public bool DebugNoDiscord => NoDiscord == 1; } } diff --git a/Tools/BiblioTech/LoggingRoleDriver.cs b/Tools/BiblioTech/LoggingRoleDriver.cs new file mode 100644 index 00000000..9275a7a5 --- /dev/null +++ b/Tools/BiblioTech/LoggingRoleDriver.cs @@ -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)); + } + } +} diff --git a/Tools/BiblioTech/Program.cs b/Tools/BiblioTech/Program.cs index f289d9b8..6ec7dae9 100644 --- a/Tools/BiblioTech/Program.cs +++ b/Tools/BiblioTech/Program.cs @@ -41,25 +41,15 @@ namespace BiblioTech public async Task MainAsync(string[] args) { Log.Log("Starting Codex Discord Bot..."); - client = new DiscordSocketClient(); - client.Log += ClientLog; - - var notifyCommand = new NotifyCommand(); - var associateCommand = new UserAssociateCommand(notifyCommand); - var sprCommand = new SprCommand(); - var handler = new CommandHandler(client, - new GetBalanceCommand(associateCommand), - new MintCommand(associateCommand), - sprCommand, - associateCommand, - notifyCommand, - new AdminCommand(sprCommand), - new MarketCommand() - ); - - await client.LoginAsync(TokenType.Bot, Config.ApplicationToken); - await client.StartAsync(); - AdminChecker = new AdminChecker(); + if (Config.DebugNoDiscord) + { + Log.Log("Debug option is set. Discord connection disabled!"); + RoleDriver = new LoggingRoleDriver(Log); + } + else + { + await StartDiscordBot(); + } var builder = WebApplication.CreateBuilder(args); builder.WebHost.ConfigureKestrel((context, options) => @@ -75,6 +65,29 @@ namespace BiblioTech await Task.Delay(-1); } + private async Task StartDiscordBot() + { + client = new DiscordSocketClient(); + client.Log += ClientLog; + + var notifyCommand = new NotifyCommand(); + var associateCommand = new UserAssociateCommand(notifyCommand); + var sprCommand = new SprCommand(); + var handler = new CommandHandler(client, + new GetBalanceCommand(associateCommand), + new MintCommand(associateCommand), + sprCommand, + associateCommand, + notifyCommand, + new AdminCommand(sprCommand), + new MarketCommand() + ); + + await client.LoginAsync(TokenType.Bot, Config.ApplicationToken); + await client.StartAsync(); + AdminChecker = new AdminChecker(); + } + private static void PrintHelp() { Log.Log("BiblioTech - Codex Discord Bot"); diff --git a/Tools/TestNetRewarder/BufferLogger.cs b/Tools/TestNetRewarder/BufferLogger.cs new file mode 100644 index 00000000..1f323323 --- /dev/null +++ b/Tools/TestNetRewarder/BufferLogger.cs @@ -0,0 +1,41 @@ +using Logging; + +namespace TestNetRewarder +{ + public class BufferLogger : ILog + { + private readonly List lines = new List(); + + 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; + } + } +} diff --git a/Tools/TestNetRewarder/ChainChangeMux.cs b/Tools/TestNetRewarder/ChainChangeMux.cs new file mode 100644 index 00000000..f70490bf --- /dev/null +++ b/Tools/TestNetRewarder/ChainChangeMux.cs @@ -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); + } + } +} diff --git a/Tools/TestNetRewarder/ChainState.cs b/Tools/TestNetRewarder/ChainState.cs deleted file mode 100644 index c3ce8a24..00000000 --- a/Tools/TestNetRewarder/ChainState.cs +++ /dev/null @@ -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(); - - 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 - { - 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); - } - } - } -} diff --git a/Tools/TestNetRewarder/Checks.cs b/Tools/TestNetRewarder/Checks.cs deleted file mode 100644 index 43102e06..00000000 --- a/Tools/TestNetRewarder/Checks.cs +++ /dev/null @@ -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; - } - } -} diff --git a/Tools/TestNetRewarder/Configuration.cs b/Tools/TestNetRewarder/Configuration.cs index c88a0aa8..5d6ae51d 100644 --- a/Tools/TestNetRewarder/Configuration.cs +++ b/Tools/TestNetRewarder/Configuration.cs @@ -40,5 +40,14 @@ namespace TestNetRewarder return TimeSpan.FromMinutes(IntervalMinutes); } } + + public DateTime HistoryStartUtc + { + get + { + if (CheckHistoryTimestamp == 0) throw new Exception("'check-history' unix timestamp is required. Set it to the start/launch moment of the testnet."); + return DateTimeOffset.FromUnixTimeSeconds(CheckHistoryTimestamp).UtcDateTime; + } + } } } diff --git a/Tools/TestNetRewarder/HistoricState.cs b/Tools/TestNetRewarder/HistoricState.cs index 36d335df..8487aa06 100644 --- a/Tools/TestNetRewarder/HistoricState.cs +++ b/Tools/TestNetRewarder/HistoricState.cs @@ -29,6 +29,20 @@ namespace TestNetRewarder r.State == RequestState.Failed ); } + + public string EntireString() + { + return JsonConvert.SerializeObject(StorageRequests); + } + + public HistoricState() + { + } + + public HistoricState(StorageRequest[] requests) + { + storageRequests.AddRange(requests); + } } public class StorageRequest diff --git a/Tools/TestNetRewarder/MarketBuffer.cs b/Tools/TestNetRewarder/MarketBuffer.cs new file mode 100644 index 00000000..d8ec91d5 --- /dev/null +++ b/Tools/TestNetRewarder/MarketBuffer.cs @@ -0,0 +1,74 @@ +using CodexContractsPlugin.ChainMonitor; +using CodexContractsPlugin.Marketplace; +using DiscordRewards; +using System.Numerics; + +namespace TestNetRewarder +{ + public class MarketBuffer + { + private readonly List requests = new List(); + 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 getValue) + { + return Average(s => + { + var value = getValue(s); + return (int)value; + }); + } + + private float Average(Func 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; + } + } +} diff --git a/Tools/TestNetRewarder/MarketTracker.cs b/Tools/TestNetRewarder/MarketTracker.cs index 17e5e82d..669dd3aa 100644 --- a/Tools/TestNetRewarder/MarketTracker.cs +++ b/Tools/TestNetRewarder/MarketTracker.cs @@ -1,124 +1,73 @@ -using CodexContractsPlugin.Marketplace; +using CodexContractsPlugin.ChainMonitor; using DiscordRewards; +using Logging; using System.Numerics; namespace TestNetRewarder { - public class MarketTracker + public class MarketTracker : IChainStateChangeHandler { - private readonly List buffer = new List(); + private readonly List buffers = new List(); + private readonly ILog log; - public MarketAverage[] ProcessChainState(ChainState chainState) + public MarketTracker(Configuration config, ILog log) { - var intervalCounts = GetInsightCounts(); - if (!intervalCounts.Any()) return Array.Empty(); + var intervals = GetInsightCounts(config); - UpdateBuffer(chainState, intervalCounts.Max()); - var result = intervalCounts - .Select(GenerateMarketAverage) - .Where(a => a != null) - .Cast() - .ToArray(); - - if (!result.Any()) result = Array.Empty(); - return result; - } - - private void UpdateBuffer(ChainState chainState, int maxNumberOfIntervals) - { - buffer.Add(chainState); - while (buffer.Count > maxNumberOfIntervals) + foreach (var i in intervals) { - buffer.RemoveAt(0); + buffers.Add(new MarketBuffer( + config.Interval * i + )); } + + this.log = log; } - private MarketAverage? GenerateMarketAverage(int numberOfIntervals) + public MarketAverage[] GetAverages() { - var states = SelectStates(numberOfIntervals); - return CreateAverage(states); + foreach (var b in buffers) b.Update(); + + return buffers.Select(b => b.GetAverage()).Where(a => a != null).Cast().ToArray(); } - private ChainState[] SelectStates(int numberOfIntervals) + public void OnNewRequest(IChainStateRequest request) { - if (numberOfIntervals < 1) return Array.Empty(); - if (numberOfIntervals > buffer.Count) return Array.Empty(); - return buffer.TakeLast(numberOfIntervals).ToArray(); } - private MarketAverage? CreateAverage(ChainState[] states) + public void OnRequestFinished(IChainStateRequest request) + { + foreach (var b in buffers) b.Add(request); + } + + public void OnRequestFulfilled(IChainStateRequest request) + { + } + + public void OnRequestCancelled(IChainStateRequest request) + { + } + + public void OnSlotFilled(IChainStateRequest request, BigInteger slotIndex) + { + } + + public void OnSlotFreed(IChainStateRequest request, BigInteger slotIndex) + { + } + + private int[] GetInsightCounts(Configuration config) { try { - return new MarketAverage - { - NumberOfFinished = CountNumberOfFinishedRequests(states), - TimeRangeSeconds = GetTotalTimeRange(states), - Price = Average(states, s => s.Request.Ask.Reward), - Duration = Average(states, s => s.Request.Ask.Duration), - Size = Average(states, s => GetTotalSize(s.Request.Ask)), - Collateral = Average(states, s => s.Request.Ask.Collateral), - ProofProbability = Average(states, s => s.Request.Ask.ProofProbability) - }; - } - catch (Exception ex) - { - Program.Log.Error($"Exception in CreateAverage: {ex}"); - return null; - } - } - - private int GetTotalSize(Ask ask) - { - var nSlots = Convert.ToInt32(ask.Slots); - var slotSize = Convert.ToInt32(ask.SlotSize); - return nSlots * slotSize; - } - - private float Average(ChainState[] states, Func getValue) - { - return Average(states, s => Convert.ToInt32(getValue(s))); - } - - private float Average(ChainState[] states, Func getValue) - { - var sum = 0.0f; - var count = 0.0f; - foreach (var state in states) - { - foreach (var finishedRequest in state.FinishedRequests) - { - sum += getValue(finishedRequest); - count++; - } - } - - if (count < 1.0f) return 0.0f; - return sum / count; - } - - private int GetTotalTimeRange(ChainState[] states) - { - return Convert.ToInt32((Program.Config.Interval * states.Length).TotalSeconds); - } - - private int CountNumberOfFinishedRequests(ChainState[] states) - { - return states.Sum(s => s.FinishedRequests.Length); - } - - private int[] GetInsightCounts() - { - try - { - var tokens = Program.Config.MarketInsights.Split(';').ToArray(); + var tokens = config.MarketInsights.Split(';').ToArray(); return tokens.Select(t => Convert.ToInt32(t)).ToArray(); } catch (Exception ex) { - Program.Log.Error($"Exception when parsing MarketInsights config parameters: {ex}"); + log.Error($"Exception when parsing MarketInsights config parameters: {ex}"); } - return Array.Empty(); + return Array.Empty(); } } } diff --git a/Tools/TestNetRewarder/Processor.cs b/Tools/TestNetRewarder/Processor.cs index c18247b4..f43a9241 100644 --- a/Tools/TestNetRewarder/Processor.cs +++ b/Tools/TestNetRewarder/Processor.cs @@ -1,146 +1,67 @@ -using DiscordRewards; -using GethPlugin; +using CodexContractsPlugin; +using CodexContractsPlugin.ChainMonitor; using Logging; -using Newtonsoft.Json; using Utils; namespace TestNetRewarder { - public class Processor + public class Processor : ITimeSegmentHandler { - private static readonly HistoricState historicState = new HistoricState(); - private static readonly RewardRepo rewardRepo = new RewardRepo(); - private static readonly MarketTracker marketTracker = new MarketTracker(); + private readonly RequestBuilder builder; + private readonly RewardChecker rewardChecker; + private readonly MarketTracker marketTracker; + private readonly BufferLogger bufferLogger; + private readonly ChainState chainState; + private readonly BotClient client; private readonly ILog log; - private BlockInterval? lastBlockRange; - public Processor(ILog log) + public Processor(Configuration config, BotClient client, ICodexContracts contracts, ILog log) { + this.client = client; this.log = log; + + builder = new RequestBuilder(); + rewardChecker = new RewardChecker(builder); + marketTracker = new MarketTracker(config, log); + bufferLogger = new BufferLogger(); + + var handler = new ChainChangeMux( + rewardChecker.Handler, + marketTracker + ); + + chainState = new ChainState(new LogSplitter(log, bufferLogger), contracts, handler, config.HistoryStartUtc); } - public async Task ProcessTimeSegment(TimeRange timeRange) + public async Task OnNewSegment(TimeRange timeRange) { - var connector = GethConnector.GethConnector.Initialize(log); - if (connector == null) throw new Exception("Invalid Geth information"); - try { - var blockRange = connector.GethNode.ConvertTimeRangeToBlockRange(timeRange); - if (!IsNewBlockRange(blockRange)) - { - log.Log($"Block range {blockRange} was previously processed. Skipping..."); - return; - } + chainState.Update(timeRange.To); - var chainState = new ChainState(historicState, connector.CodexContracts, blockRange); - await ProcessChainState(chainState); + var averages = marketTracker.GetAverages(); + var lines = RemoveFirstLine(bufferLogger.Get()); + + var request = builder.Build(averages, lines); + if (request.HasAny()) + { + await client.SendRewards(request); + } } catch (Exception ex) { - log.Error("Exception processing time segment: " + ex); + var msg = "Exception processing time segment: " + ex; + log.Error(msg); + bufferLogger.Error(msg); throw; } } - private bool IsNewBlockRange(BlockInterval blockRange) + private string[] RemoveFirstLine(string[] lines) { - if (lastBlockRange == null || - lastBlockRange.From != blockRange.From || - lastBlockRange.To != blockRange.To) - { - lastBlockRange = blockRange; - return true; - } - - return false; - } - - private async Task ProcessChainState(ChainState chainState) - { - var outgoingRewards = new List(); - 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 SendRewardsCommand(List 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 outgoingRewards, RewardConfig reward, ChainState chainState) - { - var winningAddresses = PerformCheck(reward, chainState); - foreach (var win in winningAddresses) - { - log.Log($"Address '{win.Address}' wins '{reward.Message}'"); - } - if (winningAddresses.Any()) - { - outgoingRewards.Add(new RewardUsersCommand - { - RewardId = reward.RoleId, - UserAddresses = winningAddresses.Select(a => a.Address).ToArray() - }); - } - } - - private EthAddress[] PerformCheck(RewardConfig reward, ChainState chainState) - { - var check = GetCheck(reward.CheckConfig); - return check.Check(chainState).Distinct().ToArray(); - } - - private ICheck GetCheck(CheckConfig config) - { - switch (config.Type) - { - case CheckType.FilledSlot: - return new FilledAnySlotCheck(); - case CheckType.FinishedSlot: - return new FinishedSlotCheck(config.MinSlotSize, config.MinDuration); - case CheckType.PostedContract: - return new PostedContractCheck(config.MinNumberOfHosts, config.MinSlotSize, config.MinDuration); - case CheckType.StartedContract: - return new StartedContractCheck(config.MinNumberOfHosts, config.MinSlotSize, config.MinDuration); - } - - throw new Exception("Unknown check type: " + config.Type); + //if (!lines.Any()) return Array.Empty(); + //return lines.Skip(1).ToArray(); + return lines; } } } diff --git a/Tools/TestNetRewarder/Program.cs b/Tools/TestNetRewarder/Program.cs index 0aaa41c7..0056ffec 100644 --- a/Tools/TestNetRewarder/Program.cs +++ b/Tools/TestNetRewarder/Program.cs @@ -6,10 +6,10 @@ namespace TestNetRewarder { public class Program { - public static Configuration Config { get; private set; } = null!; - public static ILog Log { get; private set; } = null!; - public static CancellationToken CancellationToken { get; private set; } - public static BotClient BotClient { get; private set; } = null!; + public static CancellationToken CancellationToken; + private static Configuration Config = null!; + private static ILog Log = null!; + private static BotClient BotClient = null!; private static Processor processor = null!; private static DateTime lastCheck = DateTime.MinValue; @@ -27,8 +27,11 @@ namespace TestNetRewarder new ConsoleLog() ); + var connector = GethConnector.GethConnector.Initialize(Log); + if (connector == null) throw new Exception("Invalid Geth information"); + BotClient = new BotClient(Config, Log); - processor = new Processor(Log); + processor = new Processor(Config, BotClient, connector.CodexContracts, Log); EnsurePath(Config.DataPath); EnsurePath(Config.LogPath); @@ -41,12 +44,12 @@ namespace TestNetRewarder EnsureGethOnline(); Log.Log("Starting TestNet Rewarder..."); - var segmenter = new TimeSegmenter(Log, Config); + var segmenter = new TimeSegmenter(Log, Config, processor); while (!CancellationToken.IsCancellationRequested) { await EnsureBotOnline(); - await segmenter.WaitForNextSegment(processor.ProcessTimeSegment); + await segmenter.ProcessNextSegment(); await Task.Delay(100, CancellationToken); } } diff --git a/Tools/TestNetRewarder/RequestBuilder.cs b/Tools/TestNetRewarder/RequestBuilder.cs new file mode 100644 index 00000000..ea06eaac --- /dev/null +++ b/Tools/TestNetRewarder/RequestBuilder.cs @@ -0,0 +1,40 @@ +using DiscordRewards; +using GethPlugin; + +namespace TestNetRewarder +{ + public class RequestBuilder : IRewardGiver + { + private readonly Dictionary> rewards = new Dictionary>(); + + public void Give(RewardConfig reward, EthAddress receiver) + { + if (rewards.ContainsKey(reward.RoleId)) + { + rewards[reward.RoleId].Add(receiver); + } + else + { + rewards.Add(reward.RoleId, new List { 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; + } + } +} diff --git a/Tools/TestNetRewarder/RewardCheck.cs b/Tools/TestNetRewarder/RewardCheck.cs new file mode 100644 index 00000000..9be87d22 --- /dev/null +++ b/Tools/TestNetRewarder/RewardCheck.cs @@ -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; + } + } +} diff --git a/Tools/TestNetRewarder/RewardChecker.cs b/Tools/TestNetRewarder/RewardChecker.cs new file mode 100644 index 00000000..0d22baf4 --- /dev/null +++ b/Tools/TestNetRewarder/RewardChecker.cs @@ -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; } + } +} diff --git a/Tools/TestNetRewarder/TimeSegmenter.cs b/Tools/TestNetRewarder/TimeSegmenter.cs index 44062f8f..24899ed1 100644 --- a/Tools/TestNetRewarder/TimeSegmenter.cs +++ b/Tools/TestNetRewarder/TimeSegmenter.cs @@ -3,51 +3,60 @@ using Utils; namespace TestNetRewarder { + public interface ITimeSegmentHandler + { + Task OnNewSegment(TimeRange timeRange); + } + public class TimeSegmenter { private readonly ILog log; + private readonly ITimeSegmentHandler handler; private readonly TimeSpan segmentSize; - private DateTime start; + private DateTime latest; - public TimeSegmenter(ILog log, Configuration configuration) + public TimeSegmenter(ILog log, Configuration configuration, ITimeSegmentHandler handler) { this.log = log; - + this.handler = handler; if (configuration.IntervalMinutes < 0) configuration.IntervalMinutes = 1; - if (configuration.CheckHistoryTimestamp == 0) throw new Exception("'check-history' unix timestamp is required. Set it to the start/launch moment of the testnet."); segmentSize = configuration.Interval; - start = DateTimeOffset.FromUnixTimeSeconds(configuration.CheckHistoryTimestamp).UtcDateTime; + latest = configuration.HistoryStartUtc; - log.Log("Starting time segments at " + start); + log.Log("Starting time segments at " + latest); log.Log("Segment size: " + Time.FormatDuration(segmentSize)); } - public async Task WaitForNextSegment(Func onSegment) + public async Task ProcessNextSegment() { - var now = DateTime.UtcNow; - var end = start + segmentSize; - var waited = false; - if (end > now) - { - // Wait for the entire time segment to be in the past. - var delay = end - now; - waited = true; - log.Log($"Waiting till time segment is in the past... {Time.FormatDuration(delay)}"); - await Task.Delay(delay, Program.CancellationToken); - } - await Task.Delay(TimeSpan.FromSeconds(3), Program.CancellationToken); + var end = latest + segmentSize; + var waited = await WaitUntilTimeSegmentInPast(end); if (Program.CancellationToken.IsCancellationRequested) return; var postfix = "(Catching up...)"; if (waited) postfix = "(Real-time)"; + log.Log($"Time segment [{latest} to {end}] {postfix}"); + + var range = new TimeRange(latest, end); + latest = end; - log.Log($"Time segment [{start} to {end}] {postfix}"); - var range = new TimeRange(start, end); - start = end; + await handler.OnNewSegment(range); + } - await onSegment(range); + private async Task 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; } } }