Merge branch 'master' into feature/require-profiler-branch

# Conflicts:
#	ProjectPlugins/CodexPlugin/CodexDockerImage.cs
This commit is contained in:
ThatBen 2025-07-30 14:54:05 +02:00
commit 0c05950b8b
No known key found for this signature in database
GPG Key ID: E020A7DDCD52E1AB
83 changed files with 1247 additions and 424 deletions

View File

@ -95,7 +95,10 @@ namespace ArgsUniform
}
else
{
if (uniformProperty.PropertyType == typeof(string) || uniformProperty.PropertyType == typeof(int))
if (
uniformProperty.PropertyType == typeof(string) ||
uniformProperty.PropertyType == typeof(int) ||
uniformProperty.PropertyType == typeof(decimal))
{
uniformProperty.SetValue(result, Convert.ChangeType(value, uniformProperty.PropertyType));
return true;

View File

@ -7,7 +7,6 @@ namespace Core
{
public interface IPluginTools : IWorkflowTool, ILogTool, IHttpFactory, IFileTool
{
IWebCallTimeSet WebCallTimeSet { get; }
IK8sTimeSet K8STimeSet { get; }
/// <summary>

View File

@ -724,9 +724,11 @@ namespace KubernetesWorkflow
private V1Pod GetPodForDeployment(RunningDeployment deployment)
{
return Time.Retry(() => GetPodForDeplomentInternal(deployment),
// We will wait up to 1 minute, k8s might be moving pods around.
maxTimeout: TimeSpan.FromMinutes(1),
retryTime: TimeSpan.FromSeconds(10),
// K8s might be moving pods around. If it's scaling the cluster
// to handle the increased load, it might take a while before the new
// VMs are up and ready. So we use a generous timeout.
maxTimeout: TimeSpan.FromMinutes(15.0),
retryTime: TimeSpan.FromSeconds(30.0),
description: "Find pod by label for deployment.");
}

View File

@ -22,7 +22,9 @@
.Replace("\\", "-")
.Replace("[", "-")
.Replace("]", "-")
.Replace(",", "-");
.Replace(",", "-")
.Replace("(", "-")
.Replace(")", "-");
if (result.Length > maxLength) result = result.Substring(0, maxLength);
result = result.Trim('-');

View File

@ -61,8 +61,7 @@ namespace KubernetesWorkflow
var startResult = controller.BringOnline(recipes, location);
var containers = CreateContainers(startResult, recipes, startupConfig);
var info = GetPodInfo(startResult.Deployment);
var rc = new RunningPod(Guid.NewGuid().ToString(), info, startupConfig, startResult, containers);
var rc = new RunningPod(Guid.NewGuid().ToString(), startupConfig, startResult, containers);
cluster.Configuration.Hooks.OnContainersStarted(rc);
if (startResult.ExternalService != null)
@ -73,7 +72,7 @@ namespace KubernetesWorkflow
});
}
public void WaitUntilOnline(RunningPod rc)
public PodInfo WaitUntilOnline(RunningPod rc)
{
K8s(controller =>
{
@ -82,6 +81,8 @@ namespace KubernetesWorkflow
controller.WaitUntilOnline(c);
}
});
return GetPodInfo(rc.StartResult.Deployment);
}
public PodInfo GetPodInfo(RunningDeployment deployment)

View File

@ -13,7 +13,8 @@
public RunningPod WaitForOnline()
{
workflow.WaitUntilOnline(runningPod);
var podInfo = workflow.WaitUntilOnline(runningPod);
runningPod.Initialize(podInfo);
return runningPod;
}
}

View File

