Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
760d0211c6 | ||
|
|
32807f3f8c | ||
|
|
9ce82cb011 | ||
|
|
23396f54e0 | ||
|
|
d1a70d2465 | ||
|
|
65473d8c15 |
@@ -82,5 +82,5 @@ public static class CoreInterfaceExtensions
|
||||
}
|
||||
```
|
||||
|
||||
The primary reason to decouple deploying and wrapping functionalities is that some use cases require these steps to be performed by separate applications, and different moments in time. For this reason, whatever is returned by the deploy methods should be serializable. After deserialization at some later time, it should then be valid input for the wrap method. The Codex continuous tests system is a clear example of this use case: The `CodexNetDeployer` tool uses deploy methods to create Codex nodes. Then it writes the returned objects to a JSON file. Some time later, the `CodexContinousTests` application uses this JSON file to reconstruct the objects created by the deploy methods. It then uses the wrap methods to create accessors and interactors, which are used for testing.
|
||||
The primary reason to decouple deploying and wrapping functionalities is that some use cases require these steps to be performed by separate applications, and different moments in time. For this reason, whatever is returned by the deploy methods should be serializable. After deserialization at some later time, it should then be valid input for the wrap method. The Codex continuous tests system is a clear example of this use case: The `CodexNetDeployer` tool uses deploy methods to create Codex nodes. Then it writes the returned objects to a JSON file. Some time later, the `CodexContinuousTests` application uses this JSON file to reconstruct the objects created by the deploy methods. It then uses the wrap methods to create accessors and interactors, which are used for testing.
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ Do you want to write some tests using this distributed test setup? Great! Here's
|
||||
1. When using the auto-bootstrap, you have no control over the bootstrap node from your tests. You can't (for example) shut it down during the course of the test. If you need this level of control for your scenario, use the `CodexDistTest` instead.
|
||||
1. If your test needs a long time to run, add the `[UseLongTimeouts]` function attribute. This will greatly increase maximum time-out values for operations like for example uploading and downloading files.
|
||||
### Continuous tests
|
||||
1. Add new code files to `Tests/CodexContinousTests/Tests`
|
||||
1. Add new code files to `Tests/CodexContinuousTests/Tests`
|
||||
1. Inherrit from `ContinuousTest`
|
||||
1. Define one or more methods and decorate them with the `[TestMoment(...)]` attribute.
|
||||
1. The TestMoment takes a number of seconds as argument. Each moment will be executed by the continuous test runner applying the given seconds as delay. (Non-cumulative. So two moments at T:10 will be executed one after another without delay, in this case the order of execution should not be depended upon.)
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace Core
|
||||
{
|
||||
public interface IDownloadedLog
|
||||
{
|
||||
string[] GetLinesContaining(string expectedString);
|
||||
bool DoesLogContain(string expectedString);
|
||||
string[] FindLinesThatContain(params string[] tags);
|
||||
void DeleteFile();
|
||||
}
|
||||
@@ -18,23 +18,20 @@ namespace Core
|
||||
this.logFile = logFile;
|
||||
}
|
||||
|
||||
public string[] GetLinesContaining(string expectedString)
|
||||
public bool DoesLogContain(string expectedString)
|
||||
{
|
||||
using var file = File.OpenRead(logFile.FullFilename);
|
||||
using var streamReader = new StreamReader(file);
|
||||
var lines = new List<string>();
|
||||
|
||||
var line = streamReader.ReadLine();
|
||||
while (line != null)
|
||||
{
|
||||
if (line.Contains(expectedString))
|
||||
{
|
||||
lines.Add(line);
|
||||
}
|
||||
if (line.Contains(expectedString)) return true;
|
||||
line = streamReader.ReadLine();
|
||||
}
|
||||
|
||||
return lines.ToArray(); ;
|
||||
//Assert.Fail($"{owner} Unable to find string '{expectedString}' in CodexNode log file {logFile.FullFilename}");
|
||||
return false;
|
||||
}
|
||||
|
||||
public string[] FindLinesThatContain(params string[] tags)
|
||||
|
||||
@@ -49,14 +49,13 @@ namespace KubernetesWorkflow
|
||||
return CreatePodInfo(pod);
|
||||
}
|
||||
|
||||
public void Stop(StartResult startResult, bool waitTillStopped)
|
||||
public void Stop(StartResult startResult)
|
||||
{
|
||||
log.Debug();
|
||||
if (startResult.InternalService != null) DeleteService(startResult.InternalService);
|
||||
if (startResult.ExternalService != null) DeleteService(startResult.ExternalService);
|
||||
DeleteDeployment(startResult.Deployment);
|
||||
|
||||
if (waitTillStopped) WaitUntilPodsForDeploymentAreOffline(startResult.Deployment);
|
||||
WaitUntilPodsForDeploymentAreOffline(startResult.Deployment);
|
||||
}
|
||||
|
||||
public void DownloadPodLog(RunningContainer container, ILogHandler logHandler, int? tailLines)
|
||||
@@ -499,17 +498,10 @@ namespace KubernetesWorkflow
|
||||
Ports = CreateContainerPorts(recipe),
|
||||
Env = CreateEnv(recipe),
|
||||
VolumeMounts = CreateContainerVolumeMounts(recipe),
|
||||
Resources = CreateResourceLimits(recipe),
|
||||
Command = CreateCommandList(recipe)
|
||||
Resources = CreateResourceLimits(recipe)
|
||||
};
|
||||
}
|
||||
|
||||
private IList<string> CreateCommandList(ContainerRecipe recipe)
|
||||
{
|
||||
if (recipe.CommandOverride == null || !recipe.CommandOverride.Command.Any()) return null!;
|
||||
return recipe.CommandOverride.Command.ToList();
|
||||
}
|
||||
|
||||
private V1ResourceRequirements CreateResourceLimits(ContainerRecipe recipe)
|
||||
{
|
||||
return new V1ResourceRequirements
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace KubernetesWorkflow.Recipe
|
||||
{
|
||||
public class CommandOverride
|
||||
{
|
||||
public CommandOverride(params string[] command)
|
||||
{
|
||||
Command = command;
|
||||
}
|
||||
|
||||
public string[] Command { get; }
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,13 @@
|
||||
{
|
||||
public class ContainerRecipe
|
||||
{
|
||||
public ContainerRecipe(int number, string? nameOverride, string image, ContainerResources resources, SchedulingAffinity schedulingAffinity, CommandOverride commandOverride, bool setCriticalPriority, Port[] exposedPorts, Port[] internalPorts, EnvVar[] envVars, PodLabels podLabels, PodAnnotations podAnnotations, VolumeMount[] volumes, ContainerAdditionals additionals)
|
||||
public ContainerRecipe(int number, string? nameOverride, string image, ContainerResources resources, SchedulingAffinity schedulingAffinity, bool setCriticalPriority, Port[] exposedPorts, Port[] internalPorts, EnvVar[] envVars, PodLabels podLabels, PodAnnotations podAnnotations, VolumeMount[] volumes, ContainerAdditionals additionals)
|
||||
{
|
||||
Number = number;
|
||||
NameOverride = nameOverride;
|
||||
Image = image;
|
||||
Resources = resources;
|
||||
SchedulingAffinity = schedulingAffinity;
|
||||
CommandOverride = commandOverride;
|
||||
SetCriticalPriority = setCriticalPriority;
|
||||
ExposedPorts = exposedPorts;
|
||||
InternalPorts = internalPorts;
|
||||
@@ -36,7 +35,6 @@
|
||||
public string? NameOverride { get; }
|
||||
public ContainerResources Resources { get; }
|
||||
public SchedulingAffinity SchedulingAffinity { get; }
|
||||
public CommandOverride CommandOverride { get; }
|
||||
public bool SetCriticalPriority { get; }
|
||||
public string Image { get; }
|
||||
public Port[] ExposedPorts { get; }
|
||||
|
||||
@@ -14,7 +14,6 @@ namespace KubernetesWorkflow.Recipe
|
||||
private RecipeComponentFactory factory = null!;
|
||||
private ContainerResources resources = new ContainerResources();
|
||||
private SchedulingAffinity schedulingAffinity = new SchedulingAffinity();
|
||||
private CommandOverride commandOverride = new CommandOverride();
|
||||
private bool setCriticalPriority;
|
||||
|
||||
public ContainerRecipe CreateRecipe(int index, int containerNumber, RecipeComponentFactory factory, StartupConfig config)
|
||||
@@ -25,7 +24,7 @@ namespace KubernetesWorkflow.Recipe
|
||||
|
||||
Initialize(config);
|
||||
|
||||
var recipe = new ContainerRecipe(containerNumber, config.NameOverride, Image, resources, schedulingAffinity, commandOverride, setCriticalPriority,
|
||||
var recipe = new ContainerRecipe(containerNumber, config.NameOverride, Image, resources, schedulingAffinity, setCriticalPriority,
|
||||
exposedPorts.ToArray(),
|
||||
internalPorts.ToArray(),
|
||||
envVars.ToArray(),
|
||||
@@ -44,7 +43,6 @@ namespace KubernetesWorkflow.Recipe
|
||||
this.factory = null!;
|
||||
resources = new ContainerResources();
|
||||
schedulingAffinity = new SchedulingAffinity();
|
||||
commandOverride = new CommandOverride();
|
||||
setCriticalPriority = false;
|
||||
|
||||
return recipe;
|
||||
@@ -132,11 +130,6 @@ namespace KubernetesWorkflow.Recipe
|
||||
schedulingAffinity = new SchedulingAffinity(notIn);
|
||||
}
|
||||
|
||||
protected void OverrideCommand(params string[] command)
|
||||
{
|
||||
commandOverride = new CommandOverride(command);
|
||||
}
|
||||
|
||||
protected void SetSystemCriticalPriority()
|
||||
{
|
||||
setCriticalPriority = true;
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace KubernetesWorkflow
|
||||
PodInfo GetPodInfo(RunningContainer container);
|
||||
PodInfo GetPodInfo(RunningContainers containers);
|
||||
CrashWatcher CreateCrashWatcher(RunningContainer container);
|
||||
void Stop(RunningContainers containers, bool waitTillStopped);
|
||||
void Stop(RunningContainers containers);
|
||||
void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null);
|
||||
string ExecuteCommand(RunningContainer container, string command, params string[] args);
|
||||
void DeleteNamespace();
|
||||
@@ -86,11 +86,11 @@ namespace KubernetesWorkflow
|
||||
return K8s(c => c.CreateCrashWatcher(container));
|
||||
}
|
||||
|
||||
public void Stop(RunningContainers runningContainers, bool waitTillStopped)
|
||||
public void Stop(RunningContainers runningContainers)
|
||||
{
|
||||
K8s(controller =>
|
||||
{
|
||||
controller.Stop(runningContainers.StartResult, waitTillStopped);
|
||||
controller.Stop(runningContainers.StartResult);
|
||||
cluster.Configuration.Hooks.OnContainersStopped(runningContainers);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace NethereumWorkflow
|
||||
{
|
||||
public partial class BlockTimeFinder
|
||||
{
|
||||
public class BlockTimeEntry
|
||||
{
|
||||
public BlockTimeEntry(ulong blockNumber, DateTime utc)
|
||||
{
|
||||
BlockNumber = blockNumber;
|
||||
Utc = utc;
|
||||
}
|
||||
|
||||
public ulong BlockNumber { get; }
|
||||
public DateTime Utc { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{BlockNumber}] @ {Utc.ToString("o")}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
using Logging;
|
||||
using Nethereum.RPC.Eth.DTOs;
|
||||
using Nethereum.Web3;
|
||||
using Utils;
|
||||
|
||||
namespace NethereumWorkflow
|
||||
{
|
||||
public partial class BlockTimeFinder
|
||||
{
|
||||
private const ulong FetchRange = 6;
|
||||
private const int MaxEntries = 1024;
|
||||
private static readonly Dictionary<ulong, BlockTimeEntry> entries = new Dictionary<ulong, BlockTimeEntry>();
|
||||
private readonly Web3 web3;
|
||||
private readonly ILog log;
|
||||
|
||||
public BlockTimeFinder(Web3 web3, ILog log)
|
||||
{
|
||||
this.web3 = web3;
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
public ulong GetHighestBlockNumberBefore(DateTime moment)
|
||||
{
|
||||
log.Log("Looking for highest block before " + moment.ToString("o"));
|
||||
AssertMomentIsInPast(moment);
|
||||
Initialize();
|
||||
|
||||
return GetHighestBlockBefore(moment);
|
||||
}
|
||||
|
||||
public ulong GetLowestBlockNumberAfter(DateTime moment)
|
||||
{
|
||||
log.Log("Looking for lowest block after " + moment.ToString("o"));
|
||||
AssertMomentIsInPast(moment);
|
||||
Initialize();
|
||||
|
||||
return GetLowestBlockAfter(moment);
|
||||
}
|
||||
|
||||
private ulong GetHighestBlockBefore(DateTime moment)
|
||||
{
|
||||
var closestBefore = FindClosestBeforeEntry(moment);
|
||||
var closestAfter = FindClosestAfterEntry(moment);
|
||||
|
||||
if (closestBefore != null &&
|
||||
closestAfter != null &&
|
||||
closestBefore.Utc < moment &&
|
||||
closestAfter.Utc > moment &&
|
||||
closestBefore.BlockNumber + 1 == closestAfter.BlockNumber)
|
||||
{
|
||||
log.Log("Found highest-Before: " + closestBefore);
|
||||
return closestBefore.BlockNumber;
|
||||
}
|
||||
|
||||
FetchBlocksAround(moment);
|
||||
return GetHighestBlockBefore(moment);
|
||||
}
|
||||
|
||||
private ulong GetLowestBlockAfter(DateTime moment)
|
||||
{
|
||||
var closestBefore = FindClosestBeforeEntry(moment);
|
||||
var closestAfter = FindClosestAfterEntry(moment);
|
||||
|
||||
if (closestBefore != null &&
|
||||
closestAfter != null &&
|
||||
closestBefore.Utc < moment &&
|
||||
closestAfter.Utc > moment &&
|
||||
closestBefore.BlockNumber + 1 == closestAfter.BlockNumber)
|
||||
{
|
||||
log.Log("Found lowest-after: " + closestAfter);
|
||||
return closestAfter.BlockNumber;
|
||||
}
|
||||
|
||||
FetchBlocksAround(moment);
|
||||
return GetLowestBlockAfter(moment);
|
||||
}
|
||||
|
||||
private void FetchBlocksAround(DateTime moment)
|
||||
{
|
||||
var timePerBlock = EstimateTimePerBlock();
|
||||
log.Debug("Fetching blocks around " + moment.ToString("o") + " timePerBlock: " + timePerBlock.TotalSeconds);
|
||||
|
||||
EnsureRecentBlockIfNecessary(moment, timePerBlock);
|
||||
|
||||
var max = entries.Keys.Max();
|
||||
var blockDifference = CalculateBlockDifference(moment, timePerBlock, max);
|
||||
|
||||
FetchUp(max, blockDifference);
|
||||
FetchDown(max, blockDifference);
|
||||
}
|
||||
|
||||
private void FetchDown(ulong max, ulong blockDifference)
|
||||
{
|
||||
var target = max - blockDifference - 1;
|
||||
var fetchDown = FetchRange;
|
||||
while (fetchDown > 0)
|
||||
{
|
||||
if (!entries.ContainsKey(target))
|
||||
{
|
||||
var newBlock = AddBlockNumber(target);
|
||||
if (newBlock == null) return;
|
||||
fetchDown--;
|
||||
}
|
||||
target--;
|
||||
if (target <= 0) return;
|
||||
}
|
||||
}
|
||||
|
||||
private void FetchUp(ulong max, ulong blockDifference)
|
||||
{
|
||||
var target = max - blockDifference;
|
||||
var fetchUp = FetchRange;
|
||||
while (fetchUp > 0)
|
||||
{
|
||||
if (!entries.ContainsKey(target))
|
||||
{
|
||||
var newBlock = AddBlockNumber(target);
|
||||
if (newBlock == null) return;
|
||||
fetchUp--;
|
||||
}
|
||||
target++;
|
||||
if (target >= max) return;
|
||||
}
|
||||
}
|
||||
|
||||
private ulong CalculateBlockDifference(DateTime moment, TimeSpan timePerBlock, ulong max)
|
||||
{
|
||||
var latest = entries[max];
|
||||
var timeDifference = latest.Utc - moment;
|
||||
double secondsDifference = Math.Abs(timeDifference.TotalSeconds);
|
||||
double secondsPerBlock = timePerBlock.TotalSeconds;
|
||||
|
||||
double numberOfBlocksDifference = secondsDifference / secondsPerBlock;
|
||||
var blockDifference = Convert.ToUInt64(numberOfBlocksDifference);
|
||||
if (blockDifference < 1) blockDifference = 1;
|
||||
return blockDifference;
|
||||
}
|
||||
|
||||
private void EnsureRecentBlockIfNecessary(DateTime moment, TimeSpan timePerBlock)
|
||||
{
|
||||
var max = entries.Keys.Max();
|
||||
var latest = entries[max];
|
||||
var maxRetry = 10;
|
||||
while (moment > latest.Utc)
|
||||
{
|
||||
var newBlock = AddCurrentBlock();
|
||||
if (newBlock == null || newBlock.BlockNumber == latest.BlockNumber)
|
||||
{
|
||||
maxRetry--;
|
||||
if (maxRetry == 0) throw new Exception("Unable to fetch recent block after 10x tries.");
|
||||
Thread.Sleep(timePerBlock);
|
||||
}
|
||||
max = entries.Keys.Max();
|
||||
latest = entries[max];
|
||||
}
|
||||
}
|
||||
|
||||
private BlockTimeEntry? AddBlockNumber(decimal blockNumber)
|
||||
{
|
||||
return AddBlockNumber(Convert.ToUInt64(blockNumber));
|
||||
}
|
||||
|
||||
private BlockTimeEntry? AddBlockNumber(ulong blockNumber)
|
||||
{
|
||||
if (entries.ContainsKey(blockNumber))
|
||||
{
|
||||
return entries[blockNumber];
|
||||
}
|
||||
|
||||
if (entries.Count > MaxEntries)
|
||||
{
|
||||
log.Debug("Entries cleared!");
|
||||
entries.Clear();
|
||||
Initialize();
|
||||
}
|
||||
|
||||
var time = GetTimestampFromBlock(blockNumber);
|
||||
if (time == null)
|
||||
{
|
||||
log.Log("Failed to get block for number: " + blockNumber);
|
||||
return null;
|
||||
}
|
||||
var entry = new BlockTimeEntry(blockNumber, time.Value);
|
||||
log.Debug("Found block " + entry.BlockNumber + " at " + entry.Utc.ToString("o"));
|
||||
entries.Add(blockNumber, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
private TimeSpan EstimateTimePerBlock()
|
||||
{
|
||||
var min = entries.Keys.Min();
|
||||
var max = entries.Keys.Max();
|
||||
var clippedMin = Math.Max(max - 100, min);
|
||||
var minTime = entries[min].Utc;
|
||||
var clippedMinBlock = AddBlockNumber(clippedMin);
|
||||
if (clippedMinBlock != null) minTime = clippedMinBlock.Utc;
|
||||
|
||||
var maxTime = entries[max].Utc;
|
||||
var elapsedTime = maxTime - minTime;
|
||||
|
||||
double elapsedSeconds = elapsedTime.TotalSeconds;
|
||||
double numberOfBlocks = max - min;
|
||||
double secondsPerBlock = elapsedSeconds / numberOfBlocks;
|
||||
|
||||
var result = TimeSpan.FromSeconds(secondsPerBlock);
|
||||
if (result.TotalSeconds < 1.0) result = TimeSpan.FromSeconds(1.0);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
if (!entries.Any())
|
||||
{
|
||||
AddCurrentBlock();
|
||||
AddBlockNumber(entries.Single().Key - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertMomentIsInPast(DateTime moment)
|
||||
{
|
||||
if (moment > DateTime.UtcNow) throw new Exception("Moment must be UTC and must be in the past.");
|
||||
}
|
||||
|
||||
private BlockTimeEntry? AddCurrentBlock()
|
||||
{
|
||||
var number = Time.Wait(web3.Eth.Blocks.GetBlockNumber.SendRequestAsync());
|
||||
var blockNumber = number.ToDecimal();
|
||||
return AddBlockNumber(blockNumber);
|
||||
}
|
||||
|
||||
private DateTime? GetTimestampFromBlock(ulong blockNumber)
|
||||
{
|
||||
try
|
||||
{
|
||||
var block = Time.Wait(web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(new BlockParameter(blockNumber)));
|
||||
if (block == null) return null;
|
||||
return DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(block.Timestamp.ToDecimal())).UtcDateTime;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
int i = 0;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private BlockTimeEntry? FindClosestBeforeEntry(DateTime moment)
|
||||
{
|
||||
BlockTimeEntry? result = null;
|
||||
foreach (var entry in entries.Values)
|
||||
{
|
||||
if (result == null)
|
||||
{
|
||||
if (entry.Utc < moment) result = entry;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (entry.Utc > result.Utc && entry.Utc < moment) result = entry;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private BlockTimeEntry? FindClosestAfterEntry(DateTime moment)
|
||||
{
|
||||
BlockTimeEntry? result = null;
|
||||
foreach (var entry in entries.Values)
|
||||
{
|
||||
if (result == null)
|
||||
{
|
||||
if (entry.Utc > moment) result = entry;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (entry.Utc < result.Utc && entry.Utc > moment) result = entry;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
namespace NethereumWorkflow.BlockUtils
|
||||
{
|
||||
public class BlockCache
|
||||
{
|
||||
public delegate void CacheClearedEvent();
|
||||
|
||||
private const int MaxEntries = 1024;
|
||||
private readonly Dictionary<ulong, BlockTimeEntry> entries = new Dictionary<ulong, BlockTimeEntry>();
|
||||
|
||||
public event CacheClearedEvent? OnCacheCleared;
|
||||
|
||||
public BlockTimeEntry Add(ulong number, DateTime dateTime)
|
||||
{
|
||||
return Add(new BlockTimeEntry(number, dateTime));
|
||||
}
|
||||
|
||||
public BlockTimeEntry Add(BlockTimeEntry entry)
|
||||
{
|
||||
if (!entries.ContainsKey(entry.BlockNumber))
|
||||
{
|
||||
if (entries.Count > MaxEntries)
|
||||
{
|
||||
entries.Clear();
|
||||
var e = OnCacheCleared;
|
||||
if (e != null) e();
|
||||
}
|
||||
entries.Add(entry.BlockNumber, entry);
|
||||
}
|
||||
|
||||
return entries[entry.BlockNumber];
|
||||
}
|
||||
|
||||
public BlockTimeEntry? Get(ulong number)
|
||||
{
|
||||
if (!entries.TryGetValue(number, out BlockTimeEntry? value)) return null;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
namespace NethereumWorkflow.BlockUtils
|
||||
{
|
||||
public class BlockTimeEntry
|
||||
{
|
||||
public BlockTimeEntry(ulong blockNumber, DateTime utc)
|
||||
{
|
||||
BlockNumber = blockNumber;
|
||||
Utc = utc;
|
||||
}
|
||||
|
||||
public ulong BlockNumber { get; }
|
||||
public DateTime Utc { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{BlockNumber}] @ {Utc.ToString("o")}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
using Logging;
|
||||
|
||||
namespace NethereumWorkflow.BlockUtils
|
||||
{
|
||||
public class BlockTimeFinder
|
||||
{
|
||||
private readonly BlockCache cache;
|
||||
private readonly BlockchainBounds bounds;
|
||||
private readonly IWeb3Blocks web3;
|
||||
private readonly ILog log;
|
||||
|
||||
public BlockTimeFinder(BlockCache cache, IWeb3Blocks web3, ILog log)
|
||||
{
|
||||
this.web3 = web3;
|
||||
this.log = log;
|
||||
|
||||
this.cache = cache;
|
||||
bounds = new BlockchainBounds(cache, web3);
|
||||
}
|
||||
|
||||
public ulong? GetHighestBlockNumberBefore(DateTime moment)
|
||||
{
|
||||
bounds.Initialize();
|
||||
if (moment <= bounds.Genesis.Utc) return null;
|
||||
if (moment >= bounds.Current.Utc) return bounds.Current.BlockNumber;
|
||||
|
||||
return Search(bounds.Genesis, bounds.Current, moment, HighestBeforeSelector);
|
||||
}
|
||||
|
||||
public ulong? GetLowestBlockNumberAfter(DateTime moment)
|
||||
{
|
||||
bounds.Initialize();
|
||||
if (moment >= bounds.Current.Utc) return null;
|
||||
if (moment <= bounds.Genesis.Utc) return bounds.Genesis.BlockNumber;
|
||||
|
||||
return Search(bounds.Genesis, bounds.Current, moment, LowestAfterSelector);
|
||||
}
|
||||
|
||||
private ulong Search(BlockTimeEntry lower, BlockTimeEntry upper, DateTime target, Func<DateTime, BlockTimeEntry, bool> isWhatIwant)
|
||||
{
|
||||
var middle = GetMiddle(lower, upper);
|
||||
if (middle.BlockNumber == lower.BlockNumber)
|
||||
{
|
||||
if (isWhatIwant(target, upper)) return upper.BlockNumber;
|
||||
}
|
||||
|
||||
if (isWhatIwant(target, middle))
|
||||
{
|
||||
return middle.BlockNumber;
|
||||
}
|
||||
|
||||
if (middle.Utc > target)
|
||||
{
|
||||
return Search(lower, middle, target, isWhatIwant);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Search(middle, upper, target, isWhatIwant);
|
||||
}
|
||||
}
|
||||
|
||||
private BlockTimeEntry GetMiddle(BlockTimeEntry lower, BlockTimeEntry upper)
|
||||
{
|
||||
ulong range = upper.BlockNumber - lower.BlockNumber;
|
||||
ulong number = lower.BlockNumber + range / 2;
|
||||
return GetBlock(number);
|
||||
}
|
||||
|
||||
private bool HighestBeforeSelector(DateTime target, BlockTimeEntry entry)
|
||||
{
|
||||
var next = GetBlock(entry.BlockNumber + 1);
|
||||
return
|
||||
entry.Utc < target &&
|
||||
next.Utc > target;
|
||||
}
|
||||
|
||||
private bool LowestAfterSelector(DateTime target, BlockTimeEntry entry)
|
||||
{
|
||||
var previous = GetBlock(entry.BlockNumber - 1);
|
||||
return
|
||||
entry.Utc > target &&
|
||||
previous.Utc < target;
|
||||
}
|
||||
|
||||
private BlockTimeEntry GetBlock(ulong number)
|
||||
{
|
||||
if (number < bounds.Genesis.BlockNumber) throw new Exception("Can't fetch block before genesis.");
|
||||
if (number > bounds.Current.BlockNumber) throw new Exception("Can't fetch block after current.");
|
||||
|
||||
var dateTime = web3.GetTimestampForBlock(number);
|
||||
if (dateTime == null) throw new Exception("Failed to get dateTime for block that should exist.");
|
||||
return cache.Add(number, dateTime.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
namespace NethereumWorkflow.BlockUtils
|
||||
{
|
||||
public class BlockchainBounds
|
||||
{
|
||||
private readonly BlockCache cache;
|
||||
private readonly IWeb3Blocks web3;
|
||||
|
||||
public BlockTimeEntry Genesis { get; private set; } = null!;
|
||||
public BlockTimeEntry Current { get; private set; } = null!;
|
||||
|
||||
public BlockchainBounds(BlockCache cache, IWeb3Blocks web3)
|
||||
{
|
||||
this.cache = cache;
|
||||
this.web3 = web3;
|
||||
|
||||
cache.OnCacheCleared += Initialize;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
AddCurrentBlock();
|
||||
LookForGenesisBlock();
|
||||
|
||||
if (Current.BlockNumber == Genesis.BlockNumber)
|
||||
{
|
||||
throw new Exception("Unsupported condition: Current block is genesis block.");
|
||||
}
|
||||
}
|
||||
|
||||
private void LookForGenesisBlock()
|
||||
{
|
||||
if (Genesis != null)
|
||||
{
|
||||
cache.Add(Genesis);
|
||||
return;
|
||||
}
|
||||
|
||||
var blockTime = web3.GetTimestampForBlock(0);
|
||||
if (blockTime != null)
|
||||
{
|
||||
AddGenesisBlock(0, blockTime.Value);
|
||||
return;
|
||||
}
|
||||
|
||||
LookForGenesisBlock(0, Current);
|
||||
}
|
||||
|
||||
private void LookForGenesisBlock(ulong lower, BlockTimeEntry upper)
|
||||
{
|
||||
if (Genesis != null) return;
|
||||
|
||||
var range = upper.BlockNumber - lower;
|
||||
if (range == 1)
|
||||
{
|
||||
var lowTime = web3.GetTimestampForBlock(lower);
|
||||
if (lowTime != null)
|
||||
{
|
||||
AddGenesisBlock(lower, lowTime.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddGenesisBlock(upper);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var current = lower + range / 2;
|
||||
|
||||
var blockTime = web3.GetTimestampForBlock(current);
|
||||
if (blockTime != null)
|
||||
{
|
||||
var newUpper = cache.Add(current, blockTime.Value);
|
||||
LookForGenesisBlock(lower, newUpper);
|
||||
}
|
||||
else
|
||||
{
|
||||
LookForGenesisBlock(current, upper);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddCurrentBlock()
|
||||
{
|
||||
var currentBlockNumber = web3.GetCurrentBlockNumber();
|
||||
var blockTime = web3.GetTimestampForBlock(currentBlockNumber);
|
||||
if (blockTime == null) throw new Exception("Unable to get dateTime for current block.");
|
||||
AddCurrentBlock(currentBlockNumber, blockTime.Value);
|
||||
}
|
||||
|
||||
private void AddCurrentBlock(ulong currentBlockNumber, DateTime dateTime)
|
||||
{
|
||||
Current = new BlockTimeEntry(currentBlockNumber, dateTime);
|
||||
cache.Add(Current);
|
||||
}
|
||||
|
||||
private void AddGenesisBlock(ulong number, DateTime dateTime)
|
||||
{
|
||||
AddGenesisBlock(new BlockTimeEntry(number, dateTime));
|
||||
}
|
||||
|
||||
private void AddGenesisBlock(BlockTimeEntry entry)
|
||||
{
|
||||
Genesis = entry;
|
||||
cache.Add(Genesis);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,13 @@ using Nethereum.ABI.FunctionEncoding.Attributes;
|
||||
using Nethereum.Contracts;
|
||||
using Nethereum.RPC.Eth.DTOs;
|
||||
using Nethereum.Web3;
|
||||
using NethereumWorkflow.BlockUtils;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Utils;
|
||||
|
||||
namespace NethereumWorkflow
|
||||
{
|
||||
public class NethereumInteraction
|
||||
{
|
||||
// BlockCache is a static instance: It stays alive for the duration of the application runtime.
|
||||
private readonly static BlockCache blockCache = new BlockCache();
|
||||
|
||||
private readonly ILog log;
|
||||
private readonly Web3 web3;
|
||||
|
||||
@@ -91,24 +88,12 @@ namespace NethereumWorkflow
|
||||
|
||||
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, TimeRange timeRange) where TEvent : IEventDTO, new()
|
||||
{
|
||||
var wrapper = new Web3Wrapper(web3, log);
|
||||
var blockTimeFinder = new BlockTimeFinder(blockCache, wrapper, log);
|
||||
var blockTimeFinder = new BlockTimeFinder(web3, log);
|
||||
|
||||
var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(timeRange.From);
|
||||
var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(timeRange.To);
|
||||
|
||||
if (!fromBlock.HasValue)
|
||||
{
|
||||
log.Error("Failed to find lowest block for time range: " + timeRange);
|
||||
throw new Exception("Failed");
|
||||
}
|
||||
if (!toBlock.HasValue)
|
||||
{
|
||||
log.Error("Failed to find highest block for time range: " + timeRange);
|
||||
throw new Exception("Failed");
|
||||
}
|
||||
|
||||
return GetEvents<TEvent>(address, fromBlock.Value, toBlock.Value);
|
||||
return GetEvents<TEvent>(address, fromBlock, toBlock);
|
||||
}
|
||||
|
||||
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new()
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
using Logging;
|
||||
using Nethereum.RPC.Eth.DTOs;
|
||||
using Nethereum.Web3;
|
||||
using Utils;
|
||||
|
||||
namespace NethereumWorkflow
|
||||
{
|
||||
public interface IWeb3Blocks
|
||||
{
|
||||
ulong GetCurrentBlockNumber();
|
||||
DateTime? GetTimestampForBlock(ulong blockNumber);
|
||||
}
|
||||
|
||||
public class Web3Wrapper : IWeb3Blocks
|
||||
{
|
||||
private readonly Web3 web3;
|
||||
private readonly ILog log;
|
||||
|
||||
public Web3Wrapper(Web3 web3, ILog log)
|
||||
{
|
||||
this.web3 = web3;
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
public ulong GetCurrentBlockNumber()
|
||||
{
|
||||
var number = Time.Wait(web3.Eth.Blocks.GetBlockNumber.SendRequestAsync());
|
||||
return Convert.ToUInt64(number.ToDecimal());
|
||||
}
|
||||
|
||||
public DateTime? GetTimestampForBlock(ulong blockNumber)
|
||||
{
|
||||
try
|
||||
{
|
||||
var block = Time.Wait(web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(new BlockParameter(blockNumber)));
|
||||
if (block == null) return null;
|
||||
return DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(block.Timestamp.ToDecimal())).UtcDateTime;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Error("Exception while getting timestamp for block: " + ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace CodexContractsPlugin
|
||||
{
|
||||
public class CodexContractsContainerRecipe : ContainerRecipeFactory
|
||||
{
|
||||
public static string DockerImage { get; } = "codexstorage/codex-contracts-eth:latest-dist-tests";
|
||||
public static string DockerImage { get; } = "codexstorage/codex-contracts-eth:sha-965529d-dist-tests";
|
||||
|
||||
public const string MarketplaceAddressFilename = "/hardhat/deployments/codexdisttestnetwork/Marketplace.json";
|
||||
public const string MarketplaceArtifactFilename = "/hardhat/artifacts/contracts/Marketplace.sol/Marketplace.json";
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace CodexContractsPlugin
|
||||
try
|
||||
{
|
||||
var result = DeployContract(container, workflow, gethNode);
|
||||
workflow.Stop(containers, waitTillStopped: false);
|
||||
workflow.Stop(containers);
|
||||
Log("Container stopped.");
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace CodexPlugin
|
||||
{
|
||||
private readonly MarketplaceStarter marketplaceStarter = new MarketplaceStarter();
|
||||
|
||||
private const string DefaultDockerImage = "codexstorage/nim-codex:sha-e4ddb94-dist-tests";
|
||||
private const string DefaultDockerImage = "codexstorage/nim-codex:latest-dist-tests";
|
||||
public const string ApiPortTag = "codex_api_port";
|
||||
public const string ListenPortTag = "codex_listen_port";
|
||||
public const string MetricsPortTag = "codex_metrics_port";
|
||||
@@ -105,6 +105,7 @@ namespace CodexPlugin
|
||||
|
||||
AddEnvVar("CODEX_ETH_PROVIDER", $"{wsAddress.Host.Replace("http://", "ws://")}:{wsAddress.Port}");
|
||||
AddEnvVar("CODEX_MARKETPLACE_ADDRESS", marketplaceAddress);
|
||||
AddEnvVar("CODEX_PERSISTENCE", "true");
|
||||
|
||||
// Custom scripting in the Codex test image will write this variable to a private-key file,
|
||||
// and pass the correct filename to Codex.
|
||||
@@ -112,9 +113,7 @@ namespace CodexPlugin
|
||||
AddEnvVar("PRIV_KEY", mStart.PrivateKey);
|
||||
Additional(mStart);
|
||||
|
||||
var marketplaceSetup = config.MarketplaceConfig.MarketplaceSetup;
|
||||
SetCommandOverride(marketplaceSetup);
|
||||
if (marketplaceSetup.IsValidator)
|
||||
if (config.MarketplaceConfig.IsValidator)
|
||||
{
|
||||
AddEnvVar("CODEX_VALIDATOR", "true");
|
||||
}
|
||||
@@ -126,18 +125,6 @@ namespace CodexPlugin
|
||||
}
|
||||
}
|
||||
|
||||
private void SetCommandOverride(MarketplaceSetup ms)
|
||||
{
|
||||
if (ms.IsStorageNode)
|
||||
{
|
||||
OverrideCommand("bash", "/docker-entrypoint.sh", "codex", "persistence", "prover");
|
||||
}
|
||||
else
|
||||
{
|
||||
OverrideCommand("bash", "/docker-entrypoint.sh", "codex", "persistence");
|
||||
}
|
||||
}
|
||||
|
||||
private Port CreateApiPort(CodexStartupConfig config, string tag)
|
||||
{
|
||||
if (config.PublicTestNet == null) return AddExposedPort(tag);
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace CodexPlugin
|
||||
public CodexDeployment(CodexInstance[] codexInstances, GethDeployment gethDeployment,
|
||||
CodexContractsDeployment codexContractsDeployment, RunningContainers? prometheusContainer,
|
||||
RunningContainers? discordBotContainer, DeploymentMetadata metadata,
|
||||
String id)
|
||||
string id)
|
||||
{
|
||||
Id = id;
|
||||
CodexInstances = codexInstances;
|
||||
@@ -20,7 +20,7 @@ namespace CodexPlugin
|
||||
Metadata = metadata;
|
||||
}
|
||||
|
||||
public String Id { get; }
|
||||
public string Id { get; }
|
||||
public CodexInstance[] CodexInstances { get; }
|
||||
public GethDeployment GethDeployment { get; }
|
||||
public CodexContractsDeployment CodexContractsDeployment { get; }
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace CodexPlugin
|
||||
CrashWatcher CrashWatcher { get; }
|
||||
PodInfo GetPodInfo();
|
||||
ITransferSpeeds TransferSpeeds { get; }
|
||||
void Stop(bool waitTillStopped);
|
||||
void Stop();
|
||||
}
|
||||
|
||||
public class CodexNode : ICodexNode
|
||||
@@ -153,13 +153,13 @@ namespace CodexPlugin
|
||||
return CodexAccess.GetPodInfo();
|
||||
}
|
||||
|
||||
public void Stop(bool waitTillStopped)
|
||||
public void Stop()
|
||||
{
|
||||
if (Group.Count() > 1) throw new InvalidOperationException("Codex-nodes that are part of a group cannot be " +
|
||||
"individually shut down. Use 'BringOffline()' on the group object to stop the group. This method is only " +
|
||||
"available for codex-nodes in groups of 1.");
|
||||
|
||||
Group.BringOffline(waitTillStopped);
|
||||
Group.BringOffline();
|
||||
}
|
||||
|
||||
public void EnsureOnlineGetVersionResponse()
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace CodexPlugin
|
||||
{
|
||||
public interface ICodexNodeGroup : IEnumerable<ICodexNode>, IHasManyMetricScrapeTargets
|
||||
{
|
||||
void BringOffline(bool waitTillStopped);
|
||||
void BringOffline();
|
||||
ICodexNode this[int index] { get; }
|
||||
}
|
||||
|
||||
@@ -31,9 +31,9 @@ namespace CodexPlugin
|
||||
}
|
||||
}
|
||||
|
||||
public void BringOffline(bool waitTillStopped)
|
||||
public void BringOffline()
|
||||
{
|
||||
starter.BringOffline(this, waitTillStopped);
|
||||
starter.BringOffline(this);
|
||||
// Clear everything. Prevent accidental use.
|
||||
Nodes = Array.Empty<CodexNode>();
|
||||
Containers = null!;
|
||||
|
||||
@@ -17,8 +17,7 @@ namespace CodexPlugin
|
||||
ICodexSetup WithBlockMaintenanceInterval(TimeSpan duration);
|
||||
ICodexSetup WithBlockMaintenanceNumber(int numberOfBlocks);
|
||||
ICodexSetup EnableMetrics();
|
||||
ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens);
|
||||
ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens, Action<IMarketplaceSetup> marketplaceSetup);
|
||||
ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens, bool isValidator = false);
|
||||
/// <summary>
|
||||
/// Provides an invalid proof every N proofs
|
||||
/// </summary>
|
||||
@@ -26,12 +25,6 @@ namespace CodexPlugin
|
||||
ICodexSetup AsPublicTestNet(CodexTestNetConfig testNetConfig);
|
||||
}
|
||||
|
||||
public interface IMarketplaceSetup
|
||||
{
|
||||
IMarketplaceSetup AsStorageNode();
|
||||
IMarketplaceSetup AsValidator();
|
||||
}
|
||||
|
||||
public class CodexLogCustomTopics
|
||||
{
|
||||
public CodexLogCustomTopics(CodexLogLevel discV5, CodexLogLevel libp2p, CodexLogLevel blockExchange)
|
||||
@@ -122,17 +115,9 @@ namespace CodexPlugin
|
||||
return this;
|
||||
}
|
||||
|
||||
public ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens)
|
||||
public ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens, bool isValidator = false)
|
||||
{
|
||||
return EnableMarketplace(gethNode, codexContracts, initialEth, initialTokens, s => { });
|
||||
}
|
||||
|
||||
public ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens, Action<IMarketplaceSetup> marketplaceSetup)
|
||||
{
|
||||
var ms = new MarketplaceSetup();
|
||||
marketplaceSetup(ms);
|
||||
|
||||
MarketplaceConfig = new MarketplaceInitialConfig(ms, gethNode, codexContracts, initialEth, initialTokens);
|
||||
MarketplaceConfig = new MarketplaceInitialConfig(gethNode, codexContracts, initialEth, initialTokens, isValidator);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -161,35 +146,7 @@ namespace CodexPlugin
|
||||
if (BootstrapSpr != null) yield return $"BootstrapNode={BootstrapSpr}";
|
||||
if (StorageQuota != null) yield return $"StorageQuota={StorageQuota}";
|
||||
if (SimulateProofFailures != null) yield return $"SimulateProofFailures={SimulateProofFailures}";
|
||||
if (MarketplaceConfig != null) yield return $"MarketplaceSetup={MarketplaceConfig.MarketplaceSetup}";
|
||||
if (MarketplaceConfig != null) yield return $"IsValidator={MarketplaceConfig.IsValidator}";
|
||||
}
|
||||
}
|
||||
|
||||
public class MarketplaceSetup : IMarketplaceSetup
|
||||
{
|
||||
public bool IsStorageNode { get; private set; }
|
||||
public bool IsValidator { get; private set; }
|
||||
|
||||
public IMarketplaceSetup AsStorageNode()
|
||||
{
|
||||
IsStorageNode = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public IMarketplaceSetup AsValidator()
|
||||
{
|
||||
IsValidator = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var result = "[(clientNode)"; // When marketplace is enabled, being a clientNode is implicit.
|
||||
result += IsStorageNode ? "(storageNode)" : "()";
|
||||
result += IsValidator ? "(validator)" : "()";
|
||||
result += "]";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -48,14 +48,14 @@ namespace CodexPlugin
|
||||
return group;
|
||||
}
|
||||
|
||||
public void BringOffline(CodexNodeGroup group, bool waitTillStopped)
|
||||
public void BringOffline(CodexNodeGroup group)
|
||||
{
|
||||
Log($"Stopping {group.Describe()}...");
|
||||
StopCrashWatcher(group);
|
||||
var workflow = pluginTools.CreateWorkflow();
|
||||
foreach (var c in group.Containers)
|
||||
{
|
||||
workflow.Stop(c, waitTillStopped);
|
||||
workflow.Stop(c);
|
||||
}
|
||||
Log("Stopped.");
|
||||
}
|
||||
|
||||
@@ -68,17 +68,10 @@ namespace CodexPlugin
|
||||
"blockexcnetwork",
|
||||
"blockexcnetworkpeer"
|
||||
};
|
||||
var contractClockTopics = new[]
|
||||
{
|
||||
"contracts",
|
||||
"clock"
|
||||
};
|
||||
|
||||
level = $"{level};" +
|
||||
$"{CustomTopics.DiscV5.ToString()!.ToLowerInvariant()}:{string.Join(",", discV5Topics)};" +
|
||||
$"{CustomTopics.Libp2p.ToString()!.ToLowerInvariant()}:{string.Join(",", libp2pTopics)};" +
|
||||
// Contract clock is always set to warn. It logs a trace every second.
|
||||
$"{CodexLogLevel.Warn.ToString().ToLowerInvariant()}:{string.Join(",", contractClockTopics)}";
|
||||
$"{CustomTopics.Libp2p.ToString()!.ToLowerInvariant()}:{string.Join(",", libp2pTopics)}";
|
||||
|
||||
if (CustomTopics.BlockExchange != null)
|
||||
{
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
using Logging;
|
||||
using CodexContractsPlugin;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Utils;
|
||||
using System.Numerics;
|
||||
|
||||
namespace CodexPlugin
|
||||
{
|
||||
public interface IMarketplaceAccess
|
||||
{
|
||||
string MakeStorageAvailable(StorageAvailability availability);
|
||||
StoragePurchaseContract RequestStorage(StoragePurchase purchase);
|
||||
string MakeStorageAvailable(ByteSize size, TestToken minPriceForTotalSpace, TestToken maxCollateral, TimeSpan maxDuration);
|
||||
StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration);
|
||||
StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration, TimeSpan expiry);
|
||||
}
|
||||
|
||||
public class MarketplaceAccess : IMarketplaceAccess
|
||||
@@ -21,12 +24,35 @@ namespace CodexPlugin
|
||||
this.codexAccess = codexAccess;
|
||||
}
|
||||
|
||||
public StoragePurchaseContract RequestStorage(StoragePurchase purchase)
|
||||
public StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration)
|
||||
{
|
||||
purchase.Log(log);
|
||||
var request = purchase.ToApiRequest();
|
||||
return RequestStorage(contentId, pricePerSlotPerSecond, requiredCollateral, minRequiredNumberOfNodes, proofProbability, duration, duration / 2);
|
||||
}
|
||||
|
||||
var response = codexAccess.RequestStorage(request, purchase.ContentId.Id);
|
||||
public StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration, TimeSpan expiry)
|
||||
{
|
||||
var expireUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + expiry.TotalSeconds;
|
||||
|
||||
var request = new CodexSalesRequestStorageRequest
|
||||
{
|
||||
duration = ToDecInt(duration.TotalSeconds),
|
||||
proofProbability = ToDecInt(proofProbability),
|
||||
reward = ToDecInt(pricePerSlotPerSecond),
|
||||
collateral = ToDecInt(requiredCollateral),
|
||||
expiry = ToDecInt(expireUtc),
|
||||
nodes = minRequiredNumberOfNodes,
|
||||
tolerance = null,
|
||||
};
|
||||
|
||||
Log($"Requesting storage for: {contentId.Id}... (" +
|
||||
$"pricePerSlotPerSecond: {pricePerSlotPerSecond}, " +
|
||||
$"requiredCollateral: {requiredCollateral}, " +
|
||||
$"minRequiredNumberOfNodes: {minRequiredNumberOfNodes}, " +
|
||||
$"proofProbability: {proofProbability}, " +
|
||||
$"expiry: {Time.FormatDuration(expiry)}, " +
|
||||
$"duration: {Time.FormatDuration(duration)})");
|
||||
|
||||
var response = codexAccess.RequestStorage(request, contentId.Id);
|
||||
|
||||
if (response == "Purchasing not available" ||
|
||||
response == "Expiry required" ||
|
||||
@@ -38,13 +64,24 @@ namespace CodexPlugin
|
||||
|
||||
Log($"Storage requested successfully. PurchaseId: '{response}'.");
|
||||
|
||||
return new StoragePurchaseContract(log, codexAccess, response, purchase);
|
||||
return new StoragePurchaseContract(log, codexAccess, response, duration);
|
||||
}
|
||||
|
||||
public string MakeStorageAvailable(StorageAvailability availability)
|
||||
public string MakeStorageAvailable(ByteSize totalSpace, TestToken minPriceForTotalSpace, TestToken maxCollateral, TimeSpan maxDuration)
|
||||
{
|
||||
availability.Log(log);
|
||||
var request = availability.ToApiRequest();
|
||||
var request = new CodexSalesAvailabilityRequest
|
||||
{
|
||||
size = ToDecInt(totalSpace.SizeInBytes),
|
||||
duration = ToDecInt(maxDuration.TotalSeconds),
|
||||
maxCollateral = ToDecInt(maxCollateral),
|
||||
minPrice = ToDecInt(minPriceForTotalSpace)
|
||||
};
|
||||
|
||||
Log($"Making storage available... (" +
|
||||
$"size: {totalSpace}, " +
|
||||
$"minPriceForTotalSpace: {minPriceForTotalSpace}, " +
|
||||
$"maxCollateral: {maxCollateral}, " +
|
||||
$"maxDuration: {Time.FormatDuration(maxDuration)})");
|
||||
|
||||
var response = codexAccess.SalesAvailability(request);
|
||||
|
||||
@@ -53,6 +90,18 @@ namespace CodexPlugin
|
||||
return response.id;
|
||||
}
|
||||
|
||||
private string ToDecInt(double d)
|
||||
{
|
||||
var i = new BigInteger(d);
|
||||
return i.ToString("D");
|
||||
}
|
||||
|
||||
public string ToDecInt(TestToken t)
|
||||
{
|
||||
var i = new BigInteger(t.Amount);
|
||||
return i.ToString("D");
|
||||
}
|
||||
|
||||
private void Log(string msg)
|
||||
{
|
||||
log.Log($"{codexAccess.Container.Name} {msg}");
|
||||
@@ -61,16 +110,22 @@ namespace CodexPlugin
|
||||
|
||||
public class MarketplaceUnavailable : IMarketplaceAccess
|
||||
{
|
||||
public string MakeStorageAvailable(StorageAvailability availability)
|
||||
public StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerBytePerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration)
|
||||
{
|
||||
Unavailable();
|
||||
return null!;
|
||||
}
|
||||
|
||||
public StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration, TimeSpan expiry)
|
||||
{
|
||||
Unavailable();
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public StoragePurchaseContract RequestStorage(StoragePurchase purchase)
|
||||
public string MakeStorageAvailable(ByteSize size, TestToken minPricePerBytePerSecond, TestToken maxCollateral, TimeSpan duration)
|
||||
{
|
||||
Unavailable();
|
||||
throw new NotImplementedException();
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private void Unavailable()
|
||||
@@ -84,26 +139,22 @@ namespace CodexPlugin
|
||||
{
|
||||
private readonly ILog log;
|
||||
private readonly CodexAccess codexAccess;
|
||||
private readonly TimeSpan gracePeriod = TimeSpan.FromSeconds(10);
|
||||
private DateTime? contractStartUtc;
|
||||
|
||||
public StoragePurchaseContract(ILog log, CodexAccess codexAccess, string purchaseId, StoragePurchase purchase)
|
||||
public StoragePurchaseContract(ILog log, CodexAccess codexAccess, string purchaseId, TimeSpan contractDuration)
|
||||
{
|
||||
this.log = log;
|
||||
this.codexAccess = codexAccess;
|
||||
PurchaseId = purchaseId;
|
||||
Purchase = purchase;
|
||||
ContractDuration = contractDuration;
|
||||
}
|
||||
|
||||
public string PurchaseId { get; }
|
||||
public StoragePurchase Purchase { get; }
|
||||
public TimeSpan ContractDuration { get; }
|
||||
|
||||
public void WaitForStorageContractStarted()
|
||||
{
|
||||
var timeout = Purchase.Expiry + gracePeriod;
|
||||
|
||||
WaitForStorageContractState(timeout, "started");
|
||||
contractStartUtc = DateTime.UtcNow;
|
||||
WaitForStorageContractStarted(TimeSpan.FromSeconds(30));
|
||||
}
|
||||
|
||||
public void WaitForStorageContractFinished()
|
||||
@@ -112,14 +163,39 @@ namespace CodexPlugin
|
||||
{
|
||||
WaitForStorageContractStarted();
|
||||
}
|
||||
var gracePeriod = TimeSpan.FromSeconds(10);
|
||||
var currentContractTime = DateTime.UtcNow - contractStartUtc!.Value;
|
||||
var timeout = (Purchase.Duration - currentContractTime) + gracePeriod;
|
||||
var timeout = (ContractDuration - currentContractTime) + gracePeriod;
|
||||
WaitForStorageContractState(timeout, "finished");
|
||||
}
|
||||
|
||||
public CodexStoragePurchase GetPurchaseStatus(string purchaseId)
|
||||
public void WaitForStorageContractFinished(ByteSize contractFileSize)
|
||||
{
|
||||
return codexAccess.GetPurchaseStatus(purchaseId);
|
||||
if (!contractStartUtc.HasValue)
|
||||
{
|
||||
WaitForStorageContractStarted(contractFileSize.ToTimeSpan());
|
||||
}
|
||||
var gracePeriod = TimeSpan.FromSeconds(10);
|
||||
var currentContractTime = DateTime.UtcNow - contractStartUtc!.Value;
|
||||
var timeout = (ContractDuration - currentContractTime) + gracePeriod;
|
||||
WaitForStorageContractState(timeout, "finished");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for contract to start. Max timeout depends on contract filesize. Allows more time for larger files.
|
||||
/// </summary>
|
||||
public void WaitForStorageContractStarted(ByteSize contractFileSize)
|
||||
{
|
||||
var filesizeInMb = contractFileSize.SizeInBytes / (1024 * 1024);
|
||||
var maxWaitTime = TimeSpan.FromSeconds(filesizeInMb * 10.0);
|
||||
|
||||
WaitForStorageContractStarted(maxWaitTime);
|
||||
}
|
||||
|
||||
public void WaitForStorageContractStarted(TimeSpan timeout)
|
||||
{
|
||||
WaitForStorageContractState(timeout, "started");
|
||||
contractStartUtc = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
private void WaitForStorageContractState(TimeSpan timeout, string desiredState)
|
||||
@@ -152,5 +228,10 @@ namespace CodexPlugin
|
||||
}
|
||||
log.Log($"Contract '{desiredState}'.");
|
||||
}
|
||||
|
||||
public CodexStoragePurchase GetPurchaseStatus(string purchaseId)
|
||||
{
|
||||
return codexAccess.GetPurchaseStatus(purchaseId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,19 +5,19 @@ namespace CodexPlugin
|
||||
{
|
||||
public class MarketplaceInitialConfig
|
||||
{
|
||||
public MarketplaceInitialConfig(MarketplaceSetup marketplaceSetup, IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens)
|
||||
public MarketplaceInitialConfig(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens, bool isValidator)
|
||||
{
|
||||
MarketplaceSetup = marketplaceSetup;
|
||||
GethNode = gethNode;
|
||||
CodexContracts = codexContracts;
|
||||
InitialEth = initialEth;
|
||||
InitialTokens = initialTokens;
|
||||
IsValidator = isValidator;
|
||||
}
|
||||
|
||||
public MarketplaceSetup MarketplaceSetup { get; }
|
||||
public IGethNode GethNode { get; }
|
||||
public ICodexContracts CodexContracts { get; }
|
||||
public Ether InitialEth { get; }
|
||||
public TestToken InitialTokens { get; }
|
||||
public bool IsValidator { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
using CodexContractsPlugin;
|
||||
using Logging;
|
||||
using System.Numerics;
|
||||
using Utils;
|
||||
|
||||
namespace CodexPlugin
|
||||
{
|
||||
public class StoragePurchase : MarketplaceType
|
||||
{
|
||||
public StoragePurchase(ContentId cid)
|
||||
{
|
||||
ContentId = cid;
|
||||
}
|
||||
|
||||
public ContentId ContentId { get; set; }
|
||||
public TestToken PricePerSlotPerSecond { get; set; } = 1.TestTokens();
|
||||
public TestToken RequiredCollateral { get; set; } = 1.TestTokens();
|
||||
public uint MinRequiredNumberOfNodes { get; set; }
|
||||
public uint NodeFailureTolerance { get; set; }
|
||||
public int ProofProbability { get; set; }
|
||||
public TimeSpan Duration { get; set; }
|
||||
public TimeSpan Expiry { get; set; }
|
||||
|
||||
public CodexSalesRequestStorageRequest ToApiRequest()
|
||||
{
|
||||
return new CodexSalesRequestStorageRequest
|
||||
{
|
||||
duration = ToDecInt(Duration.TotalSeconds),
|
||||
proofProbability = ToDecInt(ProofProbability),
|
||||
reward = ToDecInt(PricePerSlotPerSecond),
|
||||
collateral = ToDecInt(RequiredCollateral),
|
||||
expiry = ToDecInt(DateTimeOffset.UtcNow.ToUnixTimeSeconds() + Expiry.TotalSeconds),
|
||||
nodes = MinRequiredNumberOfNodes,
|
||||
tolerance = NodeFailureTolerance
|
||||
};
|
||||
}
|
||||
|
||||
public void Log(ILog log)
|
||||
{
|
||||
log.Log($"Requesting storage for: {ContentId.Id}... (" +
|
||||
$"pricePerSlotPerSecond: {PricePerSlotPerSecond}, " +
|
||||
$"requiredCollateral: {RequiredCollateral}, " +
|
||||
$"minRequiredNumberOfNodes: {MinRequiredNumberOfNodes}, " +
|
||||
$"nodeFailureTolerance: {NodeFailureTolerance}, " +
|
||||
$"proofProbability: {ProofProbability}, " +
|
||||
$"expiry: {Time.FormatDuration(Expiry)}, " +
|
||||
$"duration: {Time.FormatDuration(Duration)})");
|
||||
}
|
||||
}
|
||||
|
||||
public class StorageAvailability : MarketplaceType
|
||||
{
|
||||
public StorageAvailability(ByteSize totalSpace, TimeSpan maxDuration, TestToken minPriceForTotalSpace, TestToken maxCollateral)
|
||||
{
|
||||
TotalSpace = totalSpace;
|
||||
MaxDuration = maxDuration;
|
||||
MinPriceForTotalSpace = minPriceForTotalSpace;
|
||||
MaxCollateral = maxCollateral;
|
||||
}
|
||||
|
||||
public ByteSize TotalSpace { get; }
|
||||
public TimeSpan MaxDuration { get; }
|
||||
public TestToken MinPriceForTotalSpace { get; }
|
||||
public TestToken MaxCollateral { get; }
|
||||
|
||||
public CodexSalesAvailabilityRequest ToApiRequest()
|
||||
{
|
||||
return new CodexSalesAvailabilityRequest
|
||||
{
|
||||
size = ToDecInt(TotalSpace.SizeInBytes),
|
||||
duration = ToDecInt(MaxDuration.TotalSeconds),
|
||||
maxCollateral = ToDecInt(MaxCollateral),
|
||||
minPrice = ToDecInt(MinPriceForTotalSpace)
|
||||
};
|
||||
}
|
||||
|
||||
public void Log(ILog log)
|
||||
{
|
||||
log.Log($"Making storage available... (" +
|
||||
$"size: {TotalSpace}, " +
|
||||
$"maxDuration: {Time.FormatDuration(MaxDuration)}, " +
|
||||
$"minPriceForTotalSpace: {MinPriceForTotalSpace}, " +
|
||||
$"maxCollateral: {MaxCollateral})");
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class MarketplaceType
|
||||
{
|
||||
protected string ToDecInt(double d)
|
||||
{
|
||||
var i = new BigInteger(d);
|
||||
return i.ToString("D");
|
||||
}
|
||||
|
||||
protected string ToDecInt(TestToken t)
|
||||
{
|
||||
var i = new BigInteger(t.Amount);
|
||||
return i.ToString("D");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ Nethereum: v4.14.0
|
||||
These are test assemblies that use NUnit3 to perform tests against transient Codex nodes.
|
||||
Read more [HERE](/Tests/CodexTests/README.md)
|
||||
|
||||
## Tests/ContinousTests
|
||||
## Tests/ContinuousTests
|
||||
A console application that runs tests in an endless loop against a persistent deployment of Codex nodes.
|
||||
Read more [HERE](/Tests/CodexContinuousTests/README.md)
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using CodexPlugin;
|
||||
using CodexContractsPlugin;
|
||||
using CodexPlugin;
|
||||
using Core;
|
||||
using DistTestCore;
|
||||
using FileUtils;
|
||||
using GethPlugin;
|
||||
using Logging;
|
||||
using MetricsPlugin;
|
||||
|
||||
@@ -32,7 +33,7 @@ namespace ContinuousTests
|
||||
|
||||
if (nodes != null)
|
||||
{
|
||||
NodeRunner = new NodeRunner(Nodes, configuration, Log, CustomK8sNamespace);
|
||||
NodeRunner = new NodeRunner(Nodes, configuration, Log);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -57,15 +58,23 @@ namespace ContinuousTests
|
||||
{
|
||||
if (Configuration.CodexDeployment.PrometheusContainer == null) throw new Exception("Expected prometheus to be part of Codex deployment.");
|
||||
|
||||
var entryPointFactory = new EntryPointFactory();
|
||||
var entryPoint = entryPointFactory.CreateEntryPoint(Configuration.KubeConfigFile, Configuration.DataPath, Configuration.CodexDeployment.Metadata.KubeNamespace, Log);
|
||||
return entryPoint.CreateInterface().WrapMetricsCollector(Configuration.CodexDeployment.PrometheusContainer, target);
|
||||
var ci = CreateCi();
|
||||
return ci.WrapMetricsCollector(Configuration.CodexDeployment.PrometheusContainer, target);
|
||||
}
|
||||
|
||||
public (IGethNode, ICodexContracts) CreateMarketplaceAccess()
|
||||
{
|
||||
var ci = CreateCi();
|
||||
|
||||
var geth = ci.WrapGethDeployment(Configuration.CodexDeployment.GethDeployment);
|
||||
var contracts = ci.WrapCodexContractsDeployment(geth, Configuration.CodexDeployment.CodexContractsDeployment);
|
||||
|
||||
return (geth, contracts);
|
||||
}
|
||||
|
||||
public abstract int RequiredNumberOfNodes { get; }
|
||||
public abstract TimeSpan RunTestEvery { get; }
|
||||
public abstract TestFailMode TestFailMode { get; }
|
||||
public virtual string CustomK8sNamespace { get { return string.Empty; } }
|
||||
|
||||
public string Name
|
||||
{
|
||||
@@ -74,6 +83,13 @@ namespace ContinuousTests
|
||||
return GetType().Name;
|
||||
}
|
||||
}
|
||||
|
||||
private CoreInterface CreateCi()
|
||||
{
|
||||
var entryPointFactory = new EntryPointFactory();
|
||||
var entryPoint = entryPointFactory.CreateEntryPoint(Configuration.KubeConfigFile, Configuration.DataPath, Configuration.CodexDeployment.Metadata.KubeNamespace, Log);
|
||||
return entryPoint.CreateInterface();
|
||||
}
|
||||
}
|
||||
|
||||
public enum TestFailMode
|
||||
|
||||
@@ -51,8 +51,6 @@ namespace ContinuousTests
|
||||
overviewLog.Log("");
|
||||
var allTests = testFactory.CreateTests();
|
||||
|
||||
ClearAllCustomNamespaces(allTests, overviewLog);
|
||||
|
||||
var filteredTests = FilterTests(allTests, overviewLog);
|
||||
if (!filteredTests.Any())
|
||||
{
|
||||
@@ -136,21 +134,6 @@ namespace ContinuousTests
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ClearAllCustomNamespaces(ContinuousTest[] allTests, ILog log)
|
||||
{
|
||||
foreach (var test in allTests) ClearAllCustomNamespaces(test, log);
|
||||
}
|
||||
|
||||
private void ClearAllCustomNamespaces(ContinuousTest test, ILog log)
|
||||
{
|
||||
if (string.IsNullOrEmpty(test.CustomK8sNamespace)) return;
|
||||
|
||||
log.Log($"Clearing namespace '{test.CustomK8sNamespace}'...");
|
||||
|
||||
var entryPoint = entryPointFactory.CreateEntryPoint(config.KubeConfigFile, config.DataPath, test.CustomK8sNamespace, log);
|
||||
entryPoint.Tools.CreateWorkflow().DeleteNamespacesStartingWith(test.CustomK8sNamespace);
|
||||
}
|
||||
|
||||
private void PerformCleanup(ILog log)
|
||||
{
|
||||
if (!config.Cleanup) return;
|
||||
|
||||
@@ -25,7 +25,6 @@ namespace ContinuousTests
|
||||
);
|
||||
|
||||
return new EntryPoint(log, lifecycleConfig, dataFilePath);
|
||||
//DefaultContainerRecipe.TestsType = "continuous-tests";
|
||||
}
|
||||
|
||||
private static string? GetKubeConfig(string kubeConfigFile)
|
||||
|
||||
@@ -13,14 +13,12 @@ namespace ContinuousTests
|
||||
private readonly ICodexNode[] nodes;
|
||||
private readonly Configuration config;
|
||||
private readonly ILog log;
|
||||
private readonly string customNamespace;
|
||||
|
||||
public NodeRunner(ICodexNode[] nodes, Configuration config, ILog log, string customNamespace)
|
||||
public NodeRunner(ICodexNode[] nodes, Configuration config, ILog log)
|
||||
{
|
||||
this.nodes = nodes;
|
||||
this.config = config;
|
||||
this.log = log;
|
||||
this.customNamespace = customNamespace;
|
||||
}
|
||||
|
||||
public IDownloadedLog DownloadLog(RunningContainer container, int? tailLines = null)
|
||||
@@ -41,36 +39,34 @@ namespace ContinuousTests
|
||||
// Therefore, we use the image of the bootstrap node.
|
||||
CodexContainerRecipe.DockerImageOverride = bootstrapNode.Container.Recipe.Image;
|
||||
|
||||
var debugInfo = bootstrapNode.GetDebugInfo();
|
||||
Assert.That(!string.IsNullOrEmpty(debugInfo.spr));
|
||||
|
||||
var node = entryPoint.CreateInterface().StartCodexNode(s =>
|
||||
{
|
||||
setup(s);
|
||||
s.WithBootstrapNode(bootstrapNode);
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var debugInfo = bootstrapNode.GetDebugInfo();
|
||||
Assert.That(!string.IsNullOrEmpty(debugInfo.spr));
|
||||
|
||||
var node = entryPoint.CreateInterface().StartCodexNode(s =>
|
||||
{
|
||||
setup(s);
|
||||
s.WithBootstrapNode(bootstrapNode);
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
operation(node);
|
||||
}
|
||||
catch
|
||||
{
|
||||
DownloadLog(node.Container);
|
||||
throw;
|
||||
}
|
||||
operation(node);
|
||||
}
|
||||
catch
|
||||
{
|
||||
DownloadLog(node.Container);
|
||||
node.Stop();
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
entryPoint.Tools.CreateWorkflow().DeleteNamespace();
|
||||
node.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
private EntryPoint CreateEntryPoint()
|
||||
{
|
||||
return entryPointFactory.CreateEntryPoint(config.KubeConfigFile, config.DataPath, customNamespace, log);
|
||||
return entryPointFactory.CreateEntryPoint(config.KubeConfigFile, config.DataPath, config.CodexDeployment.Metadata.KubeNamespace, log);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("Codex Continous-Test-Runner.");
|
||||
Console.WriteLine("Codex Continuous-Test-Runner.");
|
||||
|
||||
var runner = new ContinuousTestRunner(args, Cancellation.Cts.Token);
|
||||
|
||||
|
||||
@@ -1,64 +1,68 @@
|
||||
//using DistTestCore;
|
||||
//using DistTestCore.Codex;
|
||||
//using Newtonsoft.Json;
|
||||
//using NUnit.Framework;
|
||||
//using Utils;
|
||||
using CodexContractsPlugin;
|
||||
using CodexPlugin;
|
||||
using FileUtils;
|
||||
using NUnit.Framework;
|
||||
using Utils;
|
||||
|
||||
//namespace ContinuousTests.Tests
|
||||
//{
|
||||
// public class MarketplaceTest : ContinuousTest
|
||||
// {
|
||||
// public override int RequiredNumberOfNodes => 1;
|
||||
// public override TimeSpan RunTestEvery => TimeSpan.FromMinutes(10);
|
||||
// public override TestFailMode TestFailMode => TestFailMode.StopAfterFirstFailure;
|
||||
// public override int EthereumAccountIndex => 200;
|
||||
// public override string CustomK8sNamespace => "codex-continuous-marketplace";
|
||||
|
||||
// private readonly uint numberOfSlots = 3;
|
||||
// private readonly ByteSize fileSize = 10.MB();
|
||||
// private readonly TestToken pricePerSlotPerSecond = 10.TestTokens();
|
||||
/// manual test locally.
|
||||
|
||||
// private TestFile file = null!;
|
||||
// private ContentId? cid;
|
||||
// private string purchaseId = string.Empty;
|
||||
|
||||
// [TestMoment(t: Zero)]
|
||||
// public void NodePostsStorageRequest()
|
||||
// {
|
||||
// var contractDuration = TimeSpan.FromMinutes(8);
|
||||
// decimal totalDurationSeconds = Convert.ToDecimal(contractDuration.TotalSeconds);
|
||||
// var expectedTotalCost = numberOfSlots * pricePerSlotPerSecond.Amount * (totalDurationSeconds + 1) * 1000000;
|
||||
namespace ContinuousTests.Tests
|
||||
{
|
||||
public class MarketplaceTest : ContinuousTest
|
||||
{
|
||||
public override int RequiredNumberOfNodes => 1;
|
||||
public override TimeSpan RunTestEvery => TimeSpan.FromMinutes(10);
|
||||
public override TestFailMode TestFailMode => TestFailMode.StopAfterFirstFailure;
|
||||
|
||||
// file = FileManager.GenerateTestFile(fileSize);
|
||||
private readonly uint numberOfSlots = 3;
|
||||
private readonly ByteSize fileSize = 10.MB();
|
||||
private readonly TestToken pricePerSlotPerSecond = 10.TestTokens();
|
||||
|
||||
// NodeRunner.RunNode((codexAccess, marketplaceAccess) =>
|
||||
// {
|
||||
// cid = UploadFile(codexAccess.Node, file);
|
||||
// Assert.That(cid, Is.Not.Null);
|
||||
private TrackedFile file = null!;
|
||||
private ContentId? cid;
|
||||
private string purchaseId = string.Empty;
|
||||
|
||||
// purchaseId = marketplaceAccess.RequestStorage(
|
||||
// contentId: cid!,
|
||||
// pricePerSlotPerSecond: pricePerSlotPerSecond,
|
||||
// requiredCollateral: 100.TestTokens(),
|
||||
// minRequiredNumberOfNodes: numberOfSlots,
|
||||
// proofProbability: 10,
|
||||
// duration: contractDuration);
|
||||
[TestMoment(t: Zero)]
|
||||
public void NodePostsStorageRequest()
|
||||
{
|
||||
var contractDuration = TimeSpan.FromMinutes(8);
|
||||
decimal totalDurationSeconds = Convert.ToDecimal(contractDuration.TotalSeconds);
|
||||
var expectedTotalCost = numberOfSlots * pricePerSlotPerSecond.Amount * (totalDurationSeconds + 1) * 1000000;
|
||||
|
||||
// Assert.That(!string.IsNullOrEmpty(purchaseId));
|
||||
file = FileManager.GenerateFile(fileSize);
|
||||
|
||||
// WaitForContractToStart(codexAccess, purchaseId);
|
||||
// });
|
||||
// }
|
||||
NodeRunner.RunNode(
|
||||
s => s.WithName("Buyer"),
|
||||
node =>
|
||||
{
|
||||
cid = node.UploadFile(file);
|
||||
Assert.That(cid, Is.Not.Null);
|
||||
|
||||
// [TestMoment(t: MinuteFive + MinuteOne)]
|
||||
// public void StoredDataIsAvailableAfterThreeDays()
|
||||
// {
|
||||
// NodeRunner.RunNode((codexAccess, marketplaceAccess) =>
|
||||
// {
|
||||
// var result = DownloadFile(codexAccess.Node, cid!);
|
||||
purchaseId = node.Marketplace.RequestStorage(
|
||||
contentId: cid!,
|
||||
pricePerSlotPerSecond: pricePerSlotPerSecond,
|
||||
requiredCollateral: 100.TestTokens(),
|
||||
minRequiredNumberOfNodes: numberOfSlots,
|
||||
proofProbability: 10,
|
||||
duration: contractDuration);
|
||||
|
||||
// file.AssertIsEqual(result);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
Assert.That(!string.IsNullOrEmpty(purchaseId));
|
||||
|
||||
WaitForContractToStart(codexAccess, purchaseId);
|
||||
});
|
||||
}
|
||||
|
||||
[TestMoment(t: MinuteFive + MinuteOne)]
|
||||
public void StoredDataIsAvailableAfterThreeDays()
|
||||
{
|
||||
NodeRunner.RunNode((codexAccess, marketplaceAccess) =>
|
||||
{
|
||||
var result = DownloadFile(codexAccess.Node, cid!);
|
||||
|
||||
file.AssertIsEqual(result);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,51 @@
|
||||
//using DistTestCore;
|
||||
//using DistTestCore.Codex;
|
||||
//using NUnit.Framework;
|
||||
using CodexPlugin;
|
||||
using FileUtils;
|
||||
using NUnit.Framework;
|
||||
using Utils;
|
||||
|
||||
//namespace ContinuousTests.Tests
|
||||
//{
|
||||
// public class TransientNodeTest : ContinuousTest
|
||||
// {
|
||||
// public override int RequiredNumberOfNodes => 3;
|
||||
// public override TimeSpan RunTestEvery => TimeSpan.FromMinutes(1);
|
||||
// public override TestFailMode TestFailMode => TestFailMode.StopAfterFirstFailure;
|
||||
// public override string CustomK8sNamespace => nameof(TransientNodeTest).ToLowerInvariant();
|
||||
// public override int EthereumAccountIndex => 201;
|
||||
namespace ContinuousTests.Tests
|
||||
{
|
||||
public class TransientNodeTest : ContinuousTest
|
||||
{
|
||||
public override int RequiredNumberOfNodes => 3;
|
||||
public override TimeSpan RunTestEvery => TimeSpan.FromMinutes(1);
|
||||
public override TestFailMode TestFailMode => TestFailMode.StopAfterFirstFailure;
|
||||
public override string CustomK8sNamespace => nameof(TransientNodeTest).ToLowerInvariant();
|
||||
|
||||
// private TestFile file = null!;
|
||||
// private ContentId cid = null!;
|
||||
private TrackedFile file = null!;
|
||||
private ContentId cid = null!;
|
||||
|
||||
// private CodexAccess UploadBootstapNode { get { return Nodes[0]; } }
|
||||
// private CodexAccess DownloadBootstapNode { get { return Nodes[1]; } }
|
||||
// private CodexAccess IntermediateNode { get { return Nodes[2]; } }
|
||||
private ICodexNode UploadBootstapNode { get { return Nodes[0]; } }
|
||||
private ICodexNode DownloadBootstapNode { get { return Nodes[1]; } }
|
||||
private ICodexNode IntermediateNode { get { return Nodes[2]; } }
|
||||
|
||||
// [TestMoment(t: 0)]
|
||||
// public void UploadWithTransientNode()
|
||||
// {
|
||||
// file = FileManager.GenerateTestFile(10.MB());
|
||||
[TestMoment(t: 0)]
|
||||
public void UploadWithTransientNode()
|
||||
{
|
||||
file = FileManager.GenerateFile(10.MB());
|
||||
|
||||
// NodeRunner.RunNode(UploadBootstapNode, (codexAccess, marketplaceAccess, lifecycle) =>
|
||||
// {
|
||||
// cid = UploadFile(codexAccess, file)!;
|
||||
// Assert.That(cid, Is.Not.Null);
|
||||
NodeRunner.RunNode(UploadBootstapNode,
|
||||
s => s.WithName("TransientUploader"),
|
||||
node =>
|
||||
{
|
||||
cid = node.UploadFile(file);
|
||||
Assert.That(cid, Is.Not.Null);
|
||||
|
||||
// var dlt = Task.Run(() =>
|
||||
// {
|
||||
// Thread.Sleep(10000);
|
||||
// lifecycle.DownloadLog(codexAccess.Container);
|
||||
// });
|
||||
var resultFile = IntermediateNode.DownloadContent(cid);
|
||||
file.AssertIsEqual(resultFile);
|
||||
});
|
||||
}
|
||||
|
||||
// var resultFile = DownloadFile(IntermediateNode, cid);
|
||||
// dlt.Wait();
|
||||
// file.AssertIsEqual(resultFile);
|
||||
// });
|
||||
// }
|
||||
|
||||
// [TestMoment(t: 30)]
|
||||
// public void DownloadWithTransientNode()
|
||||
// {
|
||||
// NodeRunner.RunNode(DownloadBootstapNode, (codexAccess, marketplaceAccess, lifecycle) =>
|
||||
// {
|
||||
// var resultFile = DownloadFile(codexAccess, cid);
|
||||
// file.AssertIsEqual(resultFile);
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
[TestMoment(t: MinuteOne)]
|
||||
public void DownloadWithTransientNode()
|
||||
{
|
||||
NodeRunner.RunNode(DownloadBootstapNode,
|
||||
s => s.WithName("TransientDownloader"),
|
||||
node =>
|
||||
{
|
||||
var resultFile = node.DownloadContent(cid);
|
||||
file.AssertIsEqual(resultFile);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using CodexPlugin;
|
||||
using FileUtils;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
using Utils;
|
||||
|
||||
@@ -22,9 +21,6 @@ namespace ContinuousTests.Tests
|
||||
[TestMoment(t: Zero)]
|
||||
public void UploadTestFile()
|
||||
{
|
||||
LogBlockExchangeStatus(Nodes[0], "Before upload");
|
||||
LogBlockExchangeStatus(Nodes[1], "Before upload");
|
||||
|
||||
file = FileManager.GenerateFile(size);
|
||||
|
||||
LogStoredBytes(Nodes[0]);
|
||||
@@ -38,27 +34,9 @@ namespace ContinuousTests.Tests
|
||||
{
|
||||
TrackedFile? dl = null;
|
||||
|
||||
try
|
||||
{
|
||||
LogBytesPerMillisecond(() => dl = Nodes[1].DownloadContent(cid!));
|
||||
LogBytesPerMillisecond(() => dl = Nodes[1].DownloadContent(cid!));
|
||||
|
||||
file.AssertIsEqual(dl);
|
||||
}
|
||||
catch
|
||||
{
|
||||
LogRepoStore(Nodes[0]);
|
||||
LogRepoStore(Nodes[1]);
|
||||
throw;
|
||||
}
|
||||
|
||||
LogBlockExchangeStatus(Nodes[0], "After download");
|
||||
LogBlockExchangeStatus(Nodes[1], "After download");
|
||||
}
|
||||
|
||||
private void LogRepoStore(ICodexNode codexNode)
|
||||
{
|
||||
//var response = codexNode.GetDebugRepoStore();
|
||||
//Log.Log($"{codexNode.GetName()} has {string.Join(",", response.Select(r => r.cid))}");
|
||||
file.AssertIsEqual(dl);
|
||||
}
|
||||
|
||||
private void LogStoredBytes(ICodexNode node)
|
||||
@@ -87,11 +65,5 @@ namespace ContinuousTests.Tests
|
||||
var bytesPerMs = totalBytes / totalMs;
|
||||
Log.Log($"Bytes per millisecond: {bytesPerMs}");
|
||||
}
|
||||
|
||||
private void LogBlockExchangeStatus(ICodexNode codexNode, string msg)
|
||||
{
|
||||
//var response = codexNode.GetDebugBlockExchange();
|
||||
//Log.Log($"{codexNode.GetName()} {msg}: {JsonConvert.SerializeObject(response)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ Report for: 08-2023
|
||||
(Stopped: The number of tests that can successfully run on the test-net is not high enough to justify the cost of leaving it running.)
|
||||
|
||||
## Deployment Configuration
|
||||
Continous Test-net is deployed to the kubernetes cluster with the following configuration:
|
||||
Continuous Test-net is deployed to the kubernetes cluster with the following configuration:
|
||||
|
||||
5x Codex Nodes:
|
||||
- Log-level: Trace
|
||||
|
||||
@@ -11,7 +11,7 @@ Report for: 07-2023
|
||||
(Faulted: Tests fail with such frequency that the information gathered does not justify the cost of leaving the test-net running.)
|
||||
|
||||
## Deployment Configuration
|
||||
Continous Test-net is deployed to the kubernetes cluster with the following configuration:
|
||||
Continuous Test-net is deployed to the kubernetes cluster with the following configuration:
|
||||
|
||||
5x Codex Nodes:
|
||||
- Log-level: Trace
|
||||
|
||||
@@ -11,7 +11,7 @@ Report for: 09-2023
|
||||
(Stopped: The number of tests that can successfully run on the test-net is not high enough to justify the cost of leaving it running.)
|
||||
|
||||
## Deployment Configuration
|
||||
Continous Test-net is deployed to the kubernetes cluster with the following configuration:
|
||||
Continuous Test-net is deployed to the kubernetes cluster with the following configuration:
|
||||
|
||||
5x Codex Nodes:
|
||||
- Log-level: Trace
|
||||
|
||||
@@ -21,9 +21,7 @@ namespace CodexTests.BasicTests
|
||||
|
||||
var group = AddCodex(5, o => o
|
||||
.EnableMetrics()
|
||||
.EnableMarketplace(geth, contract, 10.Eth(), 100000.TestTokens(), s => s
|
||||
.AsStorageNode()
|
||||
.AsValidator())
|
||||
.EnableMarketplace(geth, contract, 10.Eth(), 100000.TestTokens(), isValidator: true)
|
||||
.WithBlockTTL(TimeSpan.FromMinutes(5))
|
||||
.WithBlockMaintenanceInterval(TimeSpan.FromSeconds(10))
|
||||
.WithBlockMaintenanceNumber(100)
|
||||
@@ -33,16 +31,13 @@ namespace CodexTests.BasicTests
|
||||
|
||||
var rc = Ci.DeployMetricsCollector(nodes);
|
||||
|
||||
var availability = new StorageAvailability(
|
||||
totalSpace: 500.MB(),
|
||||
maxDuration: TimeSpan.FromMinutes(5),
|
||||
minPriceForTotalSpace: 500.TestTokens(),
|
||||
maxCollateral: 1024.TestTokens()
|
||||
);
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
node.Marketplace.MakeStorageAvailable(availability);
|
||||
node.Marketplace.MakeStorageAvailable(
|
||||
size: 500.MB(),
|
||||
minPriceForTotalSpace: 500.TestTokens(),
|
||||
maxCollateral: 1024.TestTokens(),
|
||||
maxDuration: TimeSpan.FromMinutes(5));
|
||||
}
|
||||
|
||||
var endTime = DateTime.UtcNow + TimeSpan.FromHours(10);
|
||||
|
||||
@@ -54,7 +54,7 @@ namespace CodexTests.BasicTests
|
||||
public void MarketplaceExample()
|
||||
{
|
||||
var sellerInitialBalance = 234.TestTokens();
|
||||
var buyerInitialBalance = 100000.TestTokens();
|
||||
var buyerInitialBalance = 1000.TestTokens();
|
||||
var fileSize = 10.MB();
|
||||
|
||||
var geth = Ci.StartGethNode(s => s.IsMiner().WithName("disttest-geth"));
|
||||
@@ -64,19 +64,15 @@ namespace CodexTests.BasicTests
|
||||
.WithName("Seller")
|
||||
.WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn))
|
||||
.WithStorageQuota(11.GB())
|
||||
.EnableMarketplace(geth, contracts, initialEth: 10.Eth(), initialTokens: sellerInitialBalance, s => s
|
||||
.AsStorageNode()
|
||||
.AsValidator()));
|
||||
.EnableMarketplace(geth, contracts, initialEth: 10.Eth(), initialTokens: sellerInitialBalance, isValidator: true)
|
||||
.WithSimulateProofFailures(failEveryNProofs: 3));
|
||||
|
||||
AssertBalance(contracts, seller, Is.EqualTo(sellerInitialBalance));
|
||||
|
||||
var availability = new StorageAvailability(
|
||||
totalSpace: 10.GB(),
|
||||
maxDuration: TimeSpan.FromMinutes(30),
|
||||
seller.Marketplace.MakeStorageAvailable(
|
||||
size: 10.GB(),
|
||||
minPriceForTotalSpace: 1.TestTokens(),
|
||||
maxCollateral: 20.TestTokens()
|
||||
);
|
||||
seller.Marketplace.MakeStorageAvailable(availability);
|
||||
maxCollateral: 20.TestTokens(),
|
||||
maxDuration: TimeSpan.FromMinutes(3));
|
||||
|
||||
var testFile = GenerateTestFile(fileSize);
|
||||
|
||||
@@ -88,27 +84,20 @@ namespace CodexTests.BasicTests
|
||||
AssertBalance(contracts, buyer, Is.EqualTo(buyerInitialBalance));
|
||||
|
||||
var contentId = buyer.UploadFile(testFile);
|
||||
var purchaseContract = buyer.Marketplace.RequestStorage(contentId,
|
||||
pricePerSlotPerSecond: 2.TestTokens(),
|
||||
requiredCollateral: 10.TestTokens(),
|
||||
minRequiredNumberOfNodes: 1,
|
||||
proofProbability: 5,
|
||||
duration: TimeSpan.FromMinutes(1));
|
||||
|
||||
var purchase = new StoragePurchase(contentId)
|
||||
{
|
||||
PricePerSlotPerSecond = 2.TestTokens(),
|
||||
RequiredCollateral = 10.TestTokens(),
|
||||
MinRequiredNumberOfNodes = 5,
|
||||
NodeFailureTolerance = 2,
|
||||
ProofProbability = 5,
|
||||
Duration = TimeSpan.FromMinutes(5),
|
||||
Expiry = TimeSpan.FromMinutes(4)
|
||||
};
|
||||
|
||||
var purchaseContract = buyer.Marketplace.RequestStorage(purchase);
|
||||
|
||||
purchaseContract.WaitForStorageContractStarted();
|
||||
purchaseContract.WaitForStorageContractStarted(fileSize);
|
||||
|
||||
AssertBalance(contracts, seller, Is.LessThan(sellerInitialBalance), "Collateral was not placed.");
|
||||
|
||||
var request = GetOnChainStorageRequest(contracts);
|
||||
AssertStorageRequest(request, purchase, contracts, buyer);
|
||||
AssertSlotFilledEvents(contracts, purchase, request, seller);
|
||||
AssertStorageRequest(request, contracts, buyer);
|
||||
AssertSlotFilledEvents(contracts, request, seller);
|
||||
AssertContractSlot(contracts, request, 0, seller);
|
||||
|
||||
purchaseContract.WaitForStorageContractFinished();
|
||||
@@ -116,6 +105,8 @@ namespace CodexTests.BasicTests
|
||||
AssertBalance(contracts, seller, Is.GreaterThan(sellerInitialBalance), "Seller was not paid for storage.");
|
||||
AssertBalance(contracts, buyer, Is.LessThan(buyerInitialBalance), "Buyer was not charged for storage.");
|
||||
Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Finished));
|
||||
|
||||
// waiting for block retransmit fix: CheckLogForErrors(seller, buyer);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -135,29 +126,24 @@ namespace CodexTests.BasicTests
|
||||
Assert.That(discN, Is.LessThan(bootN));
|
||||
}
|
||||
|
||||
private void AssertSlotFilledEvents(ICodexContracts contracts, StoragePurchase purchase, Request request, ICodexNode seller)
|
||||
private void AssertSlotFilledEvents(ICodexContracts contracts, Request request, ICodexNode seller)
|
||||
{
|
||||
// Expect 1 fulfilled event for the purchase.
|
||||
var requestFulfilledEvents = contracts.GetRequestFulfilledEvents(GetTestRunTimeRange());
|
||||
Assert.That(requestFulfilledEvents.Length, Is.EqualTo(1));
|
||||
CollectionAssert.AreEqual(request.RequestId, requestFulfilledEvents[0].RequestId);
|
||||
|
||||
// Expect 1 filled-slot event for each slot in the purchase.
|
||||
var filledSlotEvents = contracts.GetSlotFilledEvents(GetTestRunTimeRange());
|
||||
Assert.That(filledSlotEvents.Length, Is.EqualTo(purchase.MinRequiredNumberOfNodes));
|
||||
for (var i = 0; i < purchase.MinRequiredNumberOfNodes; i++)
|
||||
{
|
||||
var filledSlotEvent = filledSlotEvents.Single(e => e.SlotIndex == i);
|
||||
Assert.That(filledSlotEvent.RequestId.ToHex(), Is.EqualTo(request.RequestId.ToHex()));
|
||||
Assert.That(filledSlotEvent.Host, Is.EqualTo(seller.EthAddress));
|
||||
}
|
||||
Assert.That(filledSlotEvents.Length, Is.EqualTo(1));
|
||||
var filledSlotEvent = filledSlotEvents.Single();
|
||||
Assert.That(filledSlotEvent.SlotIndex.IsZero);
|
||||
Assert.That(filledSlotEvent.RequestId.ToHex(), Is.EqualTo(request.RequestId.ToHex()));
|
||||
Assert.That(filledSlotEvent.Host, Is.EqualTo(seller.EthAddress));
|
||||
}
|
||||
|
||||
private void AssertStorageRequest(Request request, StoragePurchase purchase, ICodexContracts contracts, ICodexNode buyer)
|
||||
private void AssertStorageRequest(Request request, ICodexContracts contracts, ICodexNode buyer)
|
||||
{
|
||||
Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Started));
|
||||
Assert.That(request.ClientAddress, Is.EqualTo(buyer.EthAddress));
|
||||
Assert.That(request.Ask.Slots, Is.EqualTo(purchase.MinRequiredNumberOfNodes));
|
||||
Assert.That(request.Ask.Slots, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
private Request GetOnChainStorageRequest(ICodexContracts contracts)
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace CodexTests.BasicTests
|
||||
{
|
||||
var primary = Ci.StartCodexNode();
|
||||
|
||||
primary.Stop(waitTillStopped: true);
|
||||
primary.Stop();
|
||||
|
||||
primary = Ci.StartCodexNode();
|
||||
|
||||
|
||||
@@ -10,10 +10,12 @@ namespace CodexTests.BasicTests
|
||||
[Test]
|
||||
public void TwoClientTest()
|
||||
{
|
||||
var uploader = AddCodex(s => s.WithName("Uploader"));
|
||||
var downloader = AddCodex(s => s.WithName("Downloader").WithBootstrapNode(uploader));
|
||||
var group = Ci.StartCodexNodes(2);
|
||||
|
||||
PerformTwoClientTest(uploader, downloader);
|
||||
var primary = group[0];
|
||||
var secondary = group[1];
|
||||
|
||||
PerformTwoClientTest(primary, secondary);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -26,27 +28,29 @@ namespace CodexTests.BasicTests
|
||||
return;
|
||||
}
|
||||
|
||||
var uploader = Ci.StartCodexNode(s => s.WithName("Uploader").At(locations.Get(0)));
|
||||
var downloader = Ci.StartCodexNode(s => s.WithName("Downloader").WithBootstrapNode(uploader).At(locations.Get(1)));
|
||||
var primary = Ci.StartCodexNode(s => s.At(locations.Get(0)));
|
||||
var secondary = Ci.StartCodexNode(s => s.At(locations.Get(1)));
|
||||
|
||||
PerformTwoClientTest(uploader, downloader);
|
||||
PerformTwoClientTest(primary, secondary);
|
||||
}
|
||||
|
||||
private void PerformTwoClientTest(ICodexNode uploader, ICodexNode downloader)
|
||||
private void PerformTwoClientTest(ICodexNode primary, ICodexNode secondary)
|
||||
{
|
||||
PerformTwoClientTest(uploader, downloader, 10.MB());
|
||||
PerformTwoClientTest(primary, secondary, 1.MB());
|
||||
}
|
||||
|
||||
private void PerformTwoClientTest(ICodexNode uploader, ICodexNode downloader, ByteSize size)
|
||||
private void PerformTwoClientTest(ICodexNode primary, ICodexNode secondary, ByteSize size)
|
||||
{
|
||||
primary.ConnectToPeer(secondary);
|
||||
|
||||
var testFile = GenerateTestFile(size);
|
||||
|
||||
var contentId = uploader.UploadFile(testFile);
|
||||
var contentId = primary.UploadFile(testFile);
|
||||
|
||||
var downloadedFile = downloader.DownloadContent(contentId);
|
||||
var downloadedFile = secondary.DownloadContent(contentId);
|
||||
|
||||
testFile.AssertIsEqual(downloadedFile);
|
||||
CheckLogForErrors(uploader, downloader);
|
||||
CheckLogForErrors(primary, secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,6 @@ namespace CodexTests
|
||||
|
||||
public void CheckLogForErrors(ICodexNode node)
|
||||
{
|
||||
Log($"Checking {node.GetName()} log for errors.");
|
||||
var log = Ci.DownloadLog(node);
|
||||
|
||||
log.AssertLogDoesNotContain("Block validation failed");
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace DistTestCore
|
||||
{
|
||||
public static void AssertLogContains(this IDownloadedLog log, string expectedString)
|
||||
{
|
||||
Assert.That(log.GetLinesContaining(expectedString).Any(), $"Did not find '{expectedString}' in log.");
|
||||
Assert.That(log.DoesLogContain(expectedString), $"Did not find '{expectedString}' in log.");
|
||||
}
|
||||
|
||||
public static void AssertLogDoesNotContain(this IDownloadedLog log, params string[] unexpectedStrings)
|
||||
@@ -15,10 +15,9 @@ namespace DistTestCore
|
||||
var errors = new List<string>();
|
||||
foreach (var str in unexpectedStrings)
|
||||
{
|
||||
var lines = log.GetLinesContaining(str);
|
||||
foreach (var line in lines)
|
||||
if (log.DoesLogContain(str))
|
||||
{
|
||||
errors.Add($"Found '{str}' in line '{line}'.");
|
||||
errors.Add($"Did find '{str}' in log.");
|
||||
}
|
||||
}
|
||||
CollectionAssert.IsEmpty(errors);
|
||||
|
||||
@@ -7,14 +7,12 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="nunit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.4.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Framework\NethereumWorkflow\NethereumWorkflow.csproj" />
|
||||
<ProjectReference Include="..\..\Framework\Utils\Utils.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
using Logging;
|
||||
using Moq;
|
||||
using NethereumWorkflow;
|
||||
using NethereumWorkflow.BlockUtils;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace FrameworkTests.NethereumWorkflow
|
||||
{
|
||||
[TestFixture]
|
||||
public class BlockTimeFinderTests
|
||||
{
|
||||
private readonly Mock<ILog> log = new Mock<ILog>();
|
||||
private Mock<IWeb3Blocks> web3 = new Mock<IWeb3Blocks>();
|
||||
private Dictionary<ulong, Block> blocks = new Dictionary<ulong, Block>();
|
||||
|
||||
private BlockTimeFinder finder = null!;
|
||||
|
||||
private void SetupBlockchain()
|
||||
{
|
||||
var start = DateTime.UtcNow.AddDays(-1).AddSeconds(-30);
|
||||
blocks = new Dictionary<ulong, Block>();
|
||||
|
||||
for (ulong i = 0; i < 30; i++)
|
||||
{
|
||||
ulong d = 100 + i;
|
||||
blocks.Add(d, new Block(d, start + TimeSpan.FromSeconds(i * 2)));
|
||||
}
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
SetupBlockchain();
|
||||
|
||||
web3 = new Mock<IWeb3Blocks>();
|
||||
web3.Setup(w => w.GetCurrentBlockNumber()).Returns(blocks.Keys.Max());
|
||||
web3.Setup(w => w.GetTimestampForBlock(It.IsAny<ulong>())).Returns<ulong>(d =>
|
||||
{
|
||||
if (blocks.ContainsKey(d)) return blocks[d].Time;
|
||||
return null;
|
||||
});
|
||||
|
||||
finder = new BlockTimeFinder(new BlockCache(), web3.Object, log.Object);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FindsMiddleOfChain()
|
||||
{
|
||||
var b1 = blocks[115];
|
||||
var b2 = blocks[116];
|
||||
|
||||
var momentBetween = b1.JustAfter;
|
||||
|
||||
var b1Number = finder.GetHighestBlockNumberBefore(momentBetween);
|
||||
var b2Number = finder.GetLowestBlockNumberAfter(momentBetween);
|
||||
|
||||
Assert.That(b1Number, Is.EqualTo(b1.Number));
|
||||
Assert.That(b2Number, Is.EqualTo(b2.Number));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FindsFrontOfChain_Lowest()
|
||||
{
|
||||
var first = blocks.First().Value;
|
||||
|
||||
var firstNumber = finder.GetLowestBlockNumberAfter(first.JustBefore);
|
||||
|
||||
Assert.That(firstNumber, Is.EqualTo(first.Number));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FindsFrontOfChain_Highest()
|
||||
{
|
||||
var first = blocks.First().Value;
|
||||
|
||||
var firstNumber = finder.GetHighestBlockNumberBefore(first.JustAfter);
|
||||
|
||||
Assert.That(firstNumber, Is.EqualTo(first.Number));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FindsTailOfChain_Lowest()
|
||||
{
|
||||
var last = blocks.Last().Value;
|
||||
|
||||
var lastNumber = finder.GetLowestBlockNumberAfter(last.JustBefore);
|
||||
|
||||
Assert.That(lastNumber, Is.EqualTo(last.Number));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FindsTailOfChain_Highest()
|
||||
{
|
||||
var last = blocks.Last().Value;
|
||||
|
||||
var lastNumber = finder.GetHighestBlockNumberBefore(last.JustAfter);
|
||||
|
||||
Assert.That(lastNumber, Is.EqualTo(last.Number));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FailsToFindBlockBeforeFrontOfChain()
|
||||
{
|
||||
var first = blocks.First().Value;
|
||||
|
||||
var notFound = finder.GetHighestBlockNumberBefore(first.Time);
|
||||
|
||||
Assert.That(notFound, Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FailsToFindBlockAfterTailOfChain()
|
||||
{
|
||||
var last = blocks.Last().Value;
|
||||
|
||||
var notFound = finder.GetLowestBlockNumberAfter(last.Time);
|
||||
|
||||
Assert.That(notFound, Is.Null);
|
||||
}
|
||||
}
|
||||
|
||||
public class Block
|
||||
{
|
||||
public Block(ulong number, DateTime time)
|
||||
{
|
||||
Number = number;
|
||||
Time = time;
|
||||
}
|
||||
|
||||
public ulong Number { get; }
|
||||
public DateTime Time { get; }
|
||||
public DateTime JustBefore { get { return Time.AddSeconds(-1); } }
|
||||
public DateTime JustAfter { get { return Time.AddSeconds(1); } }
|
||||
}
|
||||
}
|
||||
@@ -41,11 +41,7 @@ namespace CodexNetDeployer
|
||||
|
||||
if (config.ShouldMakeStorageAvailable)
|
||||
{
|
||||
s.EnableMarketplace(gethNode, contracts, 100.Eth(), config.InitialTestTokens.TestTokens(), s =>
|
||||
{
|
||||
if (validatorsLeft > 0) s.AsValidator();
|
||||
if (config.ShouldMakeStorageAvailable) s.AsStorageNode();
|
||||
});
|
||||
s.EnableMarketplace(gethNode, contracts, 100.Eth(), config.InitialTestTokens.TestTokens(), validatorsLeft > 0);
|
||||
}
|
||||
|
||||
if (bootstrapNode != null) s.WithBootstrapNode(bootstrapNode);
|
||||
@@ -67,14 +63,11 @@ namespace CodexNetDeployer
|
||||
|
||||
if (config.ShouldMakeStorageAvailable)
|
||||
{
|
||||
var availability = new StorageAvailability(
|
||||
totalSpace: config.StorageSell!.Value.MB(),
|
||||
maxDuration: TimeSpan.FromSeconds(config.MaxDuration),
|
||||
var response = codexNode.Marketplace.MakeStorageAvailable(
|
||||
size: config.StorageSell!.Value.MB(),
|
||||
minPriceForTotalSpace: config.MinPrice.TestTokens(),
|
||||
maxCollateral: config.MaxCollateral.TestTokens()
|
||||
);
|
||||
|
||||
var response = codexNode.Marketplace.MakeStorageAvailable(availability);
|
||||
maxCollateral: config.MaxCollateral.TestTokens(),
|
||||
maxDuration: TimeSpan.FromSeconds(config.MaxDuration));
|
||||
|
||||
if (!string.IsNullOrEmpty(response))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user