@ -27,7 +27,7 @@ namespace KubernetesWorkflow.Types
public Address GetAddress(string portTag)
{
var addresses = Addresses.Where(a => a.PortTag == portTag).ToArray();
if (!addresses.Any()) throw new Exception("No addresses found for portTag: " + portTag);
if (addresses.Length == 0) throw new Exception("No addresses found for portTag: " + portTag);
var select = SelectAddress(addresses);
return select.Address;

View File

@ -4,10 +4,10 @@ namespace KubernetesWorkflow.Types
{
public class RunningPod
{
public RunningPod(string id, PodInfo podInfo, StartupConfig startupConfig, StartResult startResult, RunningContainer[] containers)
public RunningPod(string id, StartupConfig startupConfig, StartResult startResult, RunningContainer[] containers)
{
Id = id;
PodInfo = podInfo;
PodInfo = null!;
StartupConfig = startupConfig;
StartResult = startResult;
Containers = containers;
@ -16,7 +16,7 @@ namespace KubernetesWorkflow.Types
}
public string Id { get; }
public PodInfo PodInfo { get; }
public PodInfo PodInfo { get; private set; }
public StartupConfig StartupConfig { get; }
public StartResult StartResult { get; }
public RunningContainer[] Containers { get; }
@ -30,6 +30,11 @@ namespace KubernetesWorkflow.Types
[JsonIgnore]
public bool IsStopped { get; internal set; }
public void Initialize(PodInfo podInfo)
{
PodInfo = podInfo;
}
public string Describe()
{
return string.Join(",", Containers.Select(c => c.Name));

View File

@ -1,4 +1,5 @@
using BlockchainUtils;
using System.Numerics;
using BlockchainUtils;
using Logging;
using Nethereum.ABI.FunctionEncoding.Attributes;
using Nethereum.Contracts;
@ -22,25 +23,26 @@ namespace NethereumWorkflow
this.blockCache = blockCache;
}
public string SendEth(string toAddress, decimal ethAmount)
public string SendEth(string toAddress, BigInteger ethAmount)
{
log.Debug();
var receipt = Time.Wait(web3.Eth.GetEtherTransferService().TransferEtherAndWaitForReceiptAsync(toAddress, ethAmount));
var receipt = Time.Wait(web3.Eth.GetEtherTransferService().TransferEtherAndWaitForReceiptAsync(toAddress, ((decimal)ethAmount)));
if (!receipt.Succeeded()) throw new Exception("Unable to send Eth");
return receipt.TransactionHash;
}
public decimal GetEthBalance()
public BigInteger GetEthBalance()
{
log.Debug();
return GetEthBalance(web3.TransactionManager.Account.Address);
}
public decimal GetEthBalance(string address)
public BigInteger GetEthBalance(string address)
{
log.Debug();
var balance = Time.Wait(web3.Eth.GetBalance.SendRequestAsync(address));
return Web3.Convert.FromWei(balance.Value);
return balance.Value;
}
public TResult Call<TFunction, TResult>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new()
@ -57,6 +59,13 @@ namespace NethereumWorkflow
return Time.Wait(handler.QueryAsync<TResult>(contractAddress, function, new BlockParameter(blockNumber)));
}
public void Call<TFunction>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new()
{
log.Debug(typeof(TFunction).ToString());
var handler = web3.Eth.GetContractQueryHandler<TFunction>();
Time.Wait(handler.QueryRawAsync(contractAddress, function));
}
public void Call<TFunction>(string contractAddress, TFunction function, ulong blockNumber) where TFunction : FunctionMessage, new()
{
log.Debug(typeof(TFunction).ToString());
@ -110,15 +119,29 @@ namespace NethereumWorkflow
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new()
{
var eventHandler = web3.Eth.GetEvent<TEvent>(address);
var logs = new List<FilterLog>();
var p = web3.Processing.Logs.CreateProcessor(
action: logs.Add,
minimumBlockConfirmations: 1,
criteria: l => l.IsLogForEvent<TEvent>()
);
var from = new BlockParameter(fromBlockNumber);
var to = new BlockParameter(toBlockNumber);
var blockFilter = Time.Wait(eventHandler.CreateFilterBlockRangeAsync(from, to));
return Time.Wait(eventHandler.GetAllChangesAsync(blockFilter));
var ct = new CancellationTokenSource().Token;
Time.Wait(p.ExecuteAsync(toBlockNumber: to.BlockNumber, cancellationToken: ct, startAtBlockNumberIfNotProcessed: from.BlockNumber));
return logs
.Where(l => l.IsLogForEvent<TEvent>())
.Select(l => l.DecodeEvent<TEvent>())
.ToList();
}
public BlockInterval ConvertTimeRangeToBlockRange(TimeRange timeRange)
{
if (timeRange.To - timeRange.From < TimeSpan.FromSeconds(1.0))
throw new Exception(nameof(ConvertTimeRangeToBlockRange) + ": Time range too small.");
var wrapper = new Web3Wrapper(web3, log);
var blockTimeFinder = new BlockTimeFinder(blockCache, wrapper, log);

View File

@ -1,15 +1,17 @@
namespace Utils
using System.Numerics;
namespace Utils
{
public class Ether : IComparable<Ether>
{
public Ether(decimal wei)
public Ether(BigInteger wei)
{
Wei = wei;
Eth = wei / TokensIntExtensions.WeiPerEth;
}
public decimal Wei { get; }
public decimal Eth { get; }
public BigInteger Wei { get; }
public BigInteger Eth { get; }
public int CompareTo(Ether? other)
{
@ -75,7 +77,7 @@
public static class TokensIntExtensions
{
public const decimal WeiPerEth = 1000000000000000000;
public static readonly BigInteger WeiPerEth = new BigInteger(1000000000000000000);
public static Ether Eth(this int i)
{
@ -89,10 +91,22 @@
public static Ether Eth(this decimal i)
{
return new Ether(i * WeiPerEth);
var a = new BigInteger(i);
return new Ether(a * WeiPerEth);
}
public static Ether Wei(this decimal i)
{
var a = new BigInteger(i);
return new Ether(a);
}
public static Ether Eth(this BigInteger i)
{
return new Ether(i * WeiPerEth);
}
public static Ether Wei(this BigInteger i)
{
return new Ether(i);
}

View File

@ -7,12 +7,14 @@ namespace WebUtils
IHttp CreateHttp(string id, Action<HttpClient> onClientCreated);
IHttp CreateHttp(string id, Action<HttpClient> onClientCreated, IWebCallTimeSet timeSet);
IHttp CreateHttp(string id);
IWebCallTimeSet WebCallTimeSet { get; }
}
public class HttpFactory : IHttpFactory
{
private readonly ILog log;
private readonly IWebCallTimeSet defaultTimeSet;
private readonly IWebCallTimeSet timeSet;
private readonly Action<HttpClient> factoryOnClientCreated;
public HttpFactory(ILog log)
@ -33,13 +35,15 @@ namespace WebUtils
public HttpFactory(ILog log, IWebCallTimeSet defaultTimeSet, Action<HttpClient> onClientCreated)
{
this.log = log;
this.defaultTimeSet = defaultTimeSet;
this.factoryOnClientCreated = onClientCreated;
timeSet = defaultTimeSet;
factoryOnClientCreated = onClientCreated;
}
public IWebCallTimeSet WebCallTimeSet => timeSet;
public IHttp CreateHttp(string id, Action<HttpClient> onClientCreated)
{
return CreateHttp(id, onClientCreated, defaultTimeSet);
return CreateHttp(id, onClientCreated, timeSet);
}
public IHttp CreateHttp(string id, Action<HttpClient> onClientCreated, IWebCallTimeSet ts)
@ -53,7 +57,7 @@ namespace WebUtils
public IHttp CreateHttp(string id)
{
return new Http(id, log, defaultTimeSet, factoryOnClientCreated);
return new Http(id, log, timeSet, factoryOnClientCreated);
}
private static void DoNothing(HttpClient client)

View File

@ -23,12 +23,12 @@
{
public TimeSpan HttpCallTimeout()
{
return TimeSpan.FromMinutes(2);
return TimeSpan.FromMinutes(5);
}
public TimeSpan HttpRetryTimeout()
{
return TimeSpan.FromMinutes(5);
return TimeSpan.FromMinutes(20);
}
public TimeSpan HttpCallRetryDelay()

View File

@ -1,4 +1,5 @@
using CodexOpenApi;
using System.Threading;
using CodexOpenApi;
using Logging;
using Newtonsoft.Json;
using Utils;
@ -119,7 +120,7 @@ namespace CodexClient
public Stream DownloadFile(string contentId)
{
var fileResponse = OnCodex(api => api.DownloadNetworkStreamAsync(contentId));
var fileResponse = OnCodexNoRetry(api => api.DownloadNetworkStreamAsync(contentId));
if (fileResponse.StatusCode != 200) throw new Exception("Download failed with StatusCode: " + fileResponse.StatusCode);
return fileResponse.Stream;
}
@ -212,15 +213,22 @@ namespace CodexClient
processControl.DeleteDataDirFolder();
}
private T OnCodex<T>(Func<CodexApiClient, Task<T>> action)
private T OnCodexNoRetry<T>(Func<CodexApiClient, Task<T>> action)
{
var result = httpFactory.CreateHttp(GetHttpId(), h => CheckContainerCrashed()).OnClient(client => CallCodex(client, action));
var timeSet = httpFactory.WebCallTimeSet;
var noRetry = new Retry(nameof(OnCodexNoRetry),
maxTimeout: TimeSpan.FromSeconds(1.0),
sleepAfterFail: TimeSpan.FromSeconds(2.0),
onFail: f => { },
failFast: true);
var result = httpFactory.CreateHttp(GetHttpId(), h => CheckContainerCrashed()).OnClient(client => CallCodex(client, action), noRetry);
return result;
}
private T OnCodex<T>(Func<CodexApiClient, Task<T>> action, Retry retry)
private T OnCodex<T>(Func<CodexApiClient, Task<T>> action)
{
var result = httpFactory.CreateHttp(GetHttpId(), h => CheckContainerCrashed()).OnClient(client => CallCodex(client, action), retry);
var result = httpFactory.CreateHttp(GetHttpId(), h => CheckContainerCrashed()).OnClient(client => CallCodex(client, action));
return result;
}

View File

@ -17,6 +17,7 @@ namespace CodexClient
ContentId UploadFile(TrackedFile file);
ContentId UploadFile(TrackedFile file, string contentType, string contentDisposition);
TrackedFile? DownloadContent(ContentId contentId, string fileLabel = "");
TrackedFile? DownloadContent(ContentId contentId, TimeSpan timeout, string fileLabel = "");
LocalDataset DownloadStreamless(ContentId cid);
/// <summary>
/// TODO: This will monitor the quota-used of the node until 'size' bytes are added. That's a very bad way
@ -77,7 +78,16 @@ namespace CodexClient
public void Initialize()
{
InitializePeerNodeId();
// This is the moment we first connect to a codex node. Sometimes, Kubernetes takes a while to spin up the
// container. So we'll adding a custom, generous retry here.
var kubeSpinupRetry = new Retry("CodexNode_Initialize",
maxTimeout: TimeSpan.FromMinutes(10.0),
sleepAfterFail: TimeSpan.FromSeconds(10.0),
onFail: f => { },
failFast: false);
kubeSpinupRetry.Run(InitializePeerNodeId);
InitializeLogReplacements();
hooks.OnNodeStarted(this, peerId, nodeId);
@ -185,13 +195,18 @@ namespace CodexClient
}
public TrackedFile? DownloadContent(ContentId contentId, string fileLabel = "")
{
return DownloadContent(contentId, TimeSpan.FromMinutes(10.0), fileLabel);
}
public TrackedFile? DownloadContent(ContentId contentId, TimeSpan timeout, string fileLabel = "")
{
var file = fileManager.CreateEmptyFile(fileLabel);
hooks.OnFileDownloading(contentId);
Log($"Downloading '{contentId}'...");
var logMessage = $"Downloaded '{contentId}' to '{file.Filename}'";
var measurement = Stopwatch.Measure(log, logMessage, () => DownloadToFile(contentId.Id, file));
var measurement = Stopwatch.Measure(log, logMessage, () => DownloadToFile(contentId.Id, file, timeout));
var size = file.GetFilesize();
transferSpeeds.AddDownloadSample(size, measurement);
@ -299,7 +314,7 @@ namespace CodexClient
private void InitializePeerNodeId()
{
var debugInfo = Time.Retry(codexAccess.GetDebugInfo, "ensure online");
var debugInfo = codexAccess.GetDebugInfo();
if (!debugInfo.Version.IsValid())
{
throw new Exception($"Invalid version information received from Codex node {GetName()}: {debugInfo.Version}");
@ -339,13 +354,14 @@ namespace CodexClient
.ToArray();
}
private void DownloadToFile(string contentId, TrackedFile file)
private void DownloadToFile(string contentId, TrackedFile file, TimeSpan timeout)
{
using var fileStream = File.OpenWrite(file.Filename);
var timeout = TimeSpan.FromMinutes(2.0); // todo: make this user-controllable.
try
{
// Type of stream generated by openAPI client does not support timeouts.
// So we use a task and cancellation token to track our timeout manually.
var start = DateTime.UtcNow;
var cts = new CancellationTokenSource();
var downloadTask = Task.Run(() =>
@ -373,7 +389,7 @@ namespace CodexClient
public void WaitUntilQuotaUsedIncreased(CodexSpace startSpace, ByteSize expectedIncreaseOfQuotaUsed)
{
WaitUntilQuotaUsedIncreased(startSpace, expectedIncreaseOfQuotaUsed, TimeSpan.FromMinutes(2));
WaitUntilQuotaUsedIncreased(startSpace, expectedIncreaseOfQuotaUsed, TimeSpan.FromMinutes(30));
}
public void WaitUntilQuotaUsedIncreased(

View File

@ -66,7 +66,7 @@ namespace CodexClient
public class Manifest
{
public string RootHash { get; set; } = string.Empty;
public ByteSize OriginalBytes { get; set; } = ByteSize.Zero;
public ByteSize DatasetSize { get; set; } = ByteSize.Zero;
public ByteSize BlockSize { get; set; } = ByteSize.Zero;
public bool Protected { get; set; }
}

View File

@ -208,7 +208,7 @@ namespace CodexClient
return new Manifest
{
BlockSize = new ByteSize(Convert.ToInt64(manifest.BlockSize)),
OriginalBytes = new ByteSize(Convert.ToInt64(manifest.DatasetSize)),
DatasetSize = new ByteSize(Convert.ToInt64(manifest.DatasetSize)),
RootHash = manifest.TreeCid,
Protected = manifest.Protected
};

View File

@ -12,6 +12,7 @@ namespace CodexClient
ContentId ContentId { get; }
StoragePurchase? GetStatus();
void WaitForStorageContractSubmitted();
void WaitForStorageContractExpired();
void WaitForStorageContractStarted();
void WaitForStorageContractFinished();
void WaitForContractFailed(IMarketplaceConfigInput config);
@ -81,6 +82,12 @@ namespace CodexClient
AssertDuration(PendingToSubmitted, timeout, nameof(PendingToSubmitted));
}
public void WaitForStorageContractExpired()
{
var timeout = Purchase.Expiry + gracePeriod + gracePeriod;
WaitForStorageContractState(timeout, StoragePurchaseState.Cancelled);
}
public void WaitForStorageContractStarted()
{
var timeout = Purchase.Expiry + gracePeriod;
@ -124,7 +131,7 @@ namespace CodexClient
);
}
WaitForStorageContractState(timeout, StoragePurchaseState.Failed);
WaitForStorageContractState(timeout, StoragePurchaseState.Errored);
}
private TimeSpan TimeNeededToFailEnoughProofsToFreeASlot(IMarketplaceConfigInput config)
@ -157,7 +164,7 @@ namespace CodexClient
hooks.OnStorageContractUpdated(purchaseStatus);
}
if (lastState == StoragePurchaseState.Errored)
if (desiredState != StoragePurchaseState.Errored && lastState == StoragePurchaseState.Errored)
{
FrameworkAssert.Fail("Contract errored: " + statusJson);
}

View File

@ -7,7 +7,7 @@ namespace CodexContractsPlugin.ChainMonitor
{
private ChainEvents(
BlockInterval blockInterval,
Request[] requests,
StorageRequestedEventDTO[] requests,
RequestFulfilledEventDTO[] fulfilled,
RequestCancelledEventDTO[] cancelled,
RequestFailedEventDTO[] failed,
@ -30,7 +30,7 @@ namespace CodexContractsPlugin.ChainMonitor
}
public BlockInterval BlockInterval { get; }
public Request[] Requests { get; }
public StorageRequestedEventDTO[] Requests { get; }
public RequestFulfilledEventDTO[] Fulfilled { get; }
public RequestCancelledEventDTO[] Cancelled { get; }
public RequestFailedEventDTO[] Failed { get; }
@ -54,7 +54,7 @@ namespace CodexContractsPlugin.ChainMonitor
{
return new ChainEvents(
events.BlockInterval,
events.GetStorageRequests(),
events.GetStorageRequestedEvents(),
events.GetRequestFulfilledEvents(),
events.GetRequestCancelledEvents(),
events.GetRequestFailedEvents(),

View File

@ -1,6 +1,7 @@
using BlockchainUtils;
using CodexContractsPlugin.Marketplace;
using Logging;
using Nethereum.Hex.HexConvertors.Extensions;
using System.Numerics;
using Utils;
@ -13,7 +14,7 @@ namespace CodexContractsPlugin.ChainMonitor
void OnRequestFulfilled(RequestEvent requestEvent);
void OnRequestCancelled(RequestEvent requestEvent);
void OnRequestFailed(RequestEvent requestEvent);
void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex);
void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex, bool isRepair);
void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex);
void OnSlotReservationsFull(RequestEvent requestEvent, BigInteger slotIndex);
void OnProofSubmitted(BlockTimeEntry block, string id);
@ -115,15 +116,22 @@ namespace CodexContractsPlugin.ChainMonitor
ApplyTimeImplicitEvents(blockNumber, eventsUtc);
}
private void ApplyEvent(Request request)
private void ApplyEvent(StorageRequestedEventDTO @event)
{
if (requests.Any(r => Equal(r.Request.RequestId, request.RequestId)))
throw new Exception("Received NewRequest event for id that already exists.");
if (requests.Any(r => Equal(r.RequestId, @event.RequestId)))
{
var r = FindRequest(@event);
if (r == null) throw new Exception("ChainState is inconsistent. Received already-known requestId that's not known.");
if (@event.Block.BlockNumber != @event.Block.BlockNumber) throw new Exception("Same request found in different blocks.");
log.Log("Received the same request-creation event multiple times.");
return;
}
var newRequest = new ChainStateRequest(log, request, RequestState.New);
var request = contracts.GetRequest(@event.RequestId);
var newRequest = new ChainStateRequest(log, @event.RequestId, @event.Block, request, RequestState.New);
requests.Add(newRequest);
handler.OnNewRequest(new RequestEvent(request.Block, newRequest));
handler.OnNewRequest(new RequestEvent(@event.Block, newRequest));
}
private void ApplyEvent(RequestFulfilledEventDTO @event)
@ -154,16 +162,18 @@ namespace CodexContractsPlugin.ChainMonitor
{
var r = FindRequest(@event);
if (r == null) return;
r.Hosts.Add(@event.Host, (int)@event.SlotIndex);
var slotIndex = (int)@event.SlotIndex;
var isRepair = !r.Hosts.IsFilled(slotIndex) && r.Hosts.WasPreviouslyFilled(slotIndex);
r.Hosts.HostFillsSlot(@event.Host, slotIndex);
r.Log($"[{@event.Block.BlockNumber}] SlotFilled (host:'{@event.Host}', slotIndex:{@event.SlotIndex})");
handler.OnSlotFilled(new RequestEvent(@event.Block, r), @event.Host, @event.SlotIndex);
handler.OnSlotFilled(new RequestEvent(@event.Block, r), @event.Host, @event.SlotIndex, isRepair);
}
private void ApplyEvent(SlotFreedEventDTO @event)
{
var r = FindRequest(@event);
if (r == null) return;
r.Hosts.RemoveHost((int)@event.SlotIndex);
r.Hosts.SlotFreed((int)@event.SlotIndex);
r.Log($"[{@event.Block.BlockNumber}] SlotFreed (slotIndex:{@event.SlotIndex})");
handler.OnSlotFreed(new RequestEvent(@event.Block, r), @event.SlotIndex);
}
@ -196,22 +206,22 @@ namespace CodexContractsPlugin.ChainMonitor
}
}
private ChainStateRequest? FindRequest(IHasRequestId request)
private ChainStateRequest? FindRequest(IHasBlockAndRequestId hasBoth)
{
var r = requests.SingleOrDefault(r => Equal(r.Request.RequestId, request.RequestId));
var r = requests.SingleOrDefault(r => Equal(r.RequestId, hasBoth.RequestId));
if (r != null) return r;
try
{
var req = contracts.GetRequest(request.RequestId);
var state = contracts.GetRequestState(req);
var newRequest = new ChainStateRequest(log, req, state);
var req = contracts.GetRequest(hasBoth.RequestId);
var state = contracts.GetRequestState(hasBoth.RequestId);
var newRequest = new ChainStateRequest(log, hasBoth.RequestId, hasBoth.Block, req, state);
requests.Add(newRequest);
return newRequest;
}
catch (Exception ex)
{
var msg = "Failed to get request from chain: " + ex;
var msg = $"Failed to get request with id '{hasBoth.RequestId.ToHex()}' from chain: {ex}";
log.Error(msg);
handler.OnError(msg);
return null;

View File

@ -38,9 +38,9 @@ namespace CodexContractsPlugin.ChainMonitor
foreach (var handler in Handlers) handler.OnRequestFulfilled(requestEvent);
}
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex)
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex, bool isRepair)
{
foreach (var handler in Handlers) handler.OnSlotFilled(requestEvent, host, slotIndex);
foreach (var handler in Handlers) handler.OnSlotFilled(requestEvent, host, slotIndex, isRepair);
}
public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex)

View File

@ -1,11 +1,15 @@
using CodexContractsPlugin.Marketplace;
using BlockchainUtils;
using CodexContractsPlugin.Marketplace;
using Logging;
using Nethereum.Hex.HexConvertors.Extensions;
using Utils;
namespace CodexContractsPlugin.ChainMonitor
{
public interface IChainStateRequest
{
byte[] RequestId { get; }
public BlockTimeEntry Block { get; }
Request Request { get; }
RequestState State { get; }
DateTime ExpiryUtc { get; }
@ -18,21 +22,27 @@ namespace CodexContractsPlugin.ChainMonitor
{
private readonly ILog log;
public ChainStateRequest(ILog log, Request request, RequestState state)
public ChainStateRequest(ILog log, byte[] requestId, BlockTimeEntry block, Request request, RequestState state)
{
if (requestId == null || requestId.Length != 32) throw new ArgumentException(nameof(requestId));
this.log = log;
RequestId = requestId;
Block = block;
Request = request;
State = state;
ExpiryUtc = request.Block.Utc + TimeSpan.FromSeconds((double)request.Expiry);
FinishedUtc = request.Block.Utc + TimeSpan.FromSeconds((double)request.Ask.Duration);
ExpiryUtc = Block.Utc + TimeSpan.FromSeconds((double)request.Expiry);
FinishedUtc = Block.Utc + TimeSpan.FromSeconds((double)request.Ask.Duration);
Log($"[{request.Block.BlockNumber}] Created as {State}.");
Log($"[{Block.BlockNumber}] Created as {State}.");
Client = new EthAddress(request.Client);
Hosts = new RequestHosts();
}
public byte[] RequestId { get; }
public BlockTimeEntry Block { get; }
public Request Request { get; }
public RequestState State { get; private set; }
public DateTime ExpiryUtc { get; }
@ -48,20 +58,32 @@ namespace CodexContractsPlugin.ChainMonitor
public void Log(string msg)
{
log.Log($"Request '{Request.Id}': {msg}");
log.Log($"Request '{RequestId.ToHex()}': {msg}");
}
}
public class RequestHosts
{
private readonly Dictionary<int, EthAddress> hosts = new Dictionary<int, EthAddress>();
private readonly List<int> filled = new List<int>();
public void Add(EthAddress host, int index)
public void HostFillsSlot(EthAddress host, int index)
{
hosts.Add(index, host);
filled.Add(index);
}
public bool IsFilled(int index)
{
return hosts.ContainsKey(index);
}
public bool WasPreviouslyFilled(int index)
{
return filled.Contains(index);
}
public void RemoveHost(int index)
public void SlotFreed(int index)
{
hosts.Remove(index);
}

View File

@ -26,7 +26,7 @@ namespace CodexContractsPlugin.ChainMonitor
{
}
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex)
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex, bool isRepair)
{
}
@ -46,4 +46,48 @@ namespace CodexContractsPlugin.ChainMonitor
{
}
}
public class DoNothingThrowingChainEventHandler : IChainStateChangeHandler
{
public void OnNewRequest(RequestEvent requestEvent)
{
}
public void OnRequestCancelled(RequestEvent requestEvent)
{
}
public void OnRequestFailed(RequestEvent requestEvent)
{
}
public void OnRequestFinished(RequestEvent requestEvent)
{
}
public void OnRequestFulfilled(RequestEvent requestEvent)
{
}
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex, bool isRepair)
{
}
public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex)
{
}
public void OnSlotReservationsFull(RequestEvent requestEvent, BigInteger slotIndex)
{
}
public void OnError(string msg)
{
throw new Exception(msg);
}
public void OnProofSubmitted(BlockTimeEntry block, string id)
{
}
}
}

View File

@ -1,4 +1,5 @@
using Logging;
using Nethereum.Hex.HexConvertors.Extensions;
using Utils;
namespace CodexContractsPlugin.ChainMonitor
@ -46,7 +47,7 @@ namespace CodexContractsPlugin.ChainMonitor
{
for (ulong slotIndex = 0; slotIndex < request.Request.Ask.Slots; slotIndex++)
{
var state = contracts.GetProofState(request.Request, slotIndex, lastBlockInPeriod, periodNumber);
var state = contracts.GetProofState(request.RequestId, slotIndex, lastBlockInPeriod, periodNumber);
total++;
if (state.Required)
@ -116,7 +117,7 @@ namespace CodexContractsPlugin.ChainMonitor
var missed = "None";
if (MissedProofs.Length > 0)
{
missed = string.Join("+", MissedProofs.Select(p => $"{p.FormatHost()} missed {p.Request.Request.Id} slot {p.SlotIndex}"));
missed = string.Join("+", MissedProofs.Select(p => $"{p.FormatHost()} missed {p.Request.RequestId.ToHex()} slot {p.SlotIndex}"));
}
return $"Period:{PeriodNumber}=[Slots:{TotalNumSlots},ProofsRequired:{TotalProofsRequired},ProofsMissed:{missed}]";
}

View File

@ -20,15 +20,18 @@ namespace CodexContractsPlugin
string MintTestTokens(EthAddress ethAddress, TestToken testTokens);
TestToken GetTestTokenBalance(IHasEthAddress owner);
TestToken GetTestTokenBalance(EthAddress ethAddress);
void TransferTestTokens(EthAddress to, TestToken amount);
ICodexContractsEvents GetEvents(TimeRange timeRange);
ICodexContractsEvents GetEvents(BlockInterval blockInterval);
EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex);
RequestState GetRequestState(Request request);
EthAddress? GetSlotHost(byte[] requestId, decimal slotIndex);
RequestState GetRequestState(byte[] requestId);
Request GetRequest(byte[] requestId);
ulong GetPeriodNumber(DateTime utc);
void WaitUntilNextPeriod();
ProofState GetProofState(Request storageRequest, decimal slotIndex, ulong blockNumber, ulong period);
ProofState GetProofState(byte[] requestId, decimal slotIndex, ulong blockNumber, ulong period);
ICodexContracts WithDifferentGeth(IGethNode node);
}
public class ProofState
@ -93,6 +96,11 @@ namespace CodexContractsPlugin
return balance.TstWei();
}
public void TransferTestTokens(EthAddress to, TestToken amount)
{
StartInteraction().TransferTestTokens(Deployment.TokenAddress, to.Address, amount.TstWei);
}
public ICodexContractsEvents GetEvents(TimeRange timeRange)
{
return GetEvents(gethNode.ConvertTimeRangeToBlockRange(timeRange));
@ -103,9 +111,9 @@ namespace CodexContractsPlugin
return new CodexContractsEvents(log, gethNode, Deployment, blockInterval);
}
public EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex)
public EthAddress? GetSlotHost(byte[] requestId, decimal slotIndex)
{
var slotId = GetSlotId(storageRequest, slotIndex);
var slotId = GetSlotId(requestId, slotIndex);
var func = new GetHostFunction
{
SlotId = slotId
@ -115,17 +123,22 @@ namespace CodexContractsPlugin
return new EthAddress(address);
}
public RequestState GetRequestState(Request request)
public RequestState GetRequestState(byte[] requestId)
{
if (requestId == null) throw new ArgumentNullException(nameof(requestId));
if (requestId.Length != 32) throw new InvalidDataException(nameof(requestId) + $"{nameof(requestId)} length should be 32 bytes, but was: {requestId.Length}" + requestId.Length);
var func = new RequestStateFunction
{
RequestId = request.RequestId
RequestId = requestId
};
return gethNode.Call<RequestStateFunction, RequestState>(Deployment.MarketplaceAddress, func);
}
public Request GetRequest(byte[] requestId)
{
if (requestId == null) throw new ArgumentNullException(nameof(requestId));
if (requestId.Length != 32) throw new InvalidDataException(nameof(requestId) + $"{nameof(requestId)} length should be 32 bytes, but was: {requestId.Length}" + requestId.Length);
var func = new GetRequestFunction
{
RequestId = requestId
@ -152,9 +165,9 @@ namespace CodexContractsPlugin
Thread.Sleep(TimeSpan.FromSeconds(secondsLeft + 1));
}
public ProofState GetProofState(Request storageRequest, decimal slotIndex, ulong blockNumber, ulong period)
public ProofState GetProofState(byte[] requestId, decimal slotIndex, ulong blockNumber, ulong period)
{
var slotId = GetSlotId(storageRequest, slotIndex);
var slotId = GetSlotId(requestId, slotIndex);
var required = IsProofRequired(slotId, blockNumber);
if (!required) return new ProofState(false, false);
@ -163,11 +176,16 @@ namespace CodexContractsPlugin
return new ProofState(required, missing);
}
private byte[] GetSlotId(Request request, decimal slotIndex)
public ICodexContracts WithDifferentGeth(IGethNode node)
{
return new CodexContractsAccess(log, node, Deployment);
}
private byte[] GetSlotId(byte[] requestId, decimal slotIndex)
{
var encoder = new ABIEncode();
var encoded = encoder.GetABIEncoded(
new ABIValue("bytes32", request.RequestId),
new ABIValue("bytes32", requestId),
new ABIValue("uint256", slotIndex.ToBig())
);
@ -193,7 +211,7 @@ namespace CodexContractsPlugin
SlotId = slotId,
Period = period
};
gethNode.Call<MarkProofAsMissingFunction>(Deployment.MarketplaceAddress, funcB, blockNumber);
gethNode.Call(Deployment.MarketplaceAddress, funcB, blockNumber);
}
catch (AggregateException exc)
{

View File

@ -7,7 +7,7 @@ namespace CodexContractsPlugin
{
public class CodexContractsContainerRecipe : ContainerRecipeFactory
{
public const string MarketplaceAddressFilename = "/hardhat/deployments/codexdisttestnetwork/Marketplace.json";
public const string DeployedAddressesFilename = "/hardhat/ignition/deployments/chain-789988/deployed_addresses.json";
public const string MarketplaceArtifactFilename = "/hardhat/artifacts/contracts/Marketplace.sol/Marketplace.json";
private readonly DebugInfoVersion versionInfo;

View File

@ -11,7 +11,7 @@ namespace CodexContractsPlugin
public interface ICodexContractsEvents
{
BlockInterval BlockInterval { get; }
Request[] GetStorageRequests();
StorageRequestedEventDTO[] GetStorageRequestedEvents();
RequestFulfilledEventDTO[] GetRequestFulfilledEvents();
RequestCancelledEventDTO[] GetRequestCancelledEvents();
RequestFailedEventDTO[] GetRequestFailedEvents();
@ -38,18 +38,10 @@ namespace CodexContractsPlugin
public BlockInterval BlockInterval { get; }
public Request[] GetStorageRequests()
public StorageRequestedEventDTO[] GetStorageRequestedEvents()
{
var events = gethNode.GetEvents<StorageRequestedEventDTO>(deployment.MarketplaceAddress, BlockInterval);
var i = new ContractInteractions(log, gethNode);
return events.Select(e =>
{
var requestEvent = i.GetRequest(deployment.MarketplaceAddress, e.Event.RequestId);
var result = requestEvent.ReturnValue1;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.RequestId = e.Event.RequestId;
return result;
}).ToArray();
return events.Select(SetBlockOnEvent).ToArray();
}
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents()

View File

@ -35,12 +35,15 @@ namespace CodexContractsPlugin
var container = containers.Containers[0];
Log("Container started.");
var watcher = workflow.CreateCrashWatcher(container);
watcher.Start();
try
{
var result = DeployContract(container, workflow, gethNode);
workflow.Stop(containers, waitTillStopped: false);
watcher.Stop();
Log("Container stopped.");
return result;
}
@ -71,11 +74,16 @@ namespace CodexContractsPlugin
var extractor = new ContractsContainerInfoExtractor(tools.GetLog(), workflow, container);
var marketplaceAddress = extractor.ExtractMarketplaceAddress();
if (string.IsNullOrEmpty(marketplaceAddress)) throw new Exception("Marketplace address not received.");
var (abi, bytecode) = extractor.ExtractMarketplaceAbiAndByteCode();
if (string.IsNullOrEmpty(abi)) throw new Exception("ABI not received.");
if (string.IsNullOrEmpty(bytecode)) throw new Exception("bytecode not received.");
EnsureCompatbility(abi, bytecode);
var interaction = new ContractInteractions(tools.GetLog(), gethNode);
var tokenAddress = interaction.GetTokenAddress(marketplaceAddress);
if (string.IsNullOrEmpty(tokenAddress)) throw new Exception("Token address not received.");
Log("TokenAddress: " + tokenAddress);
Log("Extract completed. Checking sync...");

View File

@ -2,8 +2,6 @@
using CodexContractsPlugin.Marketplace;
using GethPlugin;
using Logging;
using Nethereum.ABI.FunctionEncoding.Attributes;
using Nethereum.Contracts;
using Nethereum.Hex.HexConvertors.Extensions;
using System.Numerics;
using Utils;
@ -33,9 +31,9 @@ namespace CodexContractsPlugin
try
{
log.Debug(tokenAddress);
var function = new GetTokenNameFunction();
var function = new NameFunction();
return gethNode.Call<GetTokenNameFunction, string>(tokenAddress, function);
return gethNode.Call<NameFunction, string>(tokenAddress, function);
}
catch (Exception ex)
{
@ -53,12 +51,24 @@ namespace CodexContractsPlugin
public decimal GetBalance(string tokenAddress, string account)
{
log.Debug($"({tokenAddress}) {account}");
var function = new GetTokenBalanceFunction
var function = new BalanceOfFunction
{
Owner = account
Account = account
};
return gethNode.Call<GetTokenBalanceFunction, BigInteger>(tokenAddress, function).ToDecimal();
return gethNode.Call<BalanceOfFunction, BigInteger>(tokenAddress, function).ToDecimal();
}
public void TransferTestTokens(string tokenAddress, string toAccount, BigInteger amount)
{
log.Debug($"({tokenAddress}) {toAccount} {amount}");
var function = new TransferFunction
{
To = toAccount,
Value = amount
};
gethNode.SendTransaction(tokenAddress, function);
}
public GetRequestOutputDTO GetRequest(string marketplaceAddress, byte[] requestId)
@ -90,7 +100,7 @@ namespace CodexContractsPlugin
log.Debug($"({tokenAddress}) {amount} --> {account}");
if (string.IsNullOrEmpty(account)) throw new ArgumentException("Invalid arguments for MintTestTokens");
var function = new MintTokensFunction
var function = new MintFunction
{
Holder = account,
Amount = amount
@ -110,26 +120,4 @@ namespace CodexContractsPlugin
return gethNode.IsContractAvailable(marketplaceAbi, marketplaceAddress);
}
}
[Function("name", "string")]
public class GetTokenNameFunction : FunctionMessage
{
}
[Function("mint")]
public class MintTokensFunction : FunctionMessage
{
[Parameter("address", "holder", 1)]
public string Holder { get; set; } = string.Empty;
[Parameter("uint256", "amount", 2)]
public BigInteger Amount { get; set; }
}
[Function("balanceOf", "uint256")]
public class GetTokenBalanceFunction : FunctionMessage
{
[Parameter("address", "owner", 1)]
public string Owner { get; set; } = string.Empty;
}
}

View File

@ -27,7 +27,7 @@ namespace CodexContractsPlugin
var marketplaceAddress = Retry(FetchMarketplaceAddress);
if (string.IsNullOrEmpty(marketplaceAddress)) throw new InvalidOperationException("Unable to fetch marketplace account from codex-contracts node. Test infra failure.");
log.Debug("Got MarketplaceAddress: " + marketplaceAddress);
log.Log("MarketplaceAddress: " + marketplaceAddress);
return marketplaceAddress;
}
@ -43,9 +43,10 @@ namespace CodexContractsPlugin
private string FetchMarketplaceAddress()
{
var json = workflow.ExecuteCommand(container, "cat", CodexContractsContainerRecipe.MarketplaceAddressFilename);
var marketplace = JsonConvert.DeserializeObject<MarketplaceJson>(json);
return marketplace!.address;
var json = workflow.ExecuteCommand(container, "cat", CodexContractsContainerRecipe.DeployedAddressesFilename);
json = json.Replace("#", "_");
var addresses = JsonConvert.DeserializeObject<DeployedAddressesJson>(json);
return addresses!.Marketplace_Marketplace;
}
private (string, string) FetchMarketplaceAbiAndByteCode()
@ -67,8 +68,10 @@ namespace CodexContractsPlugin
}
}
public class MarketplaceJson
public class DeployedAddressesJson
{
public string address { get; set; } = string.Empty;
public string Token_TestToken { get; set; } = string.Empty;
public string Verifier_Groth16Verifier { get; set; } = string.Empty;
public string Marketplace_Marketplace { get; set; } = string.Empty;
}
}

View File

@ -17,48 +17,45 @@ namespace CodexContractsPlugin.Marketplace
byte[] RequestId { get; set; }
}
public interface IHasBlockAndRequestId : IHasBlock, IHasRequestId
{
}
public interface IHasSlotIndex
{
ulong SlotIndex { get; set; }
}
public partial class Request : RequestBase, IHasBlock, IHasRequestId
public partial class Request
{
[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 : IHasBlock, IHasRequestId
public partial class StorageRequestedEventDTO : IHasBlockAndRequestId
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
}
public partial class RequestCancelledEventDTO : IHasBlock, IHasRequestId
public partial class RequestFulfilledEventDTO : IHasBlockAndRequestId
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
}
public partial class RequestFailedEventDTO : IHasBlock, IHasRequestId
public partial class RequestCancelledEventDTO : IHasBlockAndRequestId
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
}
public partial class SlotFilledEventDTO : IHasBlock, IHasRequestId, IHasSlotIndex
public partial class RequestFailedEventDTO : IHasBlockAndRequestId
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
}
public partial class SlotFilledEventDTO : IHasBlockAndRequestId, IHasSlotIndex
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
@ -70,13 +67,13 @@ namespace CodexContractsPlugin.Marketplace
}
}
public partial class SlotFreedEventDTO : IHasBlock, IHasRequestId, IHasSlotIndex
public partial class SlotFreedEventDTO : IHasBlockAndRequestId, IHasSlotIndex
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
}
public partial class SlotReservationsFullEventDTO : IHasBlock, IHasRequestId, IHasSlotIndex
public partial class SlotReservationsFullEventDTO : IHasBlockAndRequestId, IHasSlotIndex
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
@ -88,7 +85,7 @@ namespace CodexContractsPlugin.Marketplace
public BlockTimeEntry Block { get; set; }
}
public partial class ReserveSlotFunction : IHasBlock, IHasRequestId, IHasSlotIndex
public partial class ReserveSlotFunction : IHasBlockAndRequestId, IHasSlotIndex
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
@ -96,8 +93,23 @@ namespace CodexContractsPlugin.Marketplace
public partial class MarketplaceConfig : IMarketplaceConfigInput
{
public int MaxNumberOfSlashes => this.Collateral.MaxNumberOfSlashes;
public TimeSpan PeriodDuration => TimeSpan.FromSeconds(this.Proofs.Period);
public int MaxNumberOfSlashes
{
get
{
if (Collateral == null) return -1;
return Collateral.MaxNumberOfSlashes;
}
}
public TimeSpan PeriodDuration
{
get
{
if (Proofs == null) return TimeSpan.MinValue;
return TimeSpan.FromSeconds(this.Proofs.Period);
}
}
}
}
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -60,7 +60,16 @@ namespace GethPlugin
private static string Retry(Func<string> fetch)
{
return Time.Retry(fetch, nameof(GethContainerInfoExtractor));
// This class is the first moment where we interact with our new geth container.
// K8s might be moving pods and/or setting up new VMs.
// So we apply a generous retry timeout.
var retry = new Retry(nameof(GethContainerInfoExtractor),
maxTimeout: TimeSpan.FromMinutes(15.0),
sleepAfterFail: TimeSpan.FromSeconds(20.0),
onFail: f => { },
failFast: false);
return retry.Run(fetch);
}
}

View File

@ -21,6 +21,7 @@ namespace GethPlugin
string SendEth(EthAddress account, Ether eth);
TResult Call<TFunction, TResult>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new();
TResult Call<TFunction, TResult>(string contractAddress, TFunction function, ulong blockNumber) where TFunction : FunctionMessage, new();
void Call<TFunction>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new();
void Call<TFunction>(string contractAddress, TFunction function, ulong blockNumber) where TFunction : FunctionMessage, new();
string SendTransaction<TFunction>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new();
Transaction GetTransaction(string transactionHash);
@ -32,6 +33,7 @@ namespace GethPlugin
BlockInterval ConvertTimeRangeToBlockRange(TimeRange timeRange);
BlockTimeEntry GetBlockForNumber(ulong number);
void IterateFunctionCalls<TFunc>(BlockInterval blockInterval, Action<BlockTimeEntry, TFunc> onCall) where TFunc : FunctionMessage, new();
IGethNode WithDifferentAccount(EthAccount account);
}
public class DeploymentGethNode : BaseGethNode, IGethNode
@ -68,6 +70,21 @@ namespace GethPlugin
var creator = new NethereumInteractionCreator(log, blockCache, address.Host, address.Port, account.PrivateKey);
return creator.CreateWorkflow();
}
public IGethNode WithDifferentAccount(EthAccount account)
{
return new DeploymentGethNode(log, blockCache,
new GethDeployment(
StartResult.Pod,
StartResult.DiscoveryPort,
StartResult.HttpPort,
StartResult.WsPort,
new GethAccount(
account.EthAddress.Address,
account.PrivateKey
),
account.PrivateKey));
}
}
public class CustomGethNode : BaseGethNode, IGethNode
@ -95,6 +112,11 @@ namespace GethPlugin
throw new NotImplementedException();
}
public IGethNode WithDifferentAccount(EthAccount account)
{
return new CustomGethNode(log, blockCache, gethHost, gethPort, account.PrivateKey);
}
protected override NethereumInteraction StartInteraction()
{
var creator = new NethereumInteractionCreator(log, blockCache, gethHost, gethPort, privateKey);
@ -116,7 +138,7 @@ namespace GethPlugin
public Ether GetEthBalance(EthAddress address)
{
return StartInteraction().GetEthBalance(address.Address).Eth();
return StartInteraction().GetEthBalance(address.Address).Wei();
}
public string SendEth(IHasEthAddress owner, Ether eth)
@ -139,6 +161,11 @@ namespace GethPlugin
return StartInteraction().Call<TFunction, TResult>(contractAddress, function, blockNumber);
}
public void Call<TFunction>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new()
{
StartInteraction().Call(contractAddress, function);
}
public void Call<TFunction>(string contractAddress, TFunction function, ulong blockNumber) where TFunction : FunctionMessage, new()
{
StartInteraction().Call(contractAddress, function, blockNumber);

View File

@ -1,4 +1,5 @@
using CodexContractsPlugin;
using CodexPlugin;
using CodexTests;
using NUnit.Framework;
using Utils;
@ -8,17 +9,23 @@ namespace CodexReleaseTests.DataTests
[TestFixture]
public class DataExpiryTest : CodexDistTest
{
private readonly TimeSpan blockTtl = TimeSpan.FromMinutes(1.0);
private readonly TimeSpan blockInterval = TimeSpan.FromSeconds(10.0);
private readonly int blockCount = 100000;
private ICodexSetup WithFastBlockExpiry(ICodexSetup setup)
{
return setup
.WithBlockTTL(blockTtl)
.WithBlockMaintenanceInterval(blockInterval)
.WithBlockMaintenanceNumber(blockCount);
}
[Test]
public void DeletesExpiredData()
{
var fileSize = 100.MB();
var blockTtl = TimeSpan.FromMinutes(1.0);
var interval = TimeSpan.FromSeconds(10.0);
var node = StartCodex(s => s
.WithBlockTTL(blockTtl)
.WithBlockMaintenanceInterval(interval)
.WithBlockMaintenanceNumber(100000)
);
var fileSize = 3.MB();
var node = StartCodex(s => WithFastBlockExpiry(s));
var startSpace = node.Space();
Assert.That(startSpace.QuotaUsedBytes, Is.EqualTo(0));
@ -46,18 +53,13 @@ namespace CodexReleaseTests.DataTests
[Test]
public void DeletesExpiredDataUsedByStorageRequests()
{
var fileSize = 100.MB();
var blockTtl = TimeSpan.FromMinutes(1.0);
var interval = TimeSpan.FromSeconds(10.0);
var fileSize = 3.MB();
var bootstrapNode = StartCodex();
var geth = StartGethNode(s => s.IsMiner());
var contracts = Ci.StartCodexContracts(geth, bootstrapNode.Version);
var node = StartCodex(s => s
var node = StartCodex(s => WithFastBlockExpiry(s)
.EnableMarketplace(geth, contracts, m => m.WithInitial(100.Eth(), 100.Tst()))
.WithBlockTTL(blockTtl)
.WithBlockMaintenanceInterval(interval)
.WithBlockMaintenanceNumber(100000)
);
var startSpace = node.Space();
@ -92,5 +94,55 @@ namespace CodexReleaseTests.DataTests
Assert.That(cleanupSpace.QuotaUsedBytes, Is.EqualTo(startSpace.QuotaUsedBytes));
Assert.That(cleanupSpace.FreeBytes, Is.EqualTo(startSpace.FreeBytes));
}
[Test]
[Ignore("Issue not fixed. Ticket: https://github.com/codex-storage/nim-codex/issues/1291")]
public void StorageRequestsKeepManifests()
{
var bootstrapNode = StartCodex(s => s.WithName("Bootstrap"));
var geth = StartGethNode(s => s.IsMiner());
var contracts = Ci.StartCodexContracts(geth, bootstrapNode.Version);
var client = StartCodex(s => WithFastBlockExpiry(s)
.WithName("client")
.WithBootstrapNode(bootstrapNode)
.EnableMarketplace(geth, contracts, m => m.WithInitial(100.Eth(), 100.Tst()))
);
var hosts = StartCodex(3, s => WithFastBlockExpiry(s)
.WithName("host")
.WithBootstrapNode(bootstrapNode)
.EnableMarketplace(geth, contracts, m => m.AsStorageNode().WithInitial(100.Eth(), 100.Tst()))
);
foreach (var host in hosts) host.Marketplace.MakeStorageAvailable(new CodexClient.CreateStorageAvailability(
totalSpace: 2.GB(),
maxDuration: TimeSpan.FromDays(2.0),
minPricePerBytePerSecond: 1.TstWei(),
totalCollateral: 10.Tst()));
var uploadCid = client.UploadFile(GenerateTestFile(5.MB()));
var request = client.Marketplace.RequestStorage(new CodexClient.StoragePurchaseRequest(uploadCid)
{
CollateralPerByte = 1.TstWei(),
Duration = TimeSpan.FromDays(1.0),
Expiry = TimeSpan.FromHours(1.0),
MinRequiredNumberOfNodes = 3,
NodeFailureTolerance = 1,
PricePerBytePerSecond = 10.TstWei(),
ProofProbability = 99999
});
request.WaitForStorageContractSubmitted();
request.WaitForStorageContractStarted();
var storeCid = request.ContentId;
var clientManifest = client.DownloadManifestOnly(storeCid);
Assert.That(clientManifest.Manifest.Protected, Is.True);
client.Stop(waitTillStopped: true);
Thread.Sleep(blockTtl * 2.0);
var checker = StartCodex(s => s.WithName("checker").WithBootstrapNode(bootstrapNode));
var manifest = checker.DownloadManifestOnly(storeCid);
Assert.That(manifest.Manifest.Protected, Is.True);
}
}
}

View File

@ -1,6 +1,4 @@
using System.Security.Cryptography;
using CodexReleaseTests.Utils;
using Nethereum.JsonRpc.Client;
using CodexReleaseTests.Utils;
using NUnit.Framework;
using Utils;

View File

@ -33,9 +33,9 @@ namespace CodexReleaseTests.DataTests
var local2 = localFiles.Content.Single(f => f.Cid == cid2);
Assert.That(local1.Manifest.Protected, Is.False);
Assert.That(local1.Manifest.OriginalBytes, Is.EqualTo(size1));
Assert.That(local1.Manifest.DatasetSize, Is.EqualTo(size1));
Assert.That(local2.Manifest.Protected, Is.False);
Assert.That(local2.Manifest.OriginalBytes, Is.EqualTo(size2));
Assert.That(local2.Manifest.DatasetSize, Is.EqualTo(size2));
}
}
}

View File

@ -27,7 +27,7 @@ namespace CodexReleaseTests.DataTests
Assert.That(spaceDiff, Is.LessThan(64.KB().SizeInBytes));
Assert.That(localDataset.Cid, Is.EqualTo(cid));
Assert.That(localDataset.Manifest.OriginalBytes.SizeInBytes, Is.EqualTo(file.GetFilesize().SizeInBytes));
Assert.That(localDataset.Manifest.DatasetSize.SizeInBytes, Is.EqualTo(file.GetFilesize().SizeInBytes));
}
}
}

View File

@ -22,7 +22,7 @@ namespace CodexReleaseTests.DataTests
var localDataset = downloader.DownloadStreamlessWait(cid, size);
Assert.That(localDataset.Cid, Is.EqualTo(cid));
Assert.That(localDataset.Manifest.OriginalBytes.SizeInBytes, Is.EqualTo(file.GetFilesize().SizeInBytes));
Assert.That(localDataset.Manifest.DatasetSize.SizeInBytes, Is.EqualTo(file.GetFilesize().SizeInBytes));
// Stop the uploader node and verify that the downloader has the data.
uploader.Stop(waitTillStopped: true);

View File

@ -7,15 +7,22 @@ using Utils;
namespace CodexReleaseTests.DataTests
{
[TestFixture]
[TestFixture(2, 10)]
[TestFixture(5, 20)]
[TestFixture(10, 20)]
public class SwarmTests : AutoBootstrapDistTest
{
private readonly int numberOfNodes;
private readonly int filesizeMb;
public SwarmTests(int numberOfNodes, int filesizeMb)
{
this.numberOfNodes = numberOfNodes;
this.filesizeMb = filesizeMb;
}
[Test]
[Combinatorial]
public void SmallSwarm(
[Values(2)] int numberOfNodes,
[Values(10)] int filesizeMb
)
public void Swarm()
{
var filesize = filesizeMb.MB();
var nodes = StartCodex(numberOfNodes);
@ -28,11 +35,7 @@ namespace CodexReleaseTests.DataTests
}
[Test]
[Combinatorial]
public void StreamlessSmallSwarm(
[Values(2)] int numberOfNodes,
[Values(10)] int filesizeMb
)
public void StreamlessSwarm()
{
var filesize = filesizeMb.MB();
var nodes = StartCodex(numberOfNodes);
@ -86,7 +89,7 @@ namespace CodexReleaseTests.DataTests
var file = remaining.PickOneRandom();
try
{
var dl = node.DownloadContent(file.Cid);
var dl = node.DownloadContent(file.Cid, TimeSpan.FromMinutes(30));
lock (file.Lock)
{
file.Downloaded.Add(dl);

View File

@ -19,7 +19,7 @@ namespace CodexReleaseTests.DataTests
try
{
node.DownloadContent(unknownCid);
node.DownloadContent(unknownCid, TimeSpan.FromMinutes(2.0));
}
catch (Exception ex)
{

View File

@ -1,49 +0,0 @@
using CodexContractsPlugin;
using CodexContractsPlugin.ChainMonitor;
using Logging;
namespace CodexReleaseTests.MarketTests
{
public class ChainMonitor
{
private readonly ChainState chainMonitor;
private readonly TimeSpan interval;
private CancellationTokenSource cts = new CancellationTokenSource();
private Task worker = null!;
public ChainMonitor(ILog log, ICodexContracts contracts, DateTime startUtc, TimeSpan interval)
{
chainMonitor = new ChainState(log, contracts, new DoNothingChainEventHandler(), startUtc, true);
this.interval = interval;
}
public void Start()
{
cts = new CancellationTokenSource();
worker = Task.Run(Worker);
}
public void Stop()
{
cts.Cancel();
worker.Wait();
worker = null!;
cts = null!;
}
public PeriodMonitorResult GetPeriodReports()
{
return chainMonitor.PeriodMonitor.GetAndClearReports();
}
private void Worker()
{
while (!cts.IsCancellationRequested)
{
Thread.Sleep(interval);
chainMonitor.Update();
}
}
}
}

View File

@ -21,7 +21,7 @@ namespace CodexReleaseTests.MarketTests
[Test]
[Combinatorial]
public void Fail(
[Values([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])] int rerun
[Rerun] int rerun
)
{
var hosts = StartHosts();
@ -68,7 +68,7 @@ namespace CodexReleaseTests.MarketTests
private IStoragePurchaseContract CreateStorageRequest(ICodexNode client)
{
var cid = client.UploadFile(GenerateTestFile(5.MB()));
var cid = client.UploadFile(GenerateTestFile(3.MB()));
return client.Marketplace.RequestStorage(new StoragePurchaseRequest(cid)
{
Duration = HostAvailabilityMaxDuration / 2,

View File

@ -6,13 +6,13 @@ using Utils;
namespace CodexReleaseTests.MarketTests
{
[TestFixture(5, 3, 1)]
[TestFixture(10, 20, 10)]
[TestFixture(10, 8, 4)]
public class FinishTest : MarketplaceAutoBootstrapDistTest
{
public FinishTest(int hosts, int slots, int tolerance)
{
this.hosts = hosts;
purchaseParams = new PurchaseParams(slots, tolerance, uploadFilesize: 10.MB());
purchaseParams = new PurchaseParams(slots, tolerance, uploadFilesize: 3.MB());
}
private readonly TestToken pricePerBytePerSecond = 10.TstWei();
@ -22,12 +22,12 @@ namespace CodexReleaseTests.MarketTests
protected override int NumberOfHosts => hosts;
protected override int NumberOfClients => 1;
protected override ByteSize HostAvailabilitySize => purchaseParams.SlotSize.Multiply(5.1);
protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 12;
protected override TimeSpan HostAvailabilityMaxDuration => GetContractDuration() * 2;
[Test]
[Combinatorial]
public void Finish(
[Values([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])] int rerun
[Rerun] int rerun
)
{
var hosts = StartHosts();

View File

@ -0,0 +1,51 @@
using BlockchainUtils;
using CodexContractsPlugin;
using CodexPlugin;
using DistTestCore;
using GethPlugin;
using NUnit.Framework;
using Utils;
namespace CodexReleaseTests.MarketTests
{
[TestFixture]
public class TestTokenTransferTest : DistTest
{
private readonly EthAccount user1 = EthAccountGenerator.GenerateNew();
private readonly EthAccount user2 = EthAccountGenerator.GenerateNew();
[Test]
public void CanTransferTokens()
{
var node = Ci.StartCodexNode();
var blockCache = new BlockCache();
var geth = Ci.StartGethNode(blockCache, s => s.IsMiner());
var contracts = Ci.StartCodexContracts(geth, node.Version);
geth.SendEth(user1.EthAddress, 1.Eth());
geth.SendEth(user2.EthAddress, 1.Eth());
contracts.MintTestTokens(user1.EthAddress, 10.Tst());
Balances(contracts, 10.Tst(), 0.Tst());
var geth1 = geth.WithDifferentAccount(user1);
var geth2 = geth.WithDifferentAccount(user2);
var contracts1 = contracts.WithDifferentGeth(geth1);
var contracts2 = contracts.WithDifferentGeth(geth2);
contracts1.TransferTestTokens(user2.EthAddress, 5.Tst());
Balances(contracts, 5.Tst(), 5.Tst());
contracts2.TransferTestTokens(user1.EthAddress, 2.Tst());
Balances(contracts, 7.Tst(), 3.Tst());
}
private void Balances(ICodexContracts contracts, TestToken one, TestToken two)
{
var balance1 = contracts.GetTestTokenBalance(user1.EthAddress);
var balance2 = contracts.GetTestTokenBalance(user2.EthAddress);
Assert.That(balance1, Is.EqualTo(one));
Assert.That(balance2, Is.EqualTo(two));
}
}
}

View File

@ -0,0 +1,72 @@
using CodexClient;
using CodexReleaseTests.Utils;
using NUnit.Framework;
using Utils;
namespace CodexReleaseTests.MarketTests
{
public class MaxCapacityTest : MarketplaceAutoBootstrapDistTest
{
private readonly TestToken pricePerBytePerSecond = 10.TstWei();
private readonly PurchaseParams purchaseParams = new PurchaseParams(
nodes: 10,
tolerance: 5,
uploadFilesize: 10.MB()
);
protected override int NumberOfHosts => purchaseParams.Nodes / 2;
protected override int NumberOfClients => 1;
protected override ByteSize HostAvailabilitySize => purchaseParams.SlotSize.Multiply(2.1);
protected override TimeSpan HostAvailabilityMaxDuration => GetContractDuration() * 2;
[Test]
[Combinatorial]
public void TwoSlotsEach(
[Rerun] int rerun
)
{
var hosts = StartHosts();
var client = StartClients().Single();
AssertHostAvailabilitiesAreEmpty(hosts);
var request = CreateStorageRequest(client);
request.WaitForStorageContractSubmitted();
AssertContractIsOnChain(request);
WaitForContractStarted(request);
AssertContractSlotsAreFilledByHosts(request, hosts);
}
private IStoragePurchaseContract CreateStorageRequest(ICodexNode client)
{
var cid = client.UploadFile(GenerateTestFile(purchaseParams.UploadFilesize));
var config = GetContracts().Deployment.Config;
return client.Marketplace.RequestStorage(new StoragePurchaseRequest(cid)
{
Duration = GetContractDuration(),
Expiry = GetContractExpiry(),
MinRequiredNumberOfNodes = (uint)purchaseParams.Nodes,
NodeFailureTolerance = (uint)purchaseParams.Tolerance,
PricePerBytePerSecond = pricePerBytePerSecond,
ProofProbability = 20,
CollateralPerByte = 100.TstWei()
});
}
private TimeSpan GetContractExpiry()
{
return GetContractDuration() / 2;
}
private TimeSpan GetContractDuration()
{
return Get8TimesConfiguredPeriodDuration();
}
private TimeSpan Get8TimesConfiguredPeriodDuration()
{
return GetPeriodDuration() * 8.0;
}
}
}

View File

@ -29,13 +29,11 @@ namespace CodexReleaseTests.MarketTests
#endregion
[Ignore("Test is ready. Waiting for repair implementation. " +
"Slots are never freed because proofs are never marked as missing. Issue: https://github.com/codex-storage/nim-codex/issues/1153")]
[Test]
[Combinatorial]
public void RollingRepairSingleFailure(
[Values([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])] int rerun,
[Values(10)] int numFailures)
[Rerun] int rerun,
[Values(5)] int numFailures)
{
var hosts = StartHosts().ToList();
var client = StartClients().Single();
@ -98,7 +96,7 @@ namespace CodexReleaseTests.MarketTests
private void WaitForNewSlotFilledEvent(IStoragePurchaseContract contract, ulong slotIndex)
{
Log(nameof(WaitForNewSlotFilledEvent));
var start = DateTime.UtcNow;
var start = DateTime.UtcNow - TimeSpan.FromSeconds(10.0);
var timeout = contract.Purchase.Expiry;
while (DateTime.UtcNow < start + timeout)
@ -122,6 +120,7 @@ namespace CodexReleaseTests.MarketTests
if (matches.Length == 1)
{
Log($"Found the correct new slotFilled event: {matches[0].ToString()}");
return;
}
Thread.Sleep(TimeSpan.FromSeconds(15));

View File

@ -21,13 +21,13 @@ namespace CodexReleaseTests.MarketTests
protected override int NumberOfHosts => hosts;
protected override int NumberOfClients => 6;
protected override ByteSize HostAvailabilitySize => purchaseParams.SlotSize.Multiply(100.0);
protected override TimeSpan HostAvailabilityMaxDuration => Get8TimesConfiguredPeriodDuration() * 12;
protected override TimeSpan HostAvailabilityMaxDuration => GetContractDuration() * 2;
private readonly TestToken pricePerBytePerSecond = 10.TstWei();
[Test]
[Combinatorial]
public void Sequential(
[Values(10)] int numGenerations)
[Values(5)] int numGenerations)
{
var hosts = StartHosts();
var clients = StartClients();
@ -93,7 +93,7 @@ namespace CodexReleaseTests.MarketTests
MinRequiredNumberOfNodes = (uint)purchaseParams.Nodes,
NodeFailureTolerance = (uint)purchaseParams.Tolerance,
PricePerBytePerSecond = pricePerBytePerSecond,
ProofProbability = 10000,
ProofProbability = 100000,
CollateralPerByte = 1.TstWei()
});
}

View File

@ -11,7 +11,7 @@ namespace CodexReleaseTests.MarketTests
private readonly PurchaseParams purchaseParams = new PurchaseParams(
nodes: 3,
tolerance: 1,
uploadFilesize: 10.MB()
uploadFilesize: 3.MB()
);
private readonly TestToken pricePerBytePerSecond = 10.TstWei();
@ -23,7 +23,7 @@ namespace CodexReleaseTests.MarketTests
[Test]
[Combinatorial]
public void Start(
[Values([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16])] int rerun
[Rerun] int rerun
)
{
var hosts = StartHosts();

View File

@ -0,0 +1,16 @@
using NUnit.Framework;
namespace CodexReleaseTests
{
public class RerunAttribute : ValuesAttribute
{
private const int NumberOfReRuns = 8;
public RerunAttribute()
{
var list = new List<object>();
for (var i = 0; i < NumberOfReRuns; i++) list.Add(i);
data = list.ToArray();
}
}
}

View File

@ -41,7 +41,7 @@ namespace CodexReleaseTests.Utils
private void Worker(Action onFailure)
{
var state = new ChainState(log, contracts, new DoNothingChainEventHandler(), startUtc, doProofPeriodMonitoring: true);
var state = new ChainState(log, contracts, new DoNothingThrowingChainEventHandler(), startUtc, doProofPeriodMonitoring: true);
Thread.Sleep(updateInterval);
log.Log("Chain monitoring started");

View File

@ -176,8 +176,7 @@ namespace CodexReleaseTests.Utils
var result = new ChainMonitor(log, contracts, startUtc);
result.Start(() =>
{
log.Error("Failure in chain monitor. No chain updates after this point.");
//Assert.Fail("Failure in chain monitor.");
Assert.Fail("Failure in chain monitor.");
});
return result;
}
@ -262,10 +261,15 @@ namespace CodexReleaseTests.Utils
var fills = events.GetSlotFilledEvents();
return fills.Select(f =>
{
var host = possibleHosts.Single(h => h.EthAddress.Address == f.Host.Address);
// We can encounter a fill event that's from an old host.
// We must disregard those.
var host = possibleHosts.SingleOrDefault(h => h.EthAddress.Address == f.Host.Address);
if (host == null) return null;
return new SlotFill(f, host);
}).ToArray();
})
.Where(f => f != null)
.Cast<SlotFill>()
.ToArray();
}
protected void AssertClientHasPaidForContract(TestToken pricePerBytePerSecond, ICodexNode client, IStoragePurchaseContract contract, ICodexNodeGroup hosts)
@ -363,7 +367,7 @@ namespace CodexReleaseTests.Utils
return Time.Retry(() =>
{
var events = GetContracts().GetEvents(GetTestRunTimeRange());
var submitEvent = events.GetStorageRequests().SingleOrDefault(e => e.RequestId.ToHex(false) == contract.PurchaseId);
var submitEvent = events.GetStorageRequestedEvents().SingleOrDefault(e => e.RequestId.ToHex() == contract.PurchaseId);
if (submitEvent == null)
{
// We're too early.
@ -402,12 +406,21 @@ namespace CodexReleaseTests.Utils
protected void AssertContractIsOnChain(IStoragePurchaseContract contract)
{
// Check the creation event.
AssertOnChainEvents(events =>
{
var onChainRequests = events.GetStorageRequests();
if (onChainRequests.Any(r => r.Id == contract.PurchaseId)) return;
var onChainRequests = events.GetStorageRequestedEvents();
if (onChainRequests.Any(r => r.RequestId.ToHex() == contract.PurchaseId)) return;
throw new Exception($"OnChain request {contract.PurchaseId} not found...");
}, nameof(AssertContractIsOnChain));
// Check that the getRequest call returns it.
var rid = contract.PurchaseId.HexToByteArray();
var r = GetContracts().GetRequest(rid);
if (r == null) throw new Exception($"Failed to get Request from {nameof(GetRequestFunction)}");
Assert.That(r.Ask.Duration, Is.EqualTo(contract.Purchase.Duration.TotalSeconds));
Assert.That(r.Ask.Slots, Is.EqualTo(contract.Purchase.MinRequiredNumberOfNodes));
Assert.That(((int)r.Ask.ProofProbability), Is.EqualTo(contract.Purchase.ProofProbability));
}
protected void AssertOnChainEvents(Action<ICodexContractsEvents> onEvents, string description)
@ -444,7 +457,7 @@ namespace CodexReleaseTests.Utils
float downtime = numBlocksInDowntimeSegment;
float window = 256.0f;
var chanceOfDowntime = downtime / window;
return 1.0f + chanceOfDowntime + chanceOfDowntime;
return 1.0f + (5.0f * chanceOfDowntime);
}
public class SlotFill
{

View File

@ -33,7 +33,7 @@ namespace CodexReleaseTests.Utils
var numSlotBlocks = 1 + ((numBlocks - 1) / Nodes); // round-up div.
// Next power of two:
var numSlotBlocksPow2 = NextPowerOf2(numSlotBlocks);
var numSlotBlocksPow2 = IsOrNextPowerOf2(numSlotBlocks);
return new ByteSize(blockSize.SizeInBytes * numSlotBlocksPow2);
}
@ -51,8 +51,9 @@ namespace CodexReleaseTests.Utils
return new ByteSize(blockSize.SizeInBytes * totalBlocks);
}
private int NextPowerOf2(int n)
private int IsOrNextPowerOf2(int n)
{
if (IsPowerOfTwo(n)) return n;
n = n - 1;
var lg = Convert.ToInt32(Math.Round(Math.Log2(Convert.ToDouble(n))));
return 1 << (lg + 1);
@ -63,5 +64,10 @@ namespace CodexReleaseTests.Utils
var x = size.SizeInBytes;
return (x != 0) && ((x & (x - 1)) == 0);
}
private static bool IsPowerOfTwo(int x)
{
return (x != 0) && ((x & (x - 1)) == 0);
}
}
}

View File

@ -179,16 +179,34 @@ namespace DistTestCore
private IWebCallTimeSet GetWebCallTimeSet()
{
if (IsRunningInCluster())
{
Log(" > Detected we're running in the cluster. Using long webCall timeset.");
return new LongWebCallTimeSet();
}
if (ShouldUseLongTimeouts()) return new LongWebCallTimeSet();
return new DefaultWebCallTimeSet();
}
private IK8sTimeSet GetK8sTimeSet()
{
if (IsRunningInCluster())
{
Log(" > Detected we're running in the cluster. Using long kubernetes timeset.");
return new LongK8sTimeSet();
}
if (ShouldUseLongTimeouts()) return new LongK8sTimeSet();
return new DefaultK8sTimeSet();
}
private bool IsRunningInCluster()
{
var testType = Environment.GetEnvironmentVariable("TEST_TYPE");
return testType == "release-tests";
}
private bool ShouldWaitForCleanup()
{
return CurrentTestMethodHasAttribute<WaitForCleanupAttribute>();
@ -256,7 +274,7 @@ namespace DistTestCore
private string GetCurrentTestName()
{
return $"[{TestContext.CurrentContext.Test.Name}]";
return $"[{NameUtils.GetRawFixtureName()}:{NameUtils.GetTestMethodName()}]";
}
public DistTestResult GetTestResult()

View File

@ -28,10 +28,11 @@ namespace DistTestCore
public static string GetRawFixtureName()
{
var test = TestContext.CurrentContext.Test;
if (test.ClassName!.Contains("AdhocContext")) return "none";
var className = test.ClassName!.Substring(test.ClassName.LastIndexOf('.') + 1);
className += FormatArguments(test);
return className.Replace('.', '-');
var fullName = test.FullName;
if (fullName.Contains("AdhocContext")) return "none";
var name = fullName.Substring(0, fullName.LastIndexOf('.'));
name += FormatArguments(test);
return name.Replace('.', '-').Replace(',', '-');
}
public static string GetCategoryName()

View File

@ -120,7 +120,7 @@ namespace ExperimentalTests.BasicTests
//Thread.Sleep(GethContainerRecipe.BlockInterval * blocks);
AssertBalance(contracts, client, Is.LessThan(clientInitialBalance), "Buyer was not charged for storage.");
Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Finished));
Assert.That(contracts.GetRequestState(request.RequestId), Is.EqualTo(RequestState.Finished));
}
private TrackedFile CreateFile(ByteSize fileSize)
@ -148,24 +148,25 @@ namespace ExperimentalTests.BasicTests
}, purchase.Expiry + TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(5), "Checking SlotFilled events");
}
private void AssertStorageRequest(Request request, StoragePurchaseRequest purchase, ICodexContracts contracts, ICodexNode buyer)
private void AssertStorageRequest(StorageRequestedEventDTO request, StoragePurchaseRequest purchase, ICodexContracts contracts, ICodexNode buyer)
{
Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Started));
Assert.That(request.ClientAddress, Is.EqualTo(buyer.EthAddress));
Assert.That(contracts.GetRequestState(request.RequestId), Is.EqualTo(RequestState.Started));
var r = contracts.GetRequest(request.RequestId);
Assert.That(r.ClientAddress, Is.EqualTo(buyer.EthAddress));
Assert.That(request.Ask.Slots, Is.EqualTo(purchase.MinRequiredNumberOfNodes));
}
private Request GetOnChainStorageRequest(ICodexContracts contracts, IGethNode geth)
private StorageRequestedEventDTO GetOnChainStorageRequest(ICodexContracts contracts, IGethNode geth)
{
var events = contracts.GetEvents(GetTestRunTimeRange());
var requests = events.GetStorageRequests();
var requests = events.GetStorageRequestedEvents();
Assert.That(requests.Length, Is.EqualTo(1));
return requests.Single();
}
private void AssertContractSlot(ICodexContracts contracts, Request request, int contractSlotIndex)
private void AssertContractSlot(ICodexContracts contracts, StorageRequestedEventDTO request, int contractSlotIndex)
{
var slotHost = contracts.GetSlotHost(request, contractSlotIndex);
var slotHost = contracts.GetSlotHost(request.RequestId, contractSlotIndex);
Assert.That(slotHost?.Address, Is.Not.Null);
}
}

View File

@ -35,6 +35,7 @@ namespace BiblioTech
public async Task SendInAdminChannel(string[] lines)
{
if (adminChannel == null) return;
var chunker = new LineChunker(lines);
var chunks = chunker.GetChunks();
if (!chunks.Any()) return;

View File

@ -20,7 +20,9 @@ namespace BiblioTech
Program.Log.Log($"Responding to '{Name}'");
var context = new CommandContext(command, command.Data.Options);
await command.RespondAsync(StartingMessage, ephemeral: IsEphemeral(context));
await Invoke(context);
// Fire and forget invocation handler. Return SlashCommandHandler immediately.
_ = Invoke(context);
}
catch (Exception ex)
{

View File

@ -8,9 +8,13 @@ namespace BiblioTech
{
protected override async Task Invoke(CommandContext context)
{
var gethConnector = GethConnector.GethConnector.Initialize(Program.Log);
var gethConnector = GetGeth();
if (gethConnector == null)
{
await context.Followup("Blockchain operations are (temporarily) unavailable.");
return;
}
if (gethConnector == null) return;
var gethNode = gethConnector.GethNode;
var contracts = gethConnector.CodexContracts;
@ -23,6 +27,19 @@ namespace BiblioTech
await Execute(context, gethNode, contracts);
}
private GethConnector.GethConnector? GetGeth()
{
try
{
return GethConnector.GethConnector.Initialize(Program.Log);
}
catch (Exception ex)
{
Program.Log.Error("Failed to initialize geth connector: " + ex);
return null;
}
}
protected abstract Task Execute(CommandContext context, IGethNode gethNode, ICodexContracts contracts);
}
}

View File

@ -1,16 +1,20 @@
using Newtonsoft.Json;
using Logging;
using Newtonsoft.Json;
using Utils;
namespace BiblioTech.CodexChecking
{
public class CheckRepo
{
private const string modelFilename = "model.json";
private readonly ILog log;
private readonly Configuration config;
private readonly object _lock = new object();
private CheckRepoModel? model = null;
public CheckRepo(Configuration config)
public CheckRepo(ILog log, Configuration config)
{
this.log = log;
this.config = config;
}
@ -18,20 +22,32 @@ namespace BiblioTech.CodexChecking
{
lock (_lock)
{
if (model == null) LoadModel();
var existing = model.Reports.SingleOrDefault(r => r.UserId == userId);
if (existing == null)
var sw = System.Diagnostics.Stopwatch.StartNew();
try
{
var newEntry = new CheckReport
if (model == null) LoadModel();
var existing = model.Reports.SingleOrDefault(r => r.UserId == userId);
if (existing == null)
{
UserId = userId,
};
model.Reports.Add(newEntry);
SaveChanges();
return newEntry;
var newEntry = new CheckReport
{
UserId = userId,
};
model.Reports.Add(newEntry);
SaveChanges();
return newEntry;
}
return existing;
}
finally
{
var elapsed = sw.Elapsed;
if (elapsed > TimeSpan.FromMilliseconds(500))
{
log.Log($"Warning {nameof(GetOrCreate)} took {Time.FormatDuration(elapsed)}");
}
}
return existing;
}
}

View File

@ -15,6 +15,7 @@ namespace BiblioTech.CodexChecking
Task CouldNotDownloadCid();
Task GiveCidToUser(string cid);
Task GiveDataFileToUser(string fileContent);
Task CodexUnavailable();
Task ToAdminChannel(string msg);
}
@ -44,6 +45,12 @@ namespace BiblioTech.CodexChecking
}
var cid = UploadData(check.UniqueData);
if (cid == null)
{
await handler.CodexUnavailable();
return;
}
await handler.GiveCidToUser(cid);
}
@ -96,7 +103,12 @@ namespace BiblioTech.CodexChecking
if (IsManifestLengthCompatible(handler, check, manifest))
{
if (IsContentCorrect(handler, check, receivedCid))
var correct = IsContentCorrect(handler, check, receivedCid);
if (!correct.HasValue) {
await handler.CodexUnavailable();
return;
}
if (correct.Value)
{
await CheckNowCompleted(handler, check, userId, "UploadCheck");
return;
@ -120,7 +132,7 @@ namespace BiblioTech.CodexChecking
check.CompletedUtc < expiry;
}
private string UploadData(string uniqueData)
private string? UploadData(string uniqueData)
{
var filePath = Path.Combine(config.ChecksDataPath, Guid.NewGuid().ToString());
@ -163,7 +175,7 @@ namespace BiblioTech.CodexChecking
private bool IsManifestLengthCompatible(ICheckResponseHandler handler, TransferCheck check, Manifest manifest)
{
var dataLength = check.UniqueData.Length;
var manifestLength = manifest.OriginalBytes.SizeInBytes;
var manifestLength = manifest.DatasetSize.SizeInBytes;
Log($"Checking manifest length: dataLength={dataLength},manifestLength={manifestLength}");
@ -172,7 +184,7 @@ namespace BiblioTech.CodexChecking
manifestLength < (dataLength + 1);
}
private bool IsContentCorrect(ICheckResponseHandler handler, TransferCheck check, string receivedCid)
private bool? IsContentCorrect(ICheckResponseHandler handler, TransferCheck check, string receivedCid)
{
try
{
@ -190,6 +202,8 @@ namespace BiblioTech.CodexChecking
}
});
if (content == null) return null;
Log($"Checking content: content={content},check={check.UniqueData}");
return content == check.UniqueData;
}

View File

@ -21,32 +21,72 @@ namespace BiblioTech.CodexChecking
var httpFactory = CreateHttpFactory();
factory = new CodexNodeFactory(log, httpFactory, dataDir: config.DataPath);
Task.Run(CheckCodexNode);
}
public void OnCodex(Action<ICodexNode> action)
public T? OnCodex<T>(Func<ICodexNode, T> func) where T : class
{
lock (codexLock)
{
action(Get());
if (currentCodexNode == null) return null;
return func(currentCodexNode);
}
}
public T OnCodex<T>(Func<ICodexNode, T> func)
private void CheckCodexNode()
{
lock (codexLock)
Thread.Sleep(TimeSpan.FromSeconds(10.0));
while (true)
{
return func(Get());
lock (codexLock)
{
var newNode = GetNewCodexNode();
if (newNode != null && currentCodexNode == null) ShowConnectionRestored();
if (newNode == null && currentCodexNode != null) ShowConnectionLost();
currentCodexNode = newNode;
}
Thread.Sleep(TimeSpan.FromMinutes(15.0));
}
}
private ICodexNode Get()
private ICodexNode? GetNewCodexNode()
{
if (currentCodexNode == null)
try
{
currentCodexNode = CreateCodex();
}
if (currentCodexNode != null)
{
try
{
// Current instance is responsive? Keep it.
var info = currentCodexNode.GetDebugInfo();
if (info != null && info.Version != null &&
!string.IsNullOrEmpty(info.Version.Revision)) return currentCodexNode;
}
catch
{
}
}
return currentCodexNode;
return CreateCodex();
}
catch (Exception ex)
{
log.Error("Exception when trying to check codex node: " + ex.Message);
return null;
}
}
private void ShowConnectionLost()
{
Program.AdminChecker.SendInAdminChannel("Codex node connection lost.");
}
private void ShowConnectionRestored()
{
Program.AdminChecker.SendInAdminChannel("Codex node connection restored.");
}
private ICodexNode CreateCodex()
@ -70,16 +110,34 @@ namespace BiblioTech.CodexChecking
{
if (string.IsNullOrEmpty(config.CodexEndpointAuth) || !config.CodexEndpointAuth.Contains(":"))
{
return new HttpFactory(log);
return new HttpFactory(log, new SnappyTimeSet());
}
var tokens = config.CodexEndpointAuth.Split(':');
if (tokens.Length != 2) throw new Exception("Expected '<username>:<password>' in CodexEndpointAuth parameter.");
return new HttpFactory(log, onClientCreated: client =>
return new HttpFactory(log, new SnappyTimeSet(), onClientCreated: client =>
{
client.SetBasicAuthentication(tokens[0], tokens[1]);
});
}
public class SnappyTimeSet : IWebCallTimeSet
{
public TimeSpan HttpCallRetryDelay()
{
return TimeSpan.FromSeconds(1.0);
}
public TimeSpan HttpCallTimeout()
{
return TimeSpan.FromSeconds(3.0);
}
public TimeSpan HttpRetryTimeout()
{
return TimeSpan.FromSeconds(12.0);
}
}
}
}

View File

@ -6,7 +6,6 @@ using BiblioTech.Rewards;
using Logging;
using BiblioTech.CodexChecking;
using Nethereum.Model;
using static Org.BouncyCastle.Math.EC.ECCurve;
namespace BiblioTech
{

View File

@ -18,7 +18,7 @@ namespace BiblioTech.Commands
}
public override string Name => "checkdownload";
public override string StartingMessage => RandomBusyMessage.Get();
public override string StartingMessage => "Connecting to the testnet... Please be patient... " + RandomBusyMessage.Get();
public override string Description => "Checks the download connectivity of your Codex node.";
public override CommandOption[] Options => [contentOption];

View File

@ -26,6 +26,11 @@ namespace BiblioTech.Commands
await context.Followup("Could not download the CID.");
}
public async Task CodexUnavailable()
{
await context.Followup("Couldn't perform check: Our Codex node appears unavailable. Try again later?");
}
public async Task GiveCidToUser(string cid)
{
await context.Followup(

View File

@ -18,7 +18,7 @@ namespace BiblioTech.Commands
}
public override string Name => "checkupload";
public override string StartingMessage => RandomBusyMessage.Get();
public override string StartingMessage => "Connecting to the testnet... Please be patient... " + RandomBusyMessage.Get();
public override string Description => "Checks the upload connectivity of your Codex node.";
public override CommandOption[] Options => [cidOption];

View File

@ -93,12 +93,12 @@ namespace BiblioTech.Commands
private bool ShouldSendEth(IGethNode gethNode, EthAddress addr)
{
var eth = gethNode.GetEthBalance(addr);
return eth.Eth < Program.Config.SendEth;
return ((decimal)eth.Eth) < Program.Config.SendEth;
}
private string FormatTransactionLink(string transaction)
{
var url = $"https://explorer.testnet.codex.storage/tx/{transaction}";
var url = Program.Config.TransactionLinkFormat.Replace("<ID>", transaction);
return $"- [View on block explorer](<{url}>){Environment.NewLine}Transaction ID - `{transaction}`";
}
}

View File

@ -30,7 +30,7 @@ namespace BiblioTech
public int RewardApiPort { get; set; } = 31080;
[Uniform("send-eth", "se", "SENDETH", true, "Amount of Eth send by the mint command.")]
public int SendEth { get; set; } = 10;
public decimal SendEth { get; set; } = 10.0m;
[Uniform("mint-tt", "mt", "MINTTT", true, "Amount of TSTWEI minted by the mint command.")]
public BigInteger MintTT { get; set; } = 1073741824;
@ -44,6 +44,9 @@ namespace BiblioTech
[Uniform("codex-endpoint-auth", "cea", "CODEXENDPOINTAUTH", false, "Codex endpoint basic auth. Colon separated username and password. (default: empty, no auth used.)")]
public string CodexEndpointAuth { get; set; } = "";
[Uniform("transaction-link-format", "tlf", "TRANSACTIONLINKFORMAT", false, "Format of links to transactions on the blockchain. Use '<ID>' to inject the transaction ID into this string. (default 'https://explorer.testnet.codex.storage/tx/<ID>')")]
public string TransactionLinkFormat { get; set; } = "https://explorer.testnet.codex.storage/tx/<ID>";
#region Role Rewards
/// <summary>

View File

@ -4,10 +4,7 @@ using BiblioTech.Commands;
using BiblioTech.Rewards;
using Discord;
using Discord.WebSocket;
using DiscordRewards;
using Logging;
using Nethereum.Model;
using Newtonsoft.Json;
namespace BiblioTech
{
@ -88,7 +85,7 @@ namespace BiblioTech
client = new DiscordSocketClient();
client.Log += ClientLog;
var checkRepo = new CheckRepo(Config);
var checkRepo = new CheckRepo(Log, Config);
var codexWrapper = new CodexWrapper(Log, Config);
var checker = new CodexTwoWayChecker(Log, Config, checkRepo, codexWrapper);
var notifyCommand = new NotifyCommand();

View File

@ -9,9 +9,9 @@ FROM ${IMAGE} AS builder
ARG APP_HOME
WORKDIR ${APP_HOME}
COPY ./Tools/BiblioTech ./Tools/BiblioTech
COPY ./Framework ./Framework
COPY ./ProjectPlugins ./ProjectPlugins
COPY Tools/BiblioTech Tools/BiblioTech
COPY Framework Framework
COPY ProjectPlugins ProjectPlugins
RUN dotnet restore Tools/BiblioTech
RUN dotnet publish Tools/BiblioTech -c Release -o out
@ -23,4 +23,7 @@ ENV APP_HOME=${APP_HOME}
WORKDIR ${APP_HOME}
COPY --from=builder ${APP_HOME}/out .
CMD dotnet ${APP_HOME}/BiblioTech.dll
COPY --chmod=0755 Tools/BiblioTech/docker/docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["bash", "-c", "dotnet ${APP_HOME}/BiblioTech.dll"]

View File

@ -0,0 +1,30 @@
#!/bin/bash
# Marketplace address from URL
if [[ -n "${MARKETPLACE_ADDRESS_FROM_URL}" ]]; then
WAIT=${MARKETPLACE_ADDRESS_FROM_URL_WAIT:-300}
SECONDS=0
SLEEP=1
# Run and retry if fail
while (( SECONDS < WAIT )); do
MARKETPLACE_ADDRESS=($(curl -s -f -m 5 "${MARKETPLACE_ADDRESS_FROM_URL}"))
# Check if exit code is 0 and returned value is not empty
if [[ $? -eq 0 && -n "${MARKETPLACE_ADDRESS}" ]]; then
export CODEXCONTRACTS_MARKETPLACEADDRESS="${MARKETPLACE_ADDRESS}"
break
else
# Sleep and check again
echo "Can't get Marketplace address from ${MARKETPLACE_ADDRESS_FROM_URL} - Retry in $SLEEP seconds / $((WAIT - SECONDS))"
sleep $SLEEP
fi
done
fi
# Show
echo -e "\nRun parameters:"
vars=$(env | grep "CODEX" | grep -v -e "[0-9]_SERVICE_" -e "[0-9]_NODEPORT_")
echo -e "${vars//CODEX/ - CODEX}"
echo -e " - $@\n"
# Run
exec "$@"

View File

@ -1,6 +1,5 @@
using BlockchainUtils;
using CodexContractsPlugin.ChainMonitor;
using GethPlugin;
using Logging;
using System.Numerics;
using Utils;
@ -47,7 +46,7 @@ namespace MarketInsights
AddRequestToAverage(segment.Started, requestEvent);
}
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex)
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex, bool isRepair)
{
}

View File

@ -78,6 +78,7 @@
public string NewRequest => "🌱";
public string Started => "🌳";
public string SlotFilled => "🟢";
public string SlotRepaired => "♻";
public string SlotFreed => "⭕";
public string SlotReservationsFull => "☑️";
public string Finished => "✅";

View File

@ -1,8 +1,8 @@
using BlockchainUtils;
using CodexContractsPlugin;
using CodexContractsPlugin.ChainMonitor;
using CodexContractsPlugin.Marketplace;
using DiscordRewards;
using GethPlugin;
using Nethereum.Hex.HexConvertors.Extensions;
using System.Globalization;
using System.Numerics;
using Utils;
@ -16,10 +16,12 @@ namespace TestNetRewarder
private readonly List<string> errors = new List<string>();
private readonly EmojiMaps emojiMaps = new EmojiMaps();
private readonly Configuration config;
private readonly string periodDuration;
public EventsFormatter(Configuration config)
public EventsFormatter(Configuration config, MarketplaceConfig marketplaceConfig)
{
this.config = config;
periodDuration = Time.FormatDuration(marketplaceConfig.PeriodDuration);
}
public ChainEventMessage[] GetInitializationEvents(Configuration config)
@ -49,7 +51,7 @@ namespace TestNetRewarder
public void OnNewRequest(RequestEvent requestEvent)
{
var request = requestEvent.Request;
AddRequestBlock(requestEvent, $"{emojiMaps.NewRequest} New Request",
AddRequestBlock(requestEvent, emojiMaps.NewRequest, "New Request",
$"Client: {request.Client}",
$"Content: {BytesToHexString(request.Request.Content.Cid)}",
$"Duration: {BigIntToDuration(request.Request.Ask.Duration)}",
@ -58,33 +60,34 @@ namespace TestNetRewarder
$"PricePerBytePerSecond: {BitIntToTestTokens(request.Request.Ask.PricePerBytePerSecond)}",
$"Number of Slots: {request.Request.Ask.Slots}",
$"Slot Tolerance: {request.Request.Ask.MaxSlotLoss}",
$"Slot Size: {BigIntToByteSize(request.Request.Ask.SlotSize)}"
$"Slot Size: {BigIntToByteSize(request.Request.Ask.SlotSize)}",
$"Proof Probability: 1 / {request.Request.Ask.ProofProbability} every {periodDuration}"
);
}
public void OnRequestCancelled(RequestEvent requestEvent)
{
AddRequestBlock(requestEvent, $"{emojiMaps.Cancelled} Cancelled");
AddRequestBlock(requestEvent, emojiMaps.Cancelled, "Cancelled");
}
public void OnRequestFailed(RequestEvent requestEvent)
{
AddRequestBlock(requestEvent, $"{emojiMaps.Failed} Failed");
AddRequestBlock(requestEvent, emojiMaps.Failed, "Failed");
}
public void OnRequestFinished(RequestEvent requestEvent)
{
AddRequestBlock(requestEvent, $"{emojiMaps.Finished} Finished");
AddRequestBlock(requestEvent, emojiMaps.Finished, "Finished");
}
public void OnRequestFulfilled(RequestEvent requestEvent)
{
AddRequestBlock(requestEvent, $"{emojiMaps.Started} Started");
AddRequestBlock(requestEvent, emojiMaps.Started, "Started");
}
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex)
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex, bool isRepair)
{
AddRequestBlock(requestEvent, $"{emojiMaps.SlotFilled} Slot Filled",
AddRequestBlock(requestEvent, GetSlotFilledIcon(isRepair), GetSlotFilledTitle(isRepair),
$"Host: {host}",
$"Slot Index: {slotIndex}"
);
@ -92,14 +95,14 @@ namespace TestNetRewarder
public void OnSlotFreed(RequestEvent requestEvent, BigInteger slotIndex)
{
AddRequestBlock(requestEvent, $"{emojiMaps.SlotFreed} Slot Freed",
AddRequestBlock(requestEvent, emojiMaps.SlotFreed, "Slot Freed",
$"Slot Index: {slotIndex}"
);
}
public void OnSlotReservationsFull(RequestEvent requestEvent, BigInteger slotIndex)
{
AddRequestBlock(requestEvent, $"{emojiMaps.SlotReservationsFull} Slot Reservations Full",
AddRequestBlock(requestEvent, emojiMaps.SlotReservationsFull, "Slot Reservations Full",
$"Slot Index: {slotIndex}"
);
}
@ -133,6 +136,18 @@ namespace TestNetRewarder
AddBlock(0, $"{emojiMaps.ProofReport} **Proof system report**", lines.ToArray());
}
private string GetSlotFilledIcon(bool isRepair)
{
if (isRepair) return emojiMaps.SlotRepaired;
return emojiMaps.SlotFilled;
}
private string GetSlotFilledTitle(bool isRepair)
{
if (isRepair) return $"Slot Repaired";
return $"Slot Filled";
}
private void AddMissedProofDetails(List<string> lines, PeriodReport[] reports)
{
var reportsWithMissedProofs = reports.Where(r => r.MissedProofs.Length > 0).ToArray();
@ -168,10 +183,10 @@ namespace TestNetRewarder
lines.Add($"[{missedProof.FormatHost()}] missed proof for {FormatRequestId(missedProof.Request)} (slotIndex: {missedProof.SlotIndex})");
}
private void AddRequestBlock(RequestEvent requestEvent, string eventName, params string[] content)
private void AddRequestBlock(RequestEvent requestEvent, string icon, string eventName, params string[] content)
{
var blockNumber = $"[{requestEvent.Block.BlockNumber} {FormatDateTime(requestEvent.Block.Utc)}]";
var title = $"{blockNumber} **{eventName}** {FormatRequestId(requestEvent)}";
var title = $"{blockNumber} {icon} **{eventName}** {FormatRequestId(requestEvent)}";
AddBlock(requestEvent.Block.BlockNumber, title, content);
}
@ -223,14 +238,15 @@ namespace TestNetRewarder
private string FormatRequestId(IChainStateRequest request)
{
return FormatRequestId(request.Request.Id);
return FormatRequestId(request.RequestId);
}
private string FormatRequestId(string id)
private string FormatRequestId(byte[] id)
{
var str = id.ToHex();
return
$"({emojiMaps.StringToEmojis(id, 3)})" +
$"`{id}`";
$"({emojiMaps.StringToEmojis(str, 3)})" +
$"`{str}`";
}
private string BytesToHexString(byte[] bytes)

View File

@ -25,7 +25,7 @@ namespace TestNetRewarder
if (config.ProofReportHours < 1) throw new Exception("ProofReportHours must be one or greater");
builder = new RequestBuilder();
eventsFormatter = new EventsFormatter(config);
eventsFormatter = new EventsFormatter(config, contracts.Deployment.Config);
chainState = new ChainState(log, contracts, eventsFormatter, config.HistoryStartUtc,
doProofPeriodMonitoring: config.ShowProofPeriodReports > 0);

View File

@ -9,9 +9,9 @@ FROM ${IMAGE} AS builder
ARG APP_HOME
WORKDIR ${APP_HOME}
COPY ./Tools/TestNetRewarder ./Tools/TestNetRewarder
COPY ./Framework ./Framework
COPY ./ProjectPlugins ./ProjectPlugins
COPY Tools/TestNetRewarder Tools/TestNetRewarder
COPY Framework Framework
COPY ProjectPlugins ProjectPlugins
RUN dotnet restore Tools/TestNetRewarder
RUN dotnet publish Tools/TestNetRewarder -c Release -o out
@ -23,4 +23,7 @@ ENV APP_HOME=${APP_HOME}
WORKDIR ${APP_HOME}
COPY --from=builder ${APP_HOME}/out .
CMD dotnet ${APP_HOME}/TestNetRewarder.dll
COPY --chmod=0755 Tools/TestNetRewarder/docker/docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["bash", "-c", "dotnet ${APP_HOME}/TestNetRewarder.dll"]

View File

@ -0,0 +1,30 @@
#!/bin/bash
# Marketplace address from URL
if [[ -n "${MARKETPLACE_ADDRESS_FROM_URL}" ]]; then
WAIT=${MARKETPLACE_ADDRESS_FROM_URL_WAIT:-300}
SECONDS=0
SLEEP=1
# Run and retry if fail
while (( SECONDS < WAIT )); do
MARKETPLACE_ADDRESS=($(curl -s -f -m 5 "${MARKETPLACE_ADDRESS_FROM_URL}"))
# Check if exit code is 0 and returned value is not empty
if [[ $? -eq 0 && -n "${MARKETPLACE_ADDRESS}" ]]; then
export CODEXCONTRACTS_MARKETPLACEADDRESS="${MARKETPLACE_ADDRESS}"
break
else
# Sleep and check again
echo "Can't get Marketplace address from ${MARKETPLACE_ADDRESS_FROM_URL} - Retry in $SLEEP seconds / $((WAIT - SECONDS))"
sleep $SLEEP
fi
done
fi
# Show
echo -e "\nRun parameters:"
vars=$(env | grep "CODEX" | grep -v -e "[0-9]_SERVICE_" -e "[0-9]_NODEPORT_")
echo -e "${vars//CODEX/ - CODEX}"
echo -e " - $@\n"
# Run
exec "$@"

View File

@ -1,6 +1,7 @@
using System.Numerics;
using BlockchainUtils;
using CodexContractsPlugin.ChainMonitor;
using Nethereum.Hex.HexConvertors.Extensions;
using Utils;
namespace TraceContract
@ -70,11 +71,11 @@ namespace TraceContract
}
}
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex)
public void OnSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex, bool isRepair)
{
if (IsMyRequest(requestEvent))
{
output.LogSlotFilled(requestEvent, host, slotIndex);
output.LogSlotFilled(requestEvent, host, slotIndex, isRepair);
}
}
@ -96,7 +97,7 @@ namespace TraceContract
private bool IsMyRequest(RequestEvent requestEvent)
{
return requestId == requestEvent.Request.Request.Id.ToLowerInvariant();
return requestId == requestEvent.Request.RequestId.ToHex().ToLowerInvariant();
}
}

View File

@ -3,6 +3,7 @@ using CodexContractsPlugin.ChainMonitor;
using CodexContractsPlugin.Marketplace;
using Logging;
using Nethereum.Hex.HexConvertors.Extensions;
using Nethereum.Model;
using Utils;
namespace TraceContract
@ -28,10 +29,12 @@ namespace TraceContract
var request = GetRequest();
if (request == null) throw new Exception("Failed to find the purchase in the last week of transactions.");
log.Log($"Request started at {request.Block.Utc}");
var contractEnd = RunToContractEnd(request);
var creationEvent = FindRequestCreationEvent();
var requestTimeline = new TimeRange(request.Block.Utc.AddMinutes(-1.0), contractEnd.AddMinutes(1.0));
log.Log($"Request started at {creationEvent.Block.Utc}");
var contractEnd = RunToContractEnd(creationEvent);
var requestTimeline = new TimeRange(creationEvent.Block.Utc.AddMinutes(-1.0), contractEnd.AddMinutes(1.0));
log.Log($"Request timeline: {requestTimeline.From} -> {requestTimeline.To}");
// For this timeline, we log all the calls to reserve-slot.
@ -52,7 +55,7 @@ namespace TraceContract
return requestTimeline;
}
private DateTime RunToContractEnd(Request request)
private DateTime RunToContractEnd(StorageRequestedEventDTO request)
{
var utc = request.Block.Utc.AddMinutes(-1.0);
var tracker = new ChainRequestTracker(output, input.PurchaseId);
@ -78,35 +81,35 @@ namespace TraceContract
return tracker.FinishUtc;
}
private Request? GetRequest()
{
var request = FindRequest(LastHour());
if (request == null) request = FindRequest(LastDay());
if (request == null) request = FindRequest(LastWeek());
return request;
}
private Request? FindRequest(TimeRange timeRange)
{
var events = contracts.GetEvents(timeRange);
var requests = events.GetStorageRequests();
foreach (var r in requests)
{
if (IsThisRequest(r.RequestId))
{
return r;
}
}
return null;
}
private bool IsThisRequest(byte[] requestId)
{
return requestId.ToHex().ToLowerInvariant() == input.PurchaseId.ToLowerInvariant();
}
private Request? GetRequest()
{
return contracts.GetRequest(input.RequestId);
}
public StorageRequestedEventDTO FindRequestCreationEvent()
{
var range = new TimeRange(DateTime.UtcNow - TimeSpan.FromHours(3.0), DateTime.UtcNow);
var limit = DateTime.UtcNow - TimeSpan.FromDays(30);
while (range.From > limit)
{
var events = contracts.GetEvents(range);
foreach (var r in events.GetStorageRequestedEvents())
{
if (r.RequestId.ToHex() == input.RequestId.ToHex()) return r;
}
range = new TimeRange(range.From - TimeSpan.FromHours(3.0), range.From);
}
throw new Exception("Unable to find storage request creation event on-chain after (limit) " + Time.FormatTimestamp(limit));
}
private static TimeRange LastHour()
{
return new TimeRange(DateTime.UtcNow.AddHours(-1.0), DateTime.UtcNow);

File diff suppressed because one or more lines are too long

View File

@ -40,7 +40,7 @@ namespace TraceContract
var queryTemplate = CreateQueryTemplate(podName, startUtc, endUtc);
targetFile.Write($"Downloading '{podName}' to '{targetFile.Filename}'.");
var reconstructor = new LogReconstructor(targetFile, endpoint, queryTemplate);
var reconstructor = new LogReconstructor(log, targetFile, endpoint, queryTemplate);
reconstructor.DownloadFullLog();
log.Log("Log download finished.");
@ -91,6 +91,7 @@ namespace TraceContract
public class LogReconstructor
{
private readonly List<LogQueueEntry> queue = new List<LogQueueEntry>();
private readonly ILog log;
private readonly LogFile targetFile;
private readonly IEndpoint endpoint;
private readonly string queryTemplate;
@ -98,9 +99,11 @@ namespace TraceContract
private string searchAfter = "";
private int lastHits = 1;
private ulong? lastLogLine;
private uint linesWritten = 0;
public LogReconstructor(LogFile targetFile, IEndpoint endpoint, string queryTemplate)
public LogReconstructor(ILog log, LogFile targetFile, IEndpoint endpoint, string queryTemplate)
{
this.log = log;
this.targetFile = targetFile;
this.endpoint = endpoint;
this.queryTemplate = queryTemplate;
@ -113,6 +116,8 @@ namespace TraceContract
QueryElasticSearch();
ProcessQueue();
}
log.Log($"{linesWritten} lines written.");
}
private void QueryElasticSearch()
@ -124,6 +129,8 @@ namespace TraceContract
var response = endpoint.HttpPostString<SearchResponse>("/_search", query);
lastHits = response.hits.hits.Length;
log.Log($"pageSize: {sizeOfPage} after: {searchAfter} -> {lastHits} hits");
if (lastHits > 0)
{
UpdateSearchAfter(response);
@ -176,7 +183,7 @@ namespace TraceContract
private void ProcessQueue()
{
if (lastLogLine == null)
if (lastLogLine == null && queue.Any())
{
lastLogLine = queue.Min(q => q.Number) - 1;
}
@ -208,6 +215,7 @@ namespace TraceContract
private void WriteEntryToFile(LogQueueEntry currentEntry)
{
targetFile.Write(currentEntry.Message);
linesWritten++;
}
private void DeleteOldEntries(ulong wantedNumber)

View File

@ -1,4 +1,6 @@
namespace TraceContract
using Nethereum.Hex.HexConvertors.Extensions;
namespace TraceContract
{
public class Input
{
@ -17,5 +19,15 @@
//"066df09a3a2e2587cfd577a0e96186c915b113d02b331b06e56f808494cff2b4";
}
}
public byte[] RequestId
{
get
{
var r = PurchaseId.HexToByteArray();
if (r == null || r.Length != 32) throw new ArgumentException(nameof(PurchaseId));
return r;
}
}
}
}

View File

@ -1,7 +1,9 @@
using System.Numerics;
using BlockchainUtils;
using CodexContractsPlugin.ChainMonitor;
using CodexContractsPlugin.Marketplace;
using Logging;
using Newtonsoft.Json;
using Utils;
namespace TraceContract
@ -10,13 +12,13 @@ namespace TraceContract
{
private class Entry
{
public Entry(DateTime utc, string msg)
public Entry(BlockTimeEntry blk, string msg)
{
Utc = utc;
Blk = blk;
Msg = msg;
}
public DateTime Utc { get; }
public BlockTimeEntry Blk { get; }
public string Msg { get; }
}
@ -48,52 +50,48 @@ namespace TraceContract
public void LogRequestCreated(RequestEvent requestEvent)
{
Add(requestEvent.Block.Utc, $"Storage request created: '{requestEvent.Request.Request.Id}'");
var msg = $"Storage request created: '{requestEvent.Request.RequestId}' = {Environment.NewLine}${JsonConvert.SerializeObject(requestEvent.Request.Request, Formatting.Indented)}{Environment.NewLine}";
Add(requestEvent.Block, msg);
}
public void LogRequestCancelled(RequestEvent requestEvent)
{
Add(requestEvent.Block.Utc, "Expired");
Add(requestEvent.Block, "Expired");
}
public void LogRequestFailed(RequestEvent requestEvent)
{
Add(requestEvent.Block.Utc, "Failed");
Add(requestEvent.Block, "Failed");
}
public void LogRequestFinished(RequestEvent requestEvent)
{
Add(requestEvent.Block.Utc, "Finished");
Add(requestEvent.Block, "Finished");
}
public void LogRequestStarted(RequestEvent requestEvent)
{
Add(requestEvent.Block.Utc, "Started");
Add(requestEvent.Block, "Started");
}
public void LogSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex)
public void LogSlotFilled(RequestEvent requestEvent, EthAddress host, BigInteger slotIndex, bool isRepair)
{
Add(requestEvent.Block.Utc, $"Slot filled. Index: {slotIndex} Host: '{host}'");
Add(requestEvent.Block, $"Slot filled. Index: {slotIndex} Host: '{host}' isRepair: {isRepair}");
}
public void LogSlotFreed(RequestEvent requestEvent, BigInteger slotIndex)
{
Add(requestEvent.Block.Utc, $"Slot freed. Index: {slotIndex}");
Add(requestEvent.Block, $"Slot freed. Index: {slotIndex}");
}
public void LogSlotReservationsFull(RequestEvent requestEvent, BigInteger slotIndex)
{
Add(requestEvent.Block.Utc, $"Slot reservations full. Index: {slotIndex}");
}
public void LogReserveSlotCalls(ReserveSlotFunction[] reserveSlotFunctions)
{
foreach (var call in reserveSlotFunctions) LogReserveSlotCall(call);
Add(requestEvent.Block, $"Slot reservations full. Index: {slotIndex}");
}
public void WriteContractEvents()
{
var sorted = entries.OrderBy(e => e.Utc).ToArray();
var sorted = entries.OrderBy(e => e.Blk.Utc).ToArray();
foreach (var e in sorted) Write(e);
}
@ -111,17 +109,17 @@ namespace TraceContract
private void Write(Entry e)
{
log.Log($"[{Time.FormatTimestamp(e.Utc)}] {e.Msg}");
log.Log($"Block: {e.Blk.BlockNumber} [{Time.FormatTimestamp(e.Blk.Utc)}] {e.Msg}");
}
public void LogReserveSlotCall(ReserveSlotFunction call)
{
Add(call.Block.Utc, $"Reserve-slot called. Index: {call.SlotIndex} Host: '{call.FromAddress}'");
Add(call.Block, $"Reserve-slot called. Block: {call.Block.BlockNumber} Index: {call.SlotIndex} Host: '{call.FromAddress}'");
}
private void Add(DateTime utc, string msg)
private void Add(BlockTimeEntry blk, string msg)
{
entries.Add(new Entry(utc, msg));
entries.Add(new Entry(blk, msg));
}
}
}