Compare commits

..
148 changed files with 1894 additions and 4686 deletions
+1 -1
View File
@@ -104,7 +104,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: digests-${{ matrix.target.arch }}
path: /tmp/digests
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
+13 -85
View File
@@ -1,26 +1,10 @@
# Distributed System Tests for Nim-Codex
## Contributing plugins
The testing framework was created for testing Codex. However, it's been designed such that other containerized projects can 'easily' be added.
In this file, you'll see 'users' (in quote) mentioned once or twice. This refers to code/projects/tests which end up making use of your plugin. 'Users' come in many shapes and sizes and tend to have many differen use-cases in mind. Please consider this when reading this document and writing your plugin.
## Checklist
Your application must pass this checklist to be compatible with the framework:
- It runs in a docker container.
- It can be configured via environment variables. (You may need to create a docker image which contains a shell script, to pass some env-vars as CLI arguments to your application. Container command overrides do work, but are not equally reliable across container platforms. When in doubt: use env-var!)
- It has network interaction:
- It exposes one or more APIs via one or more ports, OR
- It makes calls to other services. (OR both.)
If your application's use-cases rely primarily on shell interaction, this framework might not be for you. The framework allows you to execute commands in containers AND read stdout/stderr responses. However, its focus during development has always been webservice API interactions.
## Steps
In order to add your project to the framework you must:
The testing framework was created for testing Codex. However, it's been designed such that other distributed/containerized projects can 'easily' be added. In order to add your project to the framework you must:
1. Create a library assembly in the project plugins folder.
1. It must contain a type that implements the `IProjectPlugin` interface from the `Core` assembly.
1. If your plugin wants to expose any specific methods or objects to 'users', it must implement extensions for the `CoreInterface` type.
1. If your plugin wants to run containers of its own project, it must provide a recipe.
1. If your plugin wants to expose any specific methods or objects to the code using the framework (the tests and tools), it must implement extensions for the `CoreInterface` type.
## Constructors & Tools
Your implementation of `IProjectPlugin` must have a public constructor with a single argument of type `IPluginTools`, for example:
@@ -36,34 +20,19 @@ Your implementation of `IProjectPlugin` must have a public constructor with a si
}
```
`IPluginTools` provides your plugin access to all framework functionality, such as logging, tracked file management, container lifecycle management, and a means to create HTTP clients for containers. (Without having to figure out addresses manually.)
## Plugin Interfaces
The `IProjectPlugin` interface requires the implementation of two methods.
1. `Announce` - It is considered polite to use the logging functionality provided by the `IPluginTools` to announce that your plugin has been loaded. You may also want to log some manner of version and/or configuration information at this time if applicable.
1. `Decommission` - Should your plugin have any active system resources, free them in this method. Please note that resources managed by the framework (such as running containers and tracked data files) do *not* need to be manually disposed in this method. `Decommission` is to be used for resources not managed by the framework.
1. `Announce` - It is considered polite to use the logging functionality provided by the `IPluginTools` to announce that your plugin has been loaded. You may also want to log some manner of version information at this time if applicable.
1. `Decommission` - Should your plugin have any active system resources, free them in this method.
There are a few optional interfaces your plugin may choose to implement. The framework will automatically use these interfaces.
1. `IHasLogPrefix` - Implementing this interface allows you to provide a string which will be prepended to all log statements made by your plugin. A polite thing to do.
1. `IHasMetadata` - This allows you to provide metadata in the form of key/value pairs. This metadata can be accessed by 'users' of your plugin. Often this data finds its way into log files and container-descriptors in order to help track versions/tests/deployments, etc.
## IPluginTools
`IPluginTools` provides your plugin access to all framework functionality, such as logging, tracked file management, container lifecycle management, and a means to create HTTP clients to make calls to containers. (Figure out addresses and ports for containers is handled by the framework.)
It is possible and allowed for your plugin to depend on and use other plugins. (For example, maybe your project wants to interact with Ethereum and wants to use the GethPlugin to talk to a Geth node.) `IPluginTools` is *not* what is used for accessing functionality of other plugins. See 'Core Interface' section.
ILog GetLog();
IHttp CreateHttp(Action<HttpClient> onClientCreated);
IHttp CreateHttp(Action<HttpClient> onClientCreated, ITimeSet timeSet);
IHttp CreateHttp();
IFileManager GetFileManager();
The plugin tools provide:
1. `Workflow` - This tool allows you to start and stop containers using "container recipes". (More on those below.) It also allows you to execute commands inside a container, access stdout/stderr, detect crashes, and access pod deployment information. The workflow tool also lets you inspect the locations available in the cluster, and decide where you want to run containers. (More on that below as well.)
1. `Log` - Good logging is priceless. Use this tool to get a log object handle, and write useful debug/info/error statements.
1. `Http` - This tool gives you a convenient way to access a standard dotnet HttpClient, and takes care of timeouts and retries (in accordance with the config). Additionally, it combos nicely with container objects created by `Workflow`, such that you never have to spend any time figuring out the addresses and ports of your containers.
1. `FileManager` - Lets you use tracked temporary files. Even if the 'user' tests/application start crashing, the framework will make sure these are cleaned up.
1. `IHasLogPrefix` - Implementing this interface allows you to provide a string with will be prepended to all log statements made by your plugin.
1. `IHasMetadata` - This allows you to provide metadata in the form of key/value pairs. This metadata can be accessed by code that uses your plugin.
## Core Interface
Any functionality your plugin wants to expose to 'users' will have to be added on to the `CoreInterface` type. You can accomplish this by using C# extension methods. The framework provides a `GetPlugin` method to access your plugin instance from the `CoreInterface` type:
Any functionality your plugin wants to expose to code which uses the framework will have to be added on to the `CoreInterface` type. You can accomplish this by using C# extension methods. The framework provides a `GetPlugin` method to access your plugin instance from the `CoreInterface` type:
```C#
public static class CoreInterfaceExtensions
{
@@ -79,14 +48,12 @@ Any functionality your plugin wants to expose to 'users' will have to be added o
}
```
If your plugin wants to access the functionality exposed by other plugins, then you can pass the argument `CoreInterface ci` to your plugin code in order to do so. (For example, if you want to start a Geth node, the Geth plugin adds `IGethNode StartGethNode(this CoreInterface ci, Action<IGethSetup> setup)` to the core interface.) Don't forget you'll need to add a project reference to each plugin project you wish to use.
While technically you can build whatever you like on top of the `CoreInterface` and your own plugin types, I recommend that you follow the approach explained below.
## Deploying, Wrapping, and Starting
When building a plugin, it is important to make as few assumptions as possible about how it will be used by whoever is going to use the framework. For this reason, I recommend you expose three kinds of methods using your `CoreInterface` extensions:
1. Deploy - This kind of method should deploy your project, creating and configuring containers as needed and returning container objects as a result. If your project requires additional information, you can create a new class type to contain both it and the container objects created.
1. Wrap - This kind of method should, when given the previously mentioned container information, create some kind of convenient accessor or interactor object. This object should abstract away for example details of a REST API of your project, allowing users of your plugin to write their code using a set of methods and types that nicely model your project's domain. (For example, if my project has a REST API call that allows users to fetch some state information, the object returned by Wrap should have a convenient method to call that API and receive that state information.)
1. Deploy - This kind of method should deploy your project, creating and configuring containers as needed and returning containers as a result. If your project requires additional information, you can create a new class type to contain both it and the containers created.
1. Wrap - This kind of method should, when given the previously mentioned container information, create some kind of convenient accessor or interactor object. This object should abstract away for example details of a REST API of your project, allowing users of your plugin to write their code using a set of methods and types that nicely model your project's domain.
1. Start - This kind of method does both, simply calling a Deploy method first, then a Wrap method, and returns the result.
Here's an example:
@@ -102,8 +69,8 @@ public static class CoreInterfaceExtensions
public static IMyProjectNode WrapMyProjectContainer(this CoreInterface ci, RunningContainers container)
{
return Plugin(ci).WrapMyContainerProject(container); // <-- This method probably will use the 'PluginTools.CreateHttp()` to create an HTTP client for the container, then wrap it in an object that
// represents the API of your project, in this case 'IMyProjectNode'.
return Plugin(ci).WrapMyContainerProject(container); // <-- This method probably will use the 'PluginTools.CreateHttp()` tool to create an HTTP client for the container, then wrap it in an object that
// represents the API of your project.
}
public static IMyProjectNode StartMyProject(this CoreInterface ci, string someArgument)
@@ -115,44 +82,5 @@ public static class CoreInterfaceExtensions
}
```
Should your deploy methods not return framework-types like RunningContainers, please make sure that your custom times are serializable. (Decorate them with the `[Serializable]` attribute.) Tools have been built using this framework which rely on the ability to serialize and store deployment information for later use. Please don't break this possibility. (Consider using the `SerializeGate` type to help ensure compatibility.)
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.
## Container Recipes
In order to run a container of your application, the framework needs to know how to create that container. Think of a container recipe as being similar to a docker-compose.yaml file: You specify the docker image, ports, environment variables, persistent volumes, and secrets. However, container recipes are code. This allows you to add conditional behaviour to how your container is constructed. For example: The 'user' of your plugin specifies in their call input that they want to run your application in a certain mode. This would cause your container recipe to set certain environment variables, which cause the application to behave in the requested way.
### Addresses and ports
In a docker-compose.yaml file, it is perfectly normal to specify which ports should be exposed on your container. However, in the framework there's more to consider. When your application container starts, who knows on what kind of machine it runs, and what other processes it's sharing space with? Well, Kubernetes knows. Therefore, it is recommended that container recipes *do not* specify exact port numbers. The framework allows container recipes to declare "a port" without specifying its port number. This allows the framework and Kubernetes to figure out which ports are available when it's time to deploy. In order to find out which port numbers were assigned post-deployment, you can look up the port by tag (which is just an identifying string). When you specify a port to be mapped in your container recipe, you must specify:
1. `Tag` - An identifier.
1. `Internal` or `External` - Whether this port should be accessible only inside the cluster (for other containers (k8s: "ClusterIP")) or outside the cluster as well (for external tools/applications (k8s: "NodePort")).
1. `Protocol` - TCP or UDP. Both protocols on the same port is not universally supported by all container engines, and is therefore not supported by the framework.
If your application wants to listen for incoming traffic from inside its container, be sure to bind it to address "0.0.0.0".
Reminder: If you don't want to worry about addresses, and internal or external ports, you don't have to! The container objects returned by the `workflow` plugin tool have a method called `GetAddress`. Given a port tag, it returns and address object. The `Http` plugin tool can use that address object to set up connections.
## Locations
The framework is designed to allow you to control instances of your application in multiple (physical) locations. It accomplishes this by using kubernetes, and the ability to deploy containers to specific hosts (nodes) inside a kubernetes cluster. Since Kubernetes allows you to build clusters cross-site, this framework in theory enables you to deploy and interact with containers running anywhere.
The `workflow` plugin tool provides you a list of all available locations in the cluster. When starting a container, you are able to pick one of those locations. If no location is selected, one will be chosen by kubernetes. Locations can be chosen explicitly by kubernetes node name, or, they can be picked from the array of available locations.
Example:
```C#
{
var location = Ci.GetKnownLocations().Get("kbnode_euwest_paris1");
var codex = Ci.StartCodexNode(s => s.At(location));
}
```
In this example, 'Ci' is an instance of the core interface. The CodexPlugin exposes a function 'StartCodexNode', which allows its user to specify a location. This location is then passed to the `workflow` tool when the Codex plugin starts its container.
The available locations array guarantees that each entry corresponds to a different kubernetes host.
```C#
{
var knownLocations = Ci.GetKnownLocations();
// I don't care where exactly, as long as they are different locations.
var codexAtZero = Ci.StartCodexNode(s => s.At(knownLocations.Get(0)));
var codexAtOne = Ci.StartCodexNode(s => s.At(knownLocations.Get(1)));
}
```
+5 -22
View File
@@ -4,8 +4,7 @@ namespace Core
{
public interface IDownloadedLog
{
void IterateLines(Action<string> action);
string[] GetLinesContaining(string expectedString);
bool DoesLogContain(string expectedString);
string[] FindLinesThatContain(params string[] tags);
void DeleteFile();
}
@@ -19,7 +18,7 @@ namespace Core
this.logFile = logFile;
}
public void IterateLines(Action<string> action)
public bool DoesLogContain(string expectedString)
{
using var file = File.OpenRead(logFile.FullFilename);
using var streamReader = new StreamReader(file);
@@ -27,28 +26,12 @@ namespace Core
var line = streamReader.ReadLine();
while (line != null)
{
action(line);
line = streamReader.ReadLine();
}
}
public string[] GetLinesContaining(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)
-193
View File
@@ -1,193 +0,0 @@
using Logging;
using Newtonsoft.Json;
using Serialization = Newtonsoft.Json.Serialization;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Utils;
namespace Core
{
public interface IEndpoint
{
string HttpGetString(string route);
T HttpGetJson<T>(string route);
TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body);
string HttpPostJson<TRequest>(string route, TRequest body);
TResponse HttpPostString<TResponse>(string route, string body);
string HttpPostStream(string route, Stream stream);
Stream HttpGetStream(string route);
T Deserialize<T>(string json);
}
internal class Endpoint : IEndpoint
{
private readonly ILog log;
private readonly IHttp http;
private readonly Address address;
private readonly string baseUrl;
private readonly string? logAlias;
public Endpoint(ILog log, IHttp http, Address address, string baseUrl, string? logAlias)
{
this.log = log;
this.http = http;
this.address = address;
this.baseUrl = baseUrl;
this.logAlias = logAlias;
}
public string HttpGetString(string route)
{
return http.OnClient(client =>
{
return GetString(client, route);
}, $"HTTP-GET:{route}");
}
public T HttpGetJson<T>(string route)
{
return http.OnClient(client =>
{
var json = GetString(client, route);
return Deserialize<T>(json);
}, $"HTTP-GET:{route}");
}
public TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body)
{
return http.OnClient(client =>
{
var response = PostJson(client, route, body);
var json = Time.Wait(response.Content.ReadAsStringAsync());
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(json);
}
Log(GetUrl() + route, json);
return Deserialize<TResponse>(json);
}, $"HTTP-POST-JSON: {route}");
}
public string HttpPostJson<TRequest>(string route, TRequest body)
{
return http.OnClient(client =>
{
var response = PostJson(client, route, body);
return Time.Wait(response.Content.ReadAsStringAsync());
}, $"HTTP-POST-JSON: {route}");
}
public TResponse HttpPostString<TResponse>(string route, string body)
{
return http.OnClient(client =>
{
var response = PostJsonString(client, route, body);
if (response == null) throw new Exception("Received no response.");
var result = Deserialize<TResponse>(response);
if (result == null) throw new Exception("Failed to deserialize response");
return result;
}, $"HTTO-POST-JSON: {route}");
}
public string HttpPostStream(string route, Stream stream)
{
return http.OnClient(client =>
{
var url = GetUrl() + route;
Log(url, "~ STREAM ~");
var content = new StreamContent(stream);
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
var response = Time.Wait(client.PostAsync(url, content));
var str = Time.Wait(response.Content.ReadAsStringAsync());
Log(url, str);
return str;
}, $"HTTP-POST-STREAM: {route}");
}
public Stream HttpGetStream(string route)
{
return http.OnClient(client =>
{
var url = GetUrl() + route;
Log(url, "~ STREAM ~");
return Time.Wait(client.GetStreamAsync(url));
}, $"HTTP-GET-STREAM: {route}");
}
public T Deserialize<T>(string json)
{
var errors = new List<string>();
var deserialized = JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings()
{
Error = delegate (object? sender, Serialization.ErrorEventArgs args)
{
if (args.CurrentObject == args.ErrorContext.OriginalObject)
{
errors.Add($"""
Member: '{args.ErrorContext.Member?.ToString() ?? "<null>"}'
Path: {args.ErrorContext.Path}
Error: {args.ErrorContext.Error.Message}
""");
args.ErrorContext.Handled = true;
}
}
});
if (errors.Count > 0)
{
throw new JsonSerializationException($"Failed to deserialize JSON '{json}' with exception(s): \n{string.Join("\n", errors)}");
}
else if (deserialized == null)
{
throw new JsonSerializationException($"Failed to deserialize JSON '{json}': resulting deserialized object is null");
}
return deserialized;
}
private string GetString(HttpClient client, string route)
{
var url = GetUrl() + route;
Log(url, "");
var result = Time.Wait(client.GetAsync(url));
var str = Time.Wait(result.Content.ReadAsStringAsync());
Log(url, str);
return str;
}
private HttpResponseMessage PostJson<TRequest>(HttpClient client, string route, TRequest body)
{
var url = GetUrl() + route;
using var content = JsonContent.Create(body);
Log(url, JsonConvert.SerializeObject(body));
return Time.Wait(client.PostAsync(url, content));
}
private string PostJsonString(HttpClient client, string route, string body)
{
var url = GetUrl() + route;
Log(url, body);
var content = new StringContent(body);
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
var result = Time.Wait(client.PostAsync(url, content));
var str = Time.Wait(result.Content.ReadAsStringAsync());
Log(url, str);
return str;
}
private string GetUrl()
{
return $"{address.Host}:{address.Port}{baseUrl}";
}
private void Log(string url, string message)
{
if (logAlias != null)
{
log.Debug($"({logAlias})({url}) = '{message}'", 3);
}
else
{
log.Debug($"({url}) = '{message}'", 3);
}
}
}
}
+171 -20
View File
@@ -1,13 +1,22 @@
using Logging;
using Newtonsoft.Json;
using Serialization = Newtonsoft.Json.Serialization;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using Utils;
namespace Core
{
public interface IHttp
{
T OnClient<T>(Func<HttpClient, T> action);
T OnClient<T>(Func<HttpClient, T> action, string description);
IEndpoint CreateEndpoint(Address address, string baseUrl, string? logAlias = null);
string HttpGetString(string route);
T HttpGetJson<T>(string route);
TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body);
string HttpPostJson<TRequest>(string route, TRequest body);
TResponse HttpPostString<TResponse>(string route, string body);
string HttpPostStream(string route, Stream stream);
Stream HttpGetStream(string route);
T Deserialize<T>(string json);
}
internal class Http : IHttp
@@ -15,43 +24,185 @@ namespace Core
private static readonly object httpLock = new object();
private readonly ILog log;
private readonly ITimeSet timeSet;
private readonly Address address;
private readonly string baseUrl;
private readonly Action<HttpClient> onClientCreated;
private readonly string? logAlias;
internal Http(ILog log, ITimeSet timeSet)
: this(log, timeSet, DoNothing)
internal Http(ILog log, ITimeSet timeSet, Address address, string baseUrl, string? logAlias = null)
: this(log, timeSet, address, baseUrl, DoNothing, logAlias)
{
}
internal Http(ILog log, ITimeSet timeSet, Action<HttpClient> onClientCreated)
internal Http(ILog log, ITimeSet timeSet, Address address, string baseUrl, Action<HttpClient> onClientCreated, string? logAlias = null)
{
this.log = log;
this.timeSet = timeSet;
this.address = address;
this.baseUrl = baseUrl;
this.onClientCreated = onClientCreated;
this.logAlias = logAlias;
if (!this.baseUrl.StartsWith("/")) this.baseUrl = "/" + this.baseUrl;
if (!this.baseUrl.EndsWith("/")) this.baseUrl += "/";
}
public T OnClient<T>(Func<HttpClient, T> action)
public string HttpGetString(string route)
{
return OnClient(action, GetDescription());
}
public T OnClient<T>(Func<HttpClient, T> action, string description)
{
var client = GetClient();
return LockRetry(() =>
{
return action(client);
}, description);
return GetString(route);
}, $"HTTP-GET:{route}");
}
public IEndpoint CreateEndpoint(Address address, string baseUrl, string? logAlias = null)
public T HttpGetJson<T>(string route)
{
return new Endpoint(log, this, address, baseUrl, logAlias);
return LockRetry(() =>
{
var json = GetString(route);
return Deserialize<T>(json);
}, $"HTTP-GET:{route}");
}
private string GetDescription()
public TResponse HttpPostJson<TRequest, TResponse>(string route, TRequest body)
{
return DebugStack.GetCallerName(skipFrames: 2);
return LockRetry(() =>
{
var response = PostJson(route, body);
var json = Time.Wait(response.Content.ReadAsStringAsync());
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException(json);
}
Log(GetUrl() + route, json);
return Deserialize<TResponse>(json);
}, $"HTTP-POST-JSON: {route}");
}
public string HttpPostJson<TRequest>(string route, TRequest body)
{
return LockRetry(() =>
{
var response = PostJson(route, body);
return Time.Wait(response.Content.ReadAsStringAsync());
}, $"HTTP-POST-JSON: {route}");
}
public TResponse HttpPostString<TResponse>(string route, string body)
{
return LockRetry(() =>
{
var response = PostJsonString(route, body);
if (response == null) throw new Exception("Received no response.");
var result = Deserialize<TResponse>(response);
if (result == null) throw new Exception("Failed to deserialize response");
return result;
}, $"HTTO-POST-JSON: {route}");
}
public string HttpPostStream(string route, Stream stream)
{
return LockRetry(() =>
{
using var client = GetClient();
var url = GetUrl() + route;
Log(url, "~ STREAM ~");
var content = new StreamContent(stream);
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
var response = Time.Wait(client.PostAsync(url, content));
var str = Time.Wait(response.Content.ReadAsStringAsync());
Log(url, str);
return str;
}, $"HTTP-POST-STREAM: {route}");
}
public Stream HttpGetStream(string route)
{
return LockRetry(() =>
{
var client = GetClient();
var url = GetUrl() + route;
Log(url, "~ STREAM ~");
return Time.Wait(client.GetStreamAsync(url));
}, $"HTTP-GET-STREAM: {route}");
}
public T Deserialize<T>(string json)
{
var errors = new List<string>();
var deserialized = JsonConvert.DeserializeObject<T>(json, new JsonSerializerSettings()
{
Error = delegate(object? sender, Serialization.ErrorEventArgs args)
{
if (args.CurrentObject == args.ErrorContext.OriginalObject)
{
errors.Add($"""
Member: '{args.ErrorContext.Member?.ToString() ?? "<null>"}'
Path: {args.ErrorContext.Path}
Error: {args.ErrorContext.Error.Message}
""");
args.ErrorContext.Handled = true;
}
}
});
if (errors.Count > 0)
{
throw new JsonSerializationException($"Failed to deserialize JSON '{json}' with exception(s): \n{string.Join("\n", errors)}");
}
else if (deserialized == null)
{
throw new JsonSerializationException($"Failed to deserialize JSON '{json}': resulting deserialized object is null");
}
return deserialized;
}
private string GetString(string route)
{
using var client = GetClient();
var url = GetUrl() + route;
Log(url, "");
var result = Time.Wait(client.GetAsync(url));
var str = Time.Wait(result.Content.ReadAsStringAsync());
Log(url, str);
return str;
}
private HttpResponseMessage PostJson<TRequest>(string route, TRequest body)
{
using var client = GetClient();
var url = GetUrl() + route;
using var content = JsonContent.Create(body);
Log(url, JsonConvert.SerializeObject(body));
return Time.Wait(client.PostAsync(url, content));
}
private string PostJsonString(string route, string body)
{
using var client = GetClient();
var url = GetUrl() + route;
Log(url, body);
var content = new StringContent(body);
content.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
var result = Time.Wait(client.PostAsync(url, content));
var str = Time.Wait(result.Content.ReadAsStringAsync());
Log(url, str);
return str;
}
private string GetUrl()
{
return $"{address.Host}:{address.Port}{baseUrl}";
}
private void Log(string url, string message)
{
if (logAlias != null)
{
log.Debug($"({logAlias})({url}) = '{message}'", 3);
}
else
{
log.Debug($"({url}) = '{message}'", 3);
}
}
private T LockRetry<T>(Func<T> operation, string description)
+13 -12
View File
@@ -1,6 +1,7 @@
using FileUtils;
using KubernetesWorkflow;
using Logging;
using Utils;
namespace Core
{
@@ -21,9 +22,9 @@ namespace Core
public interface IHttpFactoryTool
{
IHttp CreateHttp(Action<HttpClient> onClientCreated);
IHttp CreateHttp(Action<HttpClient> onClientCreated, ITimeSet timeSet);
IHttp CreateHttp();
IHttp CreateHttp(Address address, string baseUrl, Action<HttpClient> onClientCreated, string? logAlias = null);
IHttp CreateHttp(Address address, string baseUrl, Action<HttpClient> onClientCreated, ITimeSet timeSet, string? logAlias = null);
IHttp CreateHttp(Address address, string baseUrl, string? logAlias = null);
}
public interface IFileTool
@@ -36,11 +37,11 @@ namespace Core
private readonly ITimeSet timeSet;
private readonly WorkflowCreator workflowCreator;
private readonly IFileManager fileManager;
private readonly LogPrefixer log;
private ILog log;
internal PluginTools(ILog log, WorkflowCreator workflowCreator, string fileManagerRootFolder, ITimeSet timeSet)
{
this.log = new LogPrefixer(log);
this.log = log;
this.workflowCreator = workflowCreator;
this.timeSet = timeSet;
fileManager = new FileManager(log, fileManagerRootFolder);
@@ -48,22 +49,22 @@ namespace Core
public void ApplyLogPrefix(string prefix)
{
log.Prefix = prefix;
log = new LogPrefixer(log, prefix);
}
public IHttp CreateHttp(Action<HttpClient> onClientCreated)
public IHttp CreateHttp(Address address, string baseUrl, Action<HttpClient> onClientCreated, string? logAlias = null)
{
return CreateHttp(onClientCreated, timeSet);
return CreateHttp(address, baseUrl, onClientCreated, timeSet, logAlias);
}
public IHttp CreateHttp(Action<HttpClient> onClientCreated, ITimeSet ts)
public IHttp CreateHttp(Address address, string baseUrl, Action<HttpClient> onClientCreated, ITimeSet ts, string? logAlias = null)
{
return new Http(log, ts, onClientCreated);
return new Http(log, ts, address, baseUrl, onClientCreated, logAlias);
}
public IHttp CreateHttp()
public IHttp CreateHttp(Address address, string baseUrl, string? logAlias = null)
{
return new Http(log, timeSet);
return new Http(log, timeSet, address, baseUrl, logAlias);
}
public IStartupWorkflow CreateWorkflow(string? namespaceOverride = null)
+5 -5
View File
@@ -5,7 +5,7 @@
TimeSpan HttpCallTimeout();
int HttpMaxNumberOfRetries();
TimeSpan HttpCallRetryDelay();
TimeSpan K8sOperationRetryDelay();
TimeSpan WaitForK8sServiceDelay();
TimeSpan K8sOperationTimeout();
}
@@ -26,7 +26,7 @@
return TimeSpan.FromSeconds(1);
}
public TimeSpan K8sOperationRetryDelay()
public TimeSpan WaitForK8sServiceDelay()
{
return TimeSpan.FromSeconds(10);
}
@@ -54,14 +54,14 @@
return TimeSpan.FromSeconds(2);
}
public TimeSpan K8sOperationRetryDelay()
public TimeSpan WaitForK8sServiceDelay()
{
return TimeSpan.FromSeconds(30);
return TimeSpan.FromSeconds(10);
}
public TimeSpan K8sOperationTimeout()
{
return TimeSpan.FromHours(1);
return TimeSpan.FromMinutes(15);
}
}
}
@@ -3,8 +3,6 @@
public class GiveRewardsCommand
{
public RewardUsersCommand[] Rewards { get; set; } = Array.Empty<RewardUsersCommand>();
public MarketAverage[] Averages { get; set; } = Array.Empty<MarketAverage>();
public string[] EventsOverview { get; set; } = Array.Empty<string>();
}
public class RewardUsersCommand
@@ -12,15 +10,4 @@
public ulong RewardId { get; set; }
public string[] UserAddresses { get; set; } = Array.Empty<string>();
}
public class MarketAverage
{
public int NumberOfFinished { get; set; }
public int TimeRangeSeconds { get; set; }
public float Price { get; set; }
public float Size { get; set; }
public float Duration { get; set; }
public float Collateral { get; set; }
public float ProofProbability { get; set; }
}
}
+7 -7
View File
@@ -21,11 +21,11 @@ namespace DiscordRewards
}),
// Finished a sizable slot
new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot! (10mb/5mins for test)", new CheckConfig
new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot!", new CheckConfig
{
Type = CheckType.FinishedSlot,
MinSlotSize = 10.MB(),
MinDuration = TimeSpan.FromMinutes(5.0),
MinSlotSize = 1.GB(),
MinDuration = TimeSpan.FromHours(24.0),
}),
// Posted any contract
@@ -41,12 +41,12 @@ namespace DiscordRewards
}),
// Started a sizable contract
new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time! (10mb/5mins for test)", new CheckConfig
new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time!", new CheckConfig
{
Type = CheckType.StartedContract,
Type = CheckType.FinishedSlot,
MinNumberOfHosts = 4,
MinSlotSize = 10.MB(),
MinDuration = TimeSpan.FromMinutes(5.0),
MinSlotSize = 1.GB(),
MinDuration = TimeSpan.FromHours(24.0),
})
};
}
+1 -1
View File
@@ -45,7 +45,7 @@ namespace FileUtils
{
var sw = Stopwatch.Begin(log);
var result = GenerateRandomFile(size, label);
sw.End($"Generated file {result.Describe()}.");
sw.End($"Generated file '{result.Describe()}'.");
return result;
}
+12 -24
View File
@@ -43,25 +43,19 @@ namespace KubernetesWorkflow
return new StartResult(cluster, containerRecipes, deployment, internalService, externalService);
}
public void WaitUntilOnline(RunningContainer container)
{
WaitUntilDeploymentOnline(container.Recipe.Name);
}
public PodInfo GetPodInfo(RunningDeployment deployment)
{
var pod = GetPodForDeployment(deployment);
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)
@@ -377,6 +371,7 @@ namespace KubernetesWorkflow
};
client.Run(c => c.CreateNamespacedDeployment(deploymentSpec, K8sNamespace));
WaitUntilDeploymentOnline(deploymentSpec.Metadata.Name);
var name = deploymentSpec.Metadata.Name;
return new RunningDeployment(name, podLabel);
@@ -503,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
@@ -705,7 +693,7 @@ namespace KubernetesWorkflow
private string GetPodName(RunningContainer container)
{
return GetPodForDeployment(container.RunningPod.StartResult.Deployment).Metadata.Name;
return GetPodForDeployment(container.RunningContainers.StartResult.Deployment).Metadata.Name;
}
private V1Pod GetPodForDeployment(RunningDeployment deployment)
@@ -868,7 +856,7 @@ namespace KubernetesWorkflow
private void WaitUntilNamespaceCreated()
{
WaitUntil(() => IsNamespaceOnline(K8sNamespace), nameof(WaitUntilNamespaceCreated));
WaitUntil(() => IsNamespaceOnline(K8sNamespace));
}
private void WaitUntilDeploymentOnline(string deploymentName)
@@ -877,7 +865,7 @@ namespace KubernetesWorkflow
{
var deployment = client.Run(c => c.ReadNamespacedDeployment(deploymentName, K8sNamespace));
return deployment?.Status.AvailableReplicas != null && deployment.Status.AvailableReplicas > 0;
}, nameof(WaitUntilDeploymentOnline));
});
}
private void WaitUntilDeploymentOffline(string deploymentName)
@@ -887,7 +875,7 @@ namespace KubernetesWorkflow
var deployments = client.Run(c => c.ListNamespacedDeployment(K8sNamespace));
var deployment = deployments.Items.SingleOrDefault(d => d.Metadata.Name == deploymentName);
return deployment == null || deployment.Status.AvailableReplicas == 0;
}, nameof(WaitUntilDeploymentOffline));
});
}
private void WaitUntilPodsForDeploymentAreOffline(RunningDeployment deployment)
@@ -896,19 +884,19 @@ namespace KubernetesWorkflow
{
var pods = FindPodsByLabel(deployment.PodLabel);
return !pods.Any();
}, nameof(WaitUntilPodsForDeploymentAreOffline));
});
}
private void WaitUntil(Func<bool> predicate, string msg)
private void WaitUntil(Func<bool> predicate)
{
var sw = Stopwatch.Begin(log, true);
try
{
Time.WaitUntil(predicate, cluster.K8sOperationTimeout(), cluster.K8sOperationRetryDelay(), msg);
Time.WaitUntil(predicate, cluster.K8sOperationTimeout(), cluster.K8sOperationRetryDelay());
}
finally
{
sw.End(msg, 1);
sw.End("", 1);
}
}
+4 -4
View File
@@ -5,18 +5,18 @@ namespace KubernetesWorkflow
{
public interface IK8sHooks
{
void OnContainersStarted(RunningPod runningPod);
void OnContainersStopped(RunningPod runningPod);
void OnContainersStarted(RunningContainers runningContainers);
void OnContainersStopped(RunningContainers runningContainers);
void OnContainerRecipeCreated(ContainerRecipe recipe);
}
public class DoNothingK8sHooks : IK8sHooks
{
public void OnContainersStarted(RunningPod runningPod)
public void OnContainersStarted(RunningContainers runningContainers)
{
}
public void OnContainersStopped(RunningPod runningPod)
public void OnContainersStopped(RunningContainers runningContainers)
{
}
@@ -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 -25
View File
@@ -9,12 +9,12 @@ namespace KubernetesWorkflow
public interface IStartupWorkflow
{
IKnownLocations GetAvailableLocations();
FutureContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
FutureContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
RunningContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
RunningContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig);
PodInfo GetPodInfo(RunningContainer container);
PodInfo GetPodInfo(RunningPod pod);
PodInfo GetPodInfo(RunningContainers containers);
CrashWatcher CreateCrashWatcher(RunningContainer container);
void Stop(RunningPod pod, 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();
@@ -45,12 +45,12 @@ namespace KubernetesWorkflow
return locationProvider.GetAvailableLocations();
}
public FutureContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
public RunningContainers Start(int numberOfContainers, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
{
return Start(numberOfContainers, KnownLocations.UnspecifiedLocation, recipeFactory, startupConfig);
}
public FutureContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
public RunningContainers Start(int numberOfContainers, ILocation location, ContainerRecipeFactory recipeFactory, StartupConfig startupConfig)
{
return K8s(controller =>
{
@@ -60,36 +60,25 @@ namespace KubernetesWorkflow
var startResult = controller.BringOnline(recipes, location);
var containers = CreateContainers(startResult, recipes, startupConfig);
var rc = new RunningPod(startupConfig, startResult, containers);
var rc = new RunningContainers(startupConfig, startResult, containers);
cluster.Configuration.Hooks.OnContainersStarted(rc);
if (startResult.ExternalService != null)
{
componentFactory.Update(controller);
}
return new FutureContainers(rc, this);
});
}
public void WaitUntilOnline(RunningPod rc)
{
K8s(controller =>
{
foreach (var c in rc.Containers)
{
controller.WaitUntilOnline(c);
}
return rc;
});
}
public PodInfo GetPodInfo(RunningContainer container)
{
return K8s(c => c.GetPodInfo(container.RunningPod.StartResult.Deployment));
return K8s(c => c.GetPodInfo(container.RunningContainers.StartResult.Deployment));
}
public PodInfo GetPodInfo(RunningPod pod)
public PodInfo GetPodInfo(RunningContainers containers)
{
return K8s(c => c.GetPodInfo(pod.StartResult.Deployment));
return K8s(c => c.GetPodInfo(containers.StartResult.Deployment));
}
public CrashWatcher CreateCrashWatcher(RunningContainer container)
@@ -97,12 +86,12 @@ namespace KubernetesWorkflow
return K8s(c => c.CreateCrashWatcher(container));
}
public void Stop(RunningPod runningPod, bool waitTillStopped)
public void Stop(RunningContainers runningContainers)
{
K8s(controller =>
{
controller.Stop(runningPod.StartResult, waitTillStopped);
cluster.Configuration.Hooks.OnContainersStopped(runningPod);
controller.Stop(runningContainers.StartResult);
cluster.Configuration.Hooks.OnContainersStopped(runningContainers);
});
}
@@ -1,20 +0,0 @@
namespace KubernetesWorkflow.Types
{
public class FutureContainers
{
private readonly RunningPod runningPod;
private readonly StartupWorkflow workflow;
public FutureContainers(RunningPod runningPod, StartupWorkflow workflow)
{
this.runningPod = runningPod;
this.workflow = workflow;
}
public RunningPod WaitForOnline()
{
workflow.WaitUntilOnline(runningPod);
return runningPod;
}
}
}
@@ -19,7 +19,7 @@ namespace KubernetesWorkflow.Types
public ContainerAddress[] Addresses { get; }
[JsonIgnore]
public RunningPod RunningPod { get; internal set; } = null!;
public RunningContainers RunningContainers { get; internal set; } = null!;
public Address GetAddress(ILog log, string portTag)
{
@@ -2,15 +2,15 @@
namespace KubernetesWorkflow.Types
{
public class RunningPod
public class RunningContainers
{
public RunningPod(StartupConfig startupConfig, StartResult startResult, RunningContainer[] containers)
public RunningContainers(StartupConfig startupConfig, StartResult startResult, RunningContainer[] containers)
{
StartupConfig = startupConfig;
StartResult = startResult;
Containers = containers;
foreach (var c in containers) c.RunningPod = this;
foreach (var c in containers) c.RunningContainers = this;
}
public StartupConfig StartupConfig { get; }
@@ -31,7 +31,12 @@ namespace KubernetesWorkflow.Types
public static class RunningContainersExtensions
{
public static string Describe(this RunningPod[] runningContainers)
public static RunningContainer[] Containers(this RunningContainers[] runningContainers)
{
return runningContainers.SelectMany(c => c.Containers).ToArray();
}
public static string Describe(this RunningContainers[] runningContainers)
{
return string.Join(",", runningContainers.Select(c => c.Describe()));
}
+5 -12
View File
@@ -3,21 +3,14 @@
public class LogPrefixer : ILog
{
private readonly ILog backingLog;
public LogPrefixer(ILog backingLog)
{
this.backingLog = backingLog;
}
private readonly string prefix;
public LogPrefixer(ILog backingLog, string prefix)
{
this.backingLog = backingLog;
Prefix = prefix;
this.prefix = prefix;
}
public string Prefix { get; set; } = string.Empty;
public LogFile CreateSubfile(string ext = "log")
{
return backingLog.CreateSubfile(ext);
@@ -25,17 +18,17 @@
public void Debug(string message = "", int skipFrames = 0)
{
backingLog.Debug(Prefix + message, skipFrames);
backingLog.Debug(prefix + message, skipFrames);
}
public void Error(string message)
{
backingLog.Error(Prefix + message);
backingLog.Error(prefix + message);
}
public void Log(string message)
{
backingLog.Log(Prefix + message);
backingLog.Log(prefix + message);
}
public void AddStringReplace(string from, string to)
@@ -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,41 +0,0 @@
namespace NethereumWorkflow.BlockUtils
{
public class BlockCache
{
public delegate void CacheClearedEvent();
private const int MaxEntries = 1024 * 1024 * 5;
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;
}
public int Size { get { return entries.Count; } }
}
}
@@ -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,111 +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 BlockTimeEntry Get(ulong blockNumber)
{
var b = cache.Get(blockNumber);
if (b != null) return b;
return GetBlock(blockNumber);
}
public ulong? GetHighestBlockNumberBefore(DateTime moment)
{
bounds.Initialize();
if (moment <= bounds.Genesis.Utc) return null;
if (moment >= bounds.Current.Utc) return bounds.Current.BlockNumber;
return Log(() => 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 Log(()=> Search(bounds.Genesis, bounds.Current, moment, LowestAfterSelector)); ;
}
private ulong Log(Func<ulong> operation)
{
var sw = Stopwatch.Begin(log, nameof(BlockTimeFinder));
var result = operation();
sw.End($"(Bounds: [{bounds.Genesis.BlockNumber}-{bounds.Current.BlockNumber}] Cache: {cache.Size})");
return result;
}
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;
@@ -89,9 +86,14 @@ namespace NethereumWorkflow
}
}
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, BlockInterval blockRange) where TEvent : IEventDTO, new()
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, TimeRange timeRange) where TEvent : IEventDTO, new()
{
return GetEvents<TEvent>(address, blockRange.From, blockRange.To);
var blockTimeFinder = new BlockTimeFinder(web3, log);
var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(timeRange.From);
var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(timeRange.To);
return GetEvents<TEvent>(address, fromBlock, toBlock);
}
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new()
@@ -102,31 +104,5 @@ namespace NethereumWorkflow
var blockFilter = Time.Wait(eventHandler.CreateFilterBlockRangeAsync(from, to));
return Time.Wait(eventHandler.GetAllChangesAsync(blockFilter));
}
public BlockInterval ConvertTimeRangeToBlockRange(TimeRange timeRange)
{
var wrapper = new Web3Wrapper(web3, log);
var blockTimeFinder = new BlockTimeFinder(blockCache, wrapper, log);
var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(timeRange.From);
var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(timeRange.To);
if (fromBlock == null || toBlock == null)
{
throw new Exception("Failed to convert time range to block range.");
}
return new BlockInterval(
from: fromBlock.Value,
to: toBlock.Value
);
}
public BlockTimeEntry GetBlockForNumber(ulong number)
{
var wrapper = new Web3Wrapper(web3, log);
var blockTimeFinder = new BlockTimeFinder(blockCache, wrapper, log);
return blockTimeFinder.Get(number);
}
}
}
@@ -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;
}
}
}
}
-27
View File
@@ -1,27 +0,0 @@
namespace Utils
{
public class BlockInterval
{
public BlockInterval(ulong from, ulong to)
{
if (from < to)
{
From = from;
To = to;
}
else
{
From = to;
To = from;
}
}
public ulong From { get; }
public ulong To { get; }
public override string ToString()
{
return $"[{From} - {To}]";
}
}
}
+1 -3
View File
@@ -2,9 +2,6 @@
{
public class ByteSize
{
public static readonly ByteSize Zero = new ByteSize(0);
public const double DefaultSecondsPerMB = 10.0;
public ByteSize(long sizeInBytes)
{
if (sizeInBytes < 0) throw new ArgumentException("Cannot create ByteSize object with size less than 0. Was: " + sizeInBytes);
@@ -13,6 +10,7 @@
public long SizeInBytes { get; }
public const double DefaultSecondsPerMB = 10.0;
public long ToMB()
{
+2 -4
View File
@@ -1,6 +1,4 @@
using System.Globalization;
namespace Utils
namespace Utils
{
public static class Formatter
{
@@ -12,7 +10,7 @@ namespace Utils
var sizeOrder = Convert.ToInt32(Math.Floor(Math.Log(bytes, 1024)));
var digit = Math.Round(bytes / Math.Pow(1024, sizeOrder), 1);
return digit.ToString(CultureInfo.InvariantCulture) + sizeSuffixes[sizeOrder];
return digit.ToString() + sizeSuffixes[sizeOrder];
}
}
}
+2 -7
View File
@@ -2,7 +2,6 @@
{
public class NumberSource
{
private readonly object @lock = new object();
private int number;
public NumberSource(int start)
@@ -12,12 +11,8 @@
public int GetNextNumber()
{
var n = -1;
lock (@lock)
{
n = number;
number++;
}
var n = number;
number++;
return n;
}
}
+5 -8
View File
@@ -57,27 +57,24 @@
return result;
}
public static void WaitUntil(Func<bool> predicate, string msg)
public static void WaitUntil(Func<bool> predicate)
{
WaitUntil(predicate, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(1), msg);
WaitUntil(predicate, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(1));
}
public static void WaitUntil(Func<bool> predicate, TimeSpan timeout, TimeSpan retryDelay, string msg)
public static void WaitUntil(Func<bool> predicate, TimeSpan timeout, TimeSpan retryDelay)
{
var start = DateTime.UtcNow;
var tries = 1;
var state = predicate();
while (!state)
{
var duration = DateTime.UtcNow - start;
if (duration > timeout)
if (DateTime.UtcNow - start > timeout)
{
throw new TimeoutException($"Operation timed out after {tries} tries over (total) {FormatDuration(duration)}. '{msg}'");
throw new TimeoutException("Operation timed out.");
}
Sleep(retryDelay);
state = predicate();
tries++;
}
}
@@ -5,9 +5,6 @@ using Nethereum.ABI;
using Nethereum.Hex.HexTypes;
using Nethereum.Util;
using NethereumWorkflow;
using NethereumWorkflow.BlockUtils;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Utils;
namespace CodexContractsPlugin
@@ -22,16 +19,15 @@ namespace CodexContractsPlugin
TestToken GetTestTokenBalance(IHasEthAddress owner);
TestToken GetTestTokenBalance(EthAddress ethAddress);
Request[] GetStorageRequests(BlockInterval blockRange);
Request[] GetStorageRequests(TimeRange timeRange);
EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex);
RequestState GetRequestState(Request request);
RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange);
RequestCancelledEventDTO[] GetRequestCancelledEvents(BlockInterval blockRange);
SlotFilledEventDTO[] GetSlotFilledEvents(BlockInterval blockRange);
SlotFreedEventDTO[] GetSlotFreedEvents(BlockInterval blockRange);
RequestFulfilledEventDTO[] GetRequestFulfilledEvents(TimeRange timeRange);
RequestCancelledEventDTO[] GetRequestCancelledEvents(TimeRange timeRange);
SlotFilledEventDTO[] GetSlotFilledEvents(TimeRange timeRange);
SlotFreedEventDTO[] GetSlotFreedEvents(TimeRange timeRange);
}
[JsonConverter(typeof(StringEnumConverter))]
public enum RequestState
{
New,
@@ -81,63 +77,63 @@ namespace CodexContractsPlugin
return balance.TestTokens();
}
public Request[] GetStorageRequests(BlockInterval blockRange)
public Request[] GetStorageRequests(TimeRange timeRange)
{
var events = gethNode.GetEvents<StorageRequestedEventDTO>(Deployment.MarketplaceAddress, blockRange);
var events = gethNode.GetEvents<StorageRequestedEventDTO>(Deployment.MarketplaceAddress, timeRange);
var i = StartInteraction();
return events
.Select(e =>
{
var requestEvent = i.GetRequest(Deployment.MarketplaceAddress, e.Event.RequestId);
var result = requestEvent.ReturnValue1;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.BlockNumber = e.Log.BlockNumber.ToUlong();
result.RequestId = e.Event.RequestId;
return result;
})
.ToArray();
}
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents(BlockInterval blockRange)
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents(TimeRange timeRange)
{
var events = gethNode.GetEvents<RequestFulfilledEventDTO>(Deployment.MarketplaceAddress, blockRange);
var events = gethNode.GetEvents<RequestFulfilledEventDTO>(Deployment.MarketplaceAddress, timeRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.BlockNumber = e.Log.BlockNumber.ToUlong();
return result;
}).ToArray();
}
public RequestCancelledEventDTO[] GetRequestCancelledEvents(BlockInterval blockRange)
public RequestCancelledEventDTO[] GetRequestCancelledEvents(TimeRange timeRange)
{
var events = gethNode.GetEvents<RequestCancelledEventDTO>(Deployment.MarketplaceAddress, blockRange);
var events = gethNode.GetEvents<RequestCancelledEventDTO>(Deployment.MarketplaceAddress, timeRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.BlockNumber = e.Log.BlockNumber.ToUlong();
return result;
}).ToArray();
}
public SlotFilledEventDTO[] GetSlotFilledEvents(BlockInterval blockRange)
public SlotFilledEventDTO[] GetSlotFilledEvents(TimeRange timeRange)
{
var events = gethNode.GetEvents<SlotFilledEventDTO>(Deployment.MarketplaceAddress, blockRange);
var events = gethNode.GetEvents<SlotFilledEventDTO>(Deployment.MarketplaceAddress, timeRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.BlockNumber = e.Log.BlockNumber.ToUlong();
result.Host = GetEthAddressFromTransaction(e.Log.TransactionHash);
return result;
}).ToArray();
}
public SlotFreedEventDTO[] GetSlotFreedEvents(BlockInterval blockRange)
public SlotFreedEventDTO[] GetSlotFreedEvents(TimeRange timeRange)
{
var events = gethNode.GetEvents<SlotFreedEventDTO>(Deployment.MarketplaceAddress, blockRange);
var events = gethNode.GetEvents<SlotFreedEventDTO>(Deployment.MarketplaceAddress, timeRange);
return events.Select(e =>
{
var result = e.Event;
result.Block = GetBlock(e.Log.BlockNumber.ToUlong());
result.BlockNumber = e.Log.BlockNumber.ToUlong();
return result;
}).ToArray();
}
@@ -170,11 +166,6 @@ namespace CodexContractsPlugin
return gethNode.Call<RequestStateFunction, RequestState>(Deployment.MarketplaceAddress, func);
}
private BlockTimeEntry GetBlock(ulong number)
{
return gethNode.GetBlockForNumber(number);
}
private EthAddress GetEthAddressFromTransaction(string transactionHash)
{
var transaction = gethNode.GetTransaction(transactionHash);
@@ -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";
@@ -24,7 +24,7 @@ namespace CodexContractsPlugin
var startupConfig = CreateStartupConfig(gethNode);
startupConfig.NameOverride = "codex-contracts";
var containers = workflow.Start(1, new CodexContractsContainerRecipe(), startupConfig).WaitForOnline();
var containers = workflow.Start(1, new CodexContractsContainerRecipe(), startupConfig);
if (containers.Containers.Length != 1) throw new InvalidOperationException("Expected 1 Codex contracts container to be created. Test infra failure.");
var container = containers.Containers[0];
@@ -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;
}
@@ -59,7 +59,7 @@ namespace CodexContractsPlugin
var logHandler = new ContractsReadyLogHandler(tools.GetLog());
workflow.DownloadContainerLog(container, logHandler, 100);
return logHandler.Found;
}, nameof(DeployContract));
});
Log("Contracts deployed. Extracting addresses...");
var extractor = new ContractsContainerInfoExtractor(tools.GetLog(), workflow, container);
@@ -71,7 +71,7 @@ namespace CodexContractsPlugin
Log("Extract completed. Checking sync...");
Time.WaitUntil(() => interaction.IsSynced(marketplaceAddress, abi), nameof(DeployContract));
Time.WaitUntil(() => interaction.IsSynced(marketplaceAddress, abi));
Log("Synced. Codex SmartContracts deployed.");
@@ -83,9 +83,9 @@ namespace CodexContractsPlugin
tools.GetLog().Log(msg);
}
private void WaitUntil(Func<bool> predicate, string msg)
private void WaitUntil(Func<bool> predicate)
{
Time.WaitUntil(predicate, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(2), msg);
Time.WaitUntil(predicate, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(2));
}
private StartupConfig CreateStartupConfig(IGethNode gethNode)
@@ -1,5 +1,4 @@
using CodexContractsPlugin.Marketplace;
using KubernetesWorkflow;
using KubernetesWorkflow;
using KubernetesWorkflow.Types;
using Logging;
using Newtonsoft.Json;
@@ -54,18 +53,7 @@ namespace CodexContractsPlugin
var artifact = JObject.Parse(json);
var abi = artifact["abi"];
var byteCode = artifact["bytecode"];
var abiResult = abi!.ToString(Formatting.None);
var byteCodeResult = byteCode!.ToString(Formatting.None);
if (byteCodeResult
.ToLowerInvariant()
.Replace("\"", "") != MarketplaceDeploymentBase.BYTECODE.ToLowerInvariant())
{
throw new Exception("BYTECODE in CodexContractsPlugin does not match BYTECODE deployed by container. Update Marketplace.cs generated code?");
}
return abiResult;
return abi!.ToString(Formatting.None);
}
private static string Retry(Func<string> fetch)
@@ -1,14 +1,11 @@
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
using GethPlugin;
using NethereumWorkflow.BlockUtils;
using Newtonsoft.Json;
namespace CodexContractsPlugin.Marketplace
{
public partial class Request : RequestBase
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
public ulong BlockNumber { get; set; }
public byte[] RequestId { get; set; }
public EthAddress ClientAddress { get { return new EthAddress(Client); } }
@@ -16,27 +13,23 @@ namespace CodexContractsPlugin.Marketplace
public partial class RequestFulfilledEventDTO
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
public ulong BlockNumber { get; set; }
}
public partial class RequestCancelledEventDTO
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
public ulong BlockNumber { get; set; }
}
public partial class SlotFilledEventDTO
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
public ulong BlockNumber { get; set; }
public EthAddress Host { get; set; }
}
public partial class SlotFreedEventDTO
{
[JsonIgnore]
public BlockTimeEntry Block { get; set; }
public ulong BlockNumber { get; set; }
}
}
#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
@@ -29,31 +29,18 @@ namespace CodexDiscordBotPlugin
{
}
public RunningPod Deploy(DiscordBotStartupConfig config)
public RunningContainers Deploy(DiscordBotStartupConfig config)
{
var workflow = tools.CreateWorkflow();
return StartContainer(workflow, config);
}
public RunningPod DeployRewarder(RewarderBotStartupConfig config)
{
var workflow = tools.CreateWorkflow();
return StartRewarderContainer(workflow, config);
}
private RunningPod StartContainer(IStartupWorkflow workflow, DiscordBotStartupConfig config)
private RunningContainers StartContainer(IStartupWorkflow workflow, DiscordBotStartupConfig config)
{
var startupConfig = new StartupConfig();
startupConfig.NameOverride = config.Name;
startupConfig.Add(config);
return workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig).WaitForOnline();
}
private RunningPod StartRewarderContainer(IStartupWorkflow workflow, RewarderBotStartupConfig config)
{
var startupConfig = new StartupConfig();
startupConfig.Add(config);
return workflow.Start(1, new RewarderBotContainerRecipe(), startupConfig).WaitForOnline();
return workflow.Start(1, new DiscordBotContainerRecipe(), startupConfig);
}
}
}
@@ -5,16 +5,11 @@ namespace CodexDiscordBotPlugin
{
public static class CoreInterfaceExtensions
{
public static RunningPod DeployCodexDiscordBot(this CoreInterface ci, DiscordBotStartupConfig config)
public static RunningContainers DeployCodexDiscordBot(this CoreInterface ci, DiscordBotStartupConfig config)
{
return Plugin(ci).Deploy(config);
}
public static RunningPod DeployRewarderBot(this CoreInterface ci, RewarderBotStartupConfig config)
{
return Plugin(ci).DeployRewarder(config);
}
private static CodexDiscordBotPlugin Plugin(CoreInterface ci)
{
return ci.GetPlugin<CodexDiscordBotPlugin>();
@@ -7,9 +7,7 @@ namespace CodexDiscordBotPlugin
public class DiscordBotContainerRecipe : ContainerRecipeFactory
{
public override string AppName => "discordbot-bibliotech";
public override string Image => "codexstorage/codex-discordbot:sha-8c64352";
public static string RewardsPort = "bot_rewards_port";
public override string Image => "thatbenbierens/codex-discordbot:initial";
protected override void Initialize(StartupConfig startupConfig)
{
@@ -21,7 +19,6 @@ namespace CodexDiscordBotPlugin
AddEnvVar("SERVERNAME", config.ServerName);
AddEnvVar("ADMINROLE", config.AdminRoleName);
AddEnvVar("ADMINCHANNELNAME", config.AdminChannelName);
AddEnvVar("REWARDSCHANNELNAME", config.RewardChannelName);
AddEnvVar("KUBECONFIG", "/opt/kubeconfig.yaml");
AddEnvVar("KUBENAMESPACE", config.KubeNamespace);
@@ -33,13 +30,13 @@ namespace CodexDiscordBotPlugin
AddEnvVar("CODEXCONTRACTS_TOKENADDRESS", gethInfo.TokenAddress);
AddEnvVar("CODEXCONTRACTS_ABI", gethInfo.Abi);
AddInternalPortAndVar("REWARDAPIPORT", RewardsPort);
if (!string.IsNullOrEmpty(config.DataPath))
{
AddEnvVar("DATAPATH", config.DataPath);
AddVolume(config.DataPath, 1.GB());
}
AddVolume(name: "kubeconfig", mountPath: "/opt/kubeconfig.yaml", subPath: "kubeconfig.yaml", secret: "discordbot-sa-kubeconfig");
}
}
}
@@ -2,7 +2,7 @@
{
public class DiscordBotStartupConfig
{
public DiscordBotStartupConfig(string name, string token, string serverName, string adminRoleName, string adminChannelName, string kubeNamespace, DiscordBotGethInfo gethInfo, string rewardChannelName)
public DiscordBotStartupConfig(string name, string token, string serverName, string adminRoleName, string adminChannelName, string kubeNamespace, DiscordBotGethInfo gethInfo)
{
Name = name;
Token = token;
@@ -11,7 +11,6 @@
AdminChannelName = adminChannelName;
KubeNamespace = kubeNamespace;
GethInfo = gethInfo;
RewardChannelName = rewardChannelName;
}
public string Name { get; }
@@ -19,32 +18,11 @@
public string ServerName { get; }
public string AdminRoleName { get; }
public string AdminChannelName { get; }
public string RewardChannelName { get; }
public string KubeNamespace { get; }
public DiscordBotGethInfo GethInfo { get; }
public string? DataPath { get; set; }
}
public class RewarderBotStartupConfig
{
public RewarderBotStartupConfig(string discordBotHost, int discordBotPort, string intervalMinutes, DateTime historyStartUtc, DiscordBotGethInfo gethInfo, string? dataPath)
{
DiscordBotHost = discordBotHost;
DiscordBotPort = discordBotPort;
IntervalMinutes = intervalMinutes;
HistoryStartUtc = historyStartUtc;
GethInfo = gethInfo;
DataPath = dataPath;
}
public string DiscordBotHost { get; }
public int DiscordBotPort { get; }
public string IntervalMinutes { get; }
public DateTime HistoryStartUtc { get; }
public DiscordBotGethInfo GethInfo { get; }
public string? DataPath { get; set; }
}
public class DiscordBotGethInfo
{
public DiscordBotGethInfo(string host, int port, string privKey, string marketplaceAddress, string tokenAddress, string abi)
@@ -1,39 +0,0 @@
using KubernetesWorkflow.Recipe;
using KubernetesWorkflow;
using Utils;
namespace CodexDiscordBotPlugin
{
public class RewarderBotContainerRecipe : ContainerRecipeFactory
{
public override string AppName => "discordbot-rewarder";
public override string Image => "codexstorage/codex-rewarderbot:sha-2ab84e2";
protected override void Initialize(StartupConfig startupConfig)
{
var config = startupConfig.Get<RewarderBotStartupConfig>();
SetSchedulingAffinity(notIn: "false");
AddEnvVar("DISCORDBOTHOST", config.DiscordBotHost);
AddEnvVar("DISCORDBOTPORT", config.DiscordBotPort.ToString());
AddEnvVar("INTERVALMINUTES", config.IntervalMinutes);
var offset = new DateTimeOffset(config.HistoryStartUtc);
AddEnvVar("CHECKHISTORY", offset.ToUnixTimeSeconds().ToString());
var gethInfo = config.GethInfo;
AddEnvVar("GETH_HOST", gethInfo.Host);
AddEnvVar("GETH_HTTP_PORT", gethInfo.Port.ToString());
AddEnvVar("GETH_PRIVATE_KEY", gethInfo.PrivKey);
AddEnvVar("CODEXCONTRACTS_MARKETPLACEADDRESS", gethInfo.MarketplaceAddress);
AddEnvVar("CODEXCONTRACTS_TOKENADDRESS", gethInfo.TokenAddress);
AddEnvVar("CODEXCONTRACTS_ABI", gethInfo.Abi);
if (!string.IsNullOrEmpty(config.DataPath))
{
AddEnvVar("DATAPATH", config.DataPath);
AddVolume(config.DataPath, 1.GB());
}
}
}
}
-92
View File
@@ -1,92 +0,0 @@
using Core;
using KubernetesWorkflow.Types;
using Logging;
using System.Security.Cryptography;
using System.Text;
namespace CodexPlugin
{
public class ApiChecker
{
// <INSERT-OPENAPI-YAML-HASH>
private const string OpenApiYamlHash = "5A-B0-2A-AC-42-B1-A2-49-6F-9D-4E-D8-56-40-10-A6-67-F4-0D-2A-9F-E0-84-5C-EB-B8-2D-4F-D8-56-79-6C";
private const string OpenApiFilePath = "/codex/openapi.yaml";
private const string DisableEnvironmentVariable = "CODEXPLUGIN_DISABLE_APICHECK";
private const bool Disable = false;
private const string Warning =
"Warning: CodexPlugin was unable to find the openapi.yaml file in the Codex container. Are you running an old version of Codex? " +
"Plugin will continue as normal, but API compatibility is not guaranteed!";
private const string Failure =
"Codex API compatibility check failed! " +
"openapi.yaml used by CodexPlugin does not match openapi.yaml in Codex container. Please update the openapi.yaml in " +
"'ProjectPlugins/CodexPlugin' and rebuild this project. If you wish to disable API compatibility checking, please set " +
$"the environment variable '{DisableEnvironmentVariable}' or set the disable bool in 'ProjectPlugins/CodexPlugin/ApiChecker.cs'.";
private static bool checkPassed = false;
private readonly IPluginTools pluginTools;
private readonly ILog log;
public ApiChecker(IPluginTools pluginTools)
{
this.pluginTools = pluginTools;
log = pluginTools.GetLog();
if (string.IsNullOrEmpty(OpenApiYamlHash)) throw new Exception("OpenAPI yaml hash was not inserted by pre-build trigger.");
}
public void CheckCompatibility(RunningPod[] containers)
{
if (checkPassed) return;
Log("CodexPlugin is checking API compatibility...");
if (Disable || !string.IsNullOrEmpty(Environment.GetEnvironmentVariable(DisableEnvironmentVariable)))
{
Log("API compatibility checking has been disabled.");
checkPassed = true;
return;
}
var workflow = pluginTools.CreateWorkflow();
var container = containers.First().Containers.First();
var containerApi = workflow.ExecuteCommand(container, "cat", OpenApiFilePath);
if (string.IsNullOrEmpty(containerApi))
{
log.Error(Warning);
checkPassed = true;
return;
}
var containerHash = Hash(containerApi);
if (containerHash == OpenApiYamlHash)
{
Log("API compatibility check passed.");
checkPassed = true;
return;
}
log.Error(Failure);
throw new Exception(Failure);
}
private string Hash(string file)
{
var fileBytes = Encoding.ASCII.GetBytes(file
.Replace(Environment.NewLine, ""));
var sha = SHA256.Create();
var hash = sha.ComputeHash(fileBytes);
return BitConverter.ToString(hash);
}
private void Log(string msg)
{
log.Log(msg);
}
}
}
+41 -58
View File
@@ -1,8 +1,6 @@
using CodexOpenApi;
using Core;
using Core;
using KubernetesWorkflow;
using KubernetesWorkflow.Types;
using Newtonsoft.Json;
using Utils;
namespace CodexPlugin
@@ -10,10 +8,9 @@ namespace CodexPlugin
public class CodexAccess : ILogHandler
{
private readonly IPluginTools tools;
private readonly Mapper mapper = new Mapper();
private bool hasContainerCrashed;
public CodexAccess(IPluginTools tools, RunningPod container, CrashWatcher crashWatcher)
public CodexAccess(IPluginTools tools, RunningContainer container, CrashWatcher crashWatcher)
{
this.tools = tools;
Container = container;
@@ -23,84 +20,80 @@ namespace CodexPlugin
CrashWatcher.Start(this);
}
public RunningPod Container { get; }
public RunningContainer Container { get; }
public CrashWatcher CrashWatcher { get; }
public DebugInfo GetDebugInfo()
public CodexDebugResponse GetDebugInfo()
{
return mapper.Map(OnCodex(api => api.GetDebugInfoAsync()));
return Http().HttpGetJson<CodexDebugResponse>("debug/info");
}
public DebugPeer GetDebugPeer(string peerId)
public CodexDebugPeerResponse GetDebugPeer(string peerId)
{
// Cannot use openAPI: debug/peer endpoint is not specified there.
var endpoint = GetEndpoint();
var str = endpoint.HttpGetString($"debug/peer/{peerId}");
var http = Http();
var str = http.HttpGetString($"debug/peer/{peerId}");
if (str.ToLowerInvariant() == "unable to find peer!")
{
return new DebugPeer
return new CodexDebugPeerResponse
{
IsPeerFound = false
};
}
var result = endpoint.Deserialize<DebugPeer>(str);
var result = http.Deserialize<CodexDebugPeerResponse>(str);
result.IsPeerFound = true;
return result;
}
public void ConnectToPeer(string peerId, string[] peerMultiAddresses)
public CodexDebugBlockExchangeResponse GetDebugBlockExchange()
{
OnCodex(api =>
{
Time.Wait(api.ConnectPeerAsync(peerId, peerMultiAddresses));
return Task.FromResult(string.Empty);
});
return Http().HttpGetJson<CodexDebugBlockExchangeResponse>("debug/blockexchange");
}
public CodexDebugRepoStoreResponse[] GetDebugRepoStore()
{
return LongHttp().HttpGetJson<CodexDebugRepoStoreResponse[]>("debug/repostore");
}
public CodexDebugThresholdBreaches GetDebugThresholdBreaches()
{
return Http().HttpGetJson<CodexDebugThresholdBreaches>("debug/loop");
}
public string UploadFile(FileStream fileStream)
{
return OnCodex(api => api.UploadAsync(fileStream));
return Http().HttpPostStream("data", fileStream);
}
public Stream DownloadFile(string contentId)
{
var fileResponse = OnCodex(api => api.DownloadNetworkAsync(contentId));
if (fileResponse.StatusCode != 200) throw new Exception("Download failed with StatusCode: " + fileResponse.StatusCode);
return fileResponse.Stream;
return Http().HttpGetStream("data/" + contentId + "/network");
}
public LocalDatasetList LocalFiles()
public CodexLocalDataResponse[] LocalFiles()
{
return mapper.Map(OnCodex(api => api.ListDataAsync()));
return Http().HttpGetJson<CodexLocalDataResponse[]>("data");
}
public StorageAvailability SalesAvailability(StorageAvailability request)
public CodexSalesAvailabilityResponse SalesAvailability(CodexSalesAvailabilityRequest request)
{
var body = mapper.Map(request);
var read = OnCodex<SalesAvailabilityREAD>(api => api.OfferStorageAsync(body));
return mapper.Map(read);
return Http().HttpPostJson<CodexSalesAvailabilityRequest, CodexSalesAvailabilityResponse>("sales/availability", request);
}
public string RequestStorage(StoragePurchaseRequest request)
public string RequestStorage(CodexSalesRequestStorageRequest request, string contentId)
{
var body = mapper.Map(request);
return OnCodex<string>(api => api.CreateStorageRequestAsync(request.ContentId.Id, body));
return Http().HttpPostJson($"storage/request/{contentId}", request);
}
public StoragePurchase GetPurchaseStatus(string purchaseId)
public CodexStoragePurchase GetPurchaseStatus(string purchaseId)
{
var endpoint = GetEndpoint();
return Time.Retry(() =>
{
var str = endpoint.HttpGetString($"storage/purchases/{purchaseId}");
if (string.IsNullOrEmpty(str)) throw new Exception("Empty response.");
return JsonConvert.DeserializeObject<StoragePurchase>(str)!;
}, nameof(GetPurchaseStatus));
return Http().HttpGetJson<CodexStoragePurchase>($"storage/purchases/{purchaseId}");
}
// TODO: current getpurchase api does not line up with its openapi spec.
// return mapper.Map(OnCodex(api => api.GetPurchaseAsync(purchaseId)));
public string ConnectToPeer(string peerId, string peerMultiAddress)
{
return Http().HttpGetString($"connect/{peerId}?addrs={peerMultiAddress}");
}
public string GetName()
@@ -114,29 +107,19 @@ namespace CodexPlugin
return workflow.GetPodInfo(Container);
}
private T OnCodex<T>(Func<CodexApi, Task<T>> action)
private IHttp Http()
{
var address = GetAddress();
var result = tools.CreateHttp(CheckContainerCrashed)
.OnClient(client =>
{
var api = new CodexApi(client);
api.BaseUrl = $"{address.Host}:{address.Port}/api/codex/v1";
return Time.Wait(action(api));
});
return result;
return tools.CreateHttp(GetAddress(), baseUrl: "/api/codex/v1", CheckContainerCrashed, Container.Name);
}
private IEndpoint GetEndpoint()
private IHttp LongHttp()
{
return tools
.CreateHttp(CheckContainerCrashed)
.CreateEndpoint(GetAddress(), "/api/codex/v1/", Container.Name);
return tools.CreateHttp(GetAddress(), baseUrl: "/api/codex/v1", CheckContainerCrashed, new LongTimeSet(), Container.Name);
}
private Address GetAddress()
{
return Container.Containers.Single().GetAddress(tools.GetLog(), CodexContainerRecipe.ApiPortTag);
return Container.GetAddress(tools.GetLog(), CodexContainerRecipe.ApiPortTag);
}
private void CheckContainerCrashed(HttpClient client)
+195
View File
@@ -0,0 +1,195 @@
using Newtonsoft.Json;
namespace CodexPlugin
{
public class CodexDebugResponse
{
public string id { get; set; } = string.Empty;
public string[] addrs { get; set; } = Array.Empty<string>();
public string repo { get; set; } = string.Empty;
public string spr { get; set; } = string.Empty;
public string[] announceAddresses { get; set; } = Array.Empty<string>();
public EnginePeerResponse[] enginePeers { get; set; } = Array.Empty<EnginePeerResponse>();
public SwitchPeerResponse[] switchPeers { get; set; } = Array.Empty<SwitchPeerResponse>();
public CodexDebugVersionResponse codex { get; set; } = new();
public CodexDebugTableResponse table { get; set; } = new();
}
public class CodexDebugFutures
{
public int futures { get; set; }
}
public class CodexDebugTableResponse
{
public CodexDebugTableNodeResponse localNode { get; set; } = new();
public CodexDebugTableNodeResponse[] nodes { get; set; } = Array.Empty<CodexDebugTableNodeResponse>();
}
public class CodexDebugTableNodeResponse
{
public string nodeId { get; set; } = string.Empty;
public string peerId { get; set; } = string.Empty;
public string record { get; set; } = string.Empty;
public string address { get; set; } = string.Empty;
public bool seen { get; set; }
}
public class EnginePeerResponse
{
public string peerId { get; set; } = string.Empty;
public EnginePeerContextResponse context { get; set; } = new();
}
public class EnginePeerContextResponse
{
public int blocks { get; set; } = 0;
public int peerWants { get; set; } = 0;
public int exchanged { get; set; } = 0;
public string lastExchange { get; set; } = string.Empty;
}
public class SwitchPeerResponse
{
public string peerId { get; set; } = string.Empty;
public string key { get; set; } = string.Empty;
}
public class CodexDebugVersionResponse
{
public string version { get; set; } = string.Empty;
public string revision { get; set; } = string.Empty;
public bool IsValid()
{
return !string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(revision);
}
public override string ToString()
{
return JsonConvert.SerializeObject(this);
}
}
public class CodexDebugPeerResponse
{
public bool IsPeerFound { get; set; }
public string peerId { get; set; } = string.Empty;
public long seqNo { get; set; }
public string[] addresses { get; set; } = Array.Empty<string>();
}
public class CodexDebugThresholdBreaches
{
public string[] breaches { get; set; } = Array.Empty<string>();
}
public class CodexSalesAvailabilityRequest
{
public string size { get; set; } = string.Empty;
public string duration { get; set; } = string.Empty;
public string minPrice { get; set; } = string.Empty;
public string maxCollateral { get; set; } = string.Empty;
}
public class CodexSalesAvailabilityResponse
{
public string id { get; set; } = string.Empty;
public string size { get; set; } = string.Empty;
public string duration { get; set; } = string.Empty;
public string minPrice { get; set; } = string.Empty;
public string maxCollateral { get; set; } = string.Empty;
}
public class CodexSalesRequestStorageRequest
{
public string duration { get; set; } = string.Empty;
public string proofProbability { get; set; } = string.Empty;
public string reward { get; set; } = string.Empty;
public string collateral { get; set; } = string.Empty;
public string? expiry { get; set; }
public uint? nodes { get; set; }
public uint? tolerance { get; set; }
}
public class CodexStoragePurchase
{
public string state { get; set; } = string.Empty;
public string error { get; set; } = string.Empty;
}
public class CodexDebugBlockExchangeResponse
{
public CodexDebugBlockExchangeResponsePeer[] peers { get; set; } = Array.Empty<CodexDebugBlockExchangeResponsePeer>();
public int taskQueue { get; set; }
public int pendingBlocks { get; set; }
public override string ToString()
{
if (peers.Length == 0 && taskQueue == 0 && pendingBlocks == 0) return "all-empty";
return $"taskqueue: {taskQueue} pendingblocks: {pendingBlocks} peers: {string.Join(",", peers.Select(p => p.ToString()))}";
}
}
public class CodexDebugBlockExchangeResponsePeer
{
public string peerid { get; set; } = string.Empty;
public CodexDebugBlockExchangeResponsePeerHasBlock[] hasBlocks { get; set; } = Array.Empty<CodexDebugBlockExchangeResponsePeerHasBlock>();
public CodexDebugBlockExchangeResponsePeerWant[] wants { get; set; } = Array.Empty<CodexDebugBlockExchangeResponsePeerWant>();
public int exchanged { get; set; }
public override string ToString()
{
return $"(blocks:{hasBlocks.Length} wants:{wants.Length})";
}
}
public class CodexDebugBlockExchangeResponsePeerHasBlock
{
public string cid { get; set; } = string.Empty;
public bool have { get; set; }
public string price { get; set; } = string.Empty;
}
public class CodexDebugBlockExchangeResponsePeerWant
{
public string block { get; set; } = string.Empty;
public int priority { get; set; }
public bool cancel { get; set; }
public string wantType { get; set; } = string.Empty;
public bool sendDontHave { get; set; }
}
public class CodexDebugRepoStoreResponse
{
public string cid { get; set; } = string.Empty;
}
public class CodexLocalData
{
public CodexLocalData(ContentId cid, CodexLocalDataManifestResponse manifest)
{
Cid = cid;
Manifest = manifest;
}
public ContentId Cid { get; }
public CodexLocalDataManifestResponse Manifest { get; }
}
public class CodexLocalDataResponse
{
public string cid { get; set; } = string.Empty;
public CodexLocalDataManifestResponse manifest { get; set; } = new();
}
public class CodexLocalDataManifestResponse
{
public string rootHash { get; set; } = string.Empty;
public int originalBytes { get; set; }
public int blockSize { get; set; }
public bool @protected { get; set; }
}
}
@@ -7,7 +7,9 @@ namespace CodexPlugin
{
public class CodexContainerRecipe : ContainerRecipeFactory
{
private const string DefaultDockerImage = "codexstorage/nim-codex:sha-1524803-dist-tests";
private readonly MarketplaceStarter marketplaceStarter = new MarketplaceStarter();
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";
@@ -103,16 +105,15 @@ namespace CodexPlugin
AddEnvVar("CODEX_ETH_PROVIDER", $"{wsAddress.Host.Replace("http://", "ws://")}:{wsAddress.Port}");
AddEnvVar("CODEX_MARKETPLACE_ADDRESS", marketplaceAddress);
var marketplaceSetup = config.MarketplaceConfig.MarketplaceSetup;
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.
AddEnvVar("PRIV_KEY", marketplaceSetup.EthAccount.PrivateKey);
Additional(marketplaceSetup.EthAccount);
var mStart = marketplaceStarter.Start();
AddEnvVar("PRIV_KEY", mStart.PrivateKey);
Additional(mStart);
SetCommandOverride(marketplaceSetup);
if (marketplaceSetup.IsValidator)
if (config.MarketplaceConfig.IsValidator)
{
AddEnvVar("CODEX_VALIDATOR", "true");
}
@@ -124,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);
+10 -10
View File
@@ -7,9 +7,9 @@ namespace CodexPlugin
public class CodexDeployment
{
public CodexDeployment(CodexInstance[] codexInstances, GethDeployment gethDeployment,
CodexContractsDeployment codexContractsDeployment, RunningPod? prometheusContainer,
RunningPod? discordBotContainer, DeploymentMetadata metadata,
String id)
CodexContractsDeployment codexContractsDeployment, RunningContainers? prometheusContainer,
RunningContainers? discordBotContainer, DeploymentMetadata metadata,
string id)
{
Id = id;
CodexInstances = codexInstances;
@@ -20,25 +20,25 @@ namespace CodexPlugin
Metadata = metadata;
}
public String Id { get; }
public string Id { get; }
public CodexInstance[] CodexInstances { get; }
public GethDeployment GethDeployment { get; }
public CodexContractsDeployment CodexContractsDeployment { get; }
public RunningPod? PrometheusContainer { get; }
public RunningPod? DiscordBotContainer { get; }
public RunningContainers? PrometheusContainer { get; }
public RunningContainers? DiscordBotContainer { get; }
public DeploymentMetadata Metadata { get; }
}
public class CodexInstance
{
public CodexInstance(RunningPod pod, DebugInfo info)
public CodexInstance(RunningContainers containers, CodexDebugResponse info)
{
Pod = pod;
Containers = containers;
Info = info;
}
public RunningPod Pod { get; }
public DebugInfo Info { get; }
public RunningContainers Containers { get; }
public CodexDebugResponse Info { get; }
}
public class DeploymentMetadata
+69 -39
View File
@@ -12,22 +12,26 @@ namespace CodexPlugin
public interface ICodexNode : IHasContainer, IHasMetricsScrapeTarget, IHasEthAddress
{
string GetName();
DebugInfo GetDebugInfo();
DebugPeer GetDebugPeer(string peerId);
CodexDebugResponse GetDebugInfo();
CodexDebugPeerResponse GetDebugPeer(string peerId);
// These debug methods are not available in master-line Codex. Use only for custom builds.
//CodexDebugBlockExchangeResponse GetDebugBlockExchange();
//CodexDebugRepoStoreResponse[] GetDebugRepoStore();
ContentId UploadFile(TrackedFile file);
TrackedFile? DownloadContent(ContentId contentId, string fileLabel = "");
LocalDatasetList LocalFiles();
CodexLocalData[] LocalFiles();
void ConnectToPeer(ICodexNode node);
DebugInfoVersion Version { get; }
CodexDebugVersionResponse Version { get; }
IMarketplaceAccess Marketplace { get; }
CrashWatcher CrashWatcher { get; }
PodInfo GetPodInfo();
ITransferSpeeds TransferSpeeds { get; }
void Stop(bool waitTillStopped);
void Stop();
}
public class CodexNode : ICodexNode
{
private const string SuccessfullyConnectedMessage = "Successfully connected to peer";
private const string UploadFailedMessage = "Unable to store block";
private readonly IPluginTools tools;
private readonly EthAddress? ethAddress;
@@ -40,28 +44,24 @@ namespace CodexPlugin
CodexAccess = codexAccess;
Group = group;
Marketplace = marketplaceAccess;
Version = new DebugInfoVersion();
Version = new CodexDebugVersionResponse();
transferSpeeds = new TransferSpeeds();
}
public RunningPod Pod { get { return CodexAccess.Container; } }
public RunningContainer Container { get { return Pod.Containers.Single(); } }
public RunningContainer Container { get { return CodexAccess.Container; } }
public CodexAccess CodexAccess { get; }
public CrashWatcher CrashWatcher { get => CodexAccess.CrashWatcher; }
public CodexNodeGroup Group { get; }
public IMarketplaceAccess Marketplace { get; }
public DebugInfoVersion Version { get; private set; }
public CodexDebugVersionResponse Version { get; private set; }
public ITransferSpeeds TransferSpeeds { get => transferSpeeds; }
public IMetricsScrapeTarget MetricsScrapeTarget
{
get
{
return new MetricsScrapeTarget(CodexAccess.Container.Containers.First(), CodexContainerRecipe.MetricsPortTag);
return new MetricsScrapeTarget(CodexAccess.Container, CodexContainerRecipe.MetricsPortTag);
}
}
public EthAddress EthAddress
{
get
@@ -73,22 +73,32 @@ namespace CodexPlugin
public string GetName()
{
return Container.Name;
return CodexAccess.Container.Name;
}
public DebugInfo GetDebugInfo()
public CodexDebugResponse GetDebugInfo()
{
var debugInfo = CodexAccess.GetDebugInfo();
var known = string.Join(",", debugInfo.Table.Nodes.Select(n => n.PeerId));
Log($"Got DebugInfo with id: '{debugInfo.Id}'. This node knows: {known}");
var known = string.Join(",", debugInfo.table.nodes.Select(n => n.peerId));
Log($"Got DebugInfo with id: '{debugInfo.id}'. This node knows: {known}");
return debugInfo;
}
public DebugPeer GetDebugPeer(string peerId)
public CodexDebugPeerResponse GetDebugPeer(string peerId)
{
return CodexAccess.GetDebugPeer(peerId);
}
public CodexDebugBlockExchangeResponse GetDebugBlockExchange()
{
return CodexAccess.GetDebugBlockExchange();
}
public CodexDebugRepoStoreResponse[] GetDebugRepoStore()
{
return CodexAccess.GetDebugRepoStore();
}
public ContentId UploadFile(TrackedFile file)
{
using var fileStream = File.OpenRead(file.Filename);
@@ -121,9 +131,9 @@ namespace CodexPlugin
return file;
}
public LocalDatasetList LocalFiles()
public CodexLocalData[] LocalFiles()
{
return CodexAccess.LocalFiles();
return CodexAccess.LocalFiles().Select(l => new CodexLocalData(new ContentId(l.cid), l.manifest)).ToArray();
}
public void ConnectToPeer(ICodexNode node)
@@ -132,8 +142,9 @@ namespace CodexPlugin
Log($"Connecting to peer {peer.GetName()}...");
var peerInfo = node.GetDebugInfo();
CodexAccess.ConnectToPeer(peerInfo.Id, GetPeerMultiAddresses(peer, peerInfo));
var response = CodexAccess.ConnectToPeer(peerInfo.id, GetPeerMultiAddress(peer, peerInfo));
FrameworkAssert.That(response == SuccessfullyConnectedMessage, "Unable to connect codex nodes.");
Log($"Successfully connected to peer {peer.GetName()}.");
}
@@ -142,44 +153,43 @@ namespace CodexPlugin
return CodexAccess.GetPodInfo();
}
public void Stop(bool waitTillStopped)
public void Stop()
{
CrashWatcher.Stop();
Group.Stop(this, waitTillStopped);
// 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);
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();
}
public void EnsureOnlineGetVersionResponse()
{
var debugInfo = Time.Retry(CodexAccess.GetDebugInfo, "ensure online");
var nodePeerId = debugInfo.Id;
var nodePeerId = debugInfo.id;
var nodeName = CodexAccess.Container.Name;
if (!debugInfo.Version.IsValid())
if (!debugInfo.codex.IsValid())
{
throw new Exception($"Invalid version information received from Codex node {GetName()}: {debugInfo.Version}");
throw new Exception($"Invalid version information received from Codex node {GetName()}: {debugInfo.codex}");
}
var log = tools.GetLog();
log.AddStringReplace(nodePeerId, nodeName);
log.AddStringReplace(debugInfo.Table.LocalNode.NodeId, nodeName);
Version = debugInfo.Version;
log.AddStringReplace(debugInfo.table.localNode.nodeId, nodeName);
Version = debugInfo.codex;
}
private string[] GetPeerMultiAddresses(CodexNode peer, DebugInfo peerInfo)
private string GetPeerMultiAddress(CodexNode peer, CodexDebugResponse peerInfo)
{
var multiAddress = peerInfo.addrs.First();
// Todo: Is there a case where First address in list is not the way?
// The peer we want to connect is in a different pod.
// We must replace the default IP with the pod IP in the multiAddress.
var workflow = tools.CreateWorkflow();
var podInfo = workflow.GetPodInfo(peer.Pod);
var podInfo = workflow.GetPodInfo(peer.Container);
return peerInfo.Addrs.Select(a => a
.Replace("0.0.0.0", podInfo.Ip))
.ToArray();
return multiAddress.Replace("0.0.0.0", podInfo.Ip);
}
private void DownloadToFile(string contentId, TrackedFile file)
@@ -202,4 +212,24 @@ namespace CodexPlugin
tools.GetLog().Log($"{GetName()}: {msg}");
}
}
public class ContentId
{
public ContentId(string id)
{
Id = id;
}
public string Id { get; }
public override bool Equals(object? obj)
{
return obj is ContentId id && Id == id.Id;
}
public override int GetHashCode()
{
return HashCode.Combine(Id);
}
}
}
@@ -35,9 +35,9 @@ namespace CodexPlugin
private EthAddress? GetEthAddress(CodexAccess access)
{
var ethAccount = access.Container.Containers.Single().Recipe.Additionals.Get<EthAccount>();
if (ethAccount == null) return null;
return ethAccount.EthAddress;
var mStart = access.Container.Recipe.Additionals.Get<MarketplaceStartResults>();
if (mStart == null) return null;
return mStart.EthAddress;
}
public CrashWatcher CreateCrashWatcher(RunningContainer c)
+11 -18
View File
@@ -7,7 +7,7 @@ namespace CodexPlugin
{
public interface ICodexNodeGroup : IEnumerable<ICodexNode>, IHasManyMetricScrapeTargets
{
void BringOffline(bool waitTillStopped);
void BringOffline();
ICodexNode this[int index] { get; }
}
@@ -15,12 +15,12 @@ namespace CodexPlugin
{
private readonly CodexStarter starter;
public CodexNodeGroup(CodexStarter starter, IPluginTools tools, RunningPod[] containers, ICodexNodeFactory codexNodeFactory)
public CodexNodeGroup(CodexStarter starter, IPluginTools tools, RunningContainers[] containers, ICodexNodeFactory codexNodeFactory)
{
this.starter = starter;
Containers = containers;
Nodes = containers.Select(c => CreateOnlineCodexNode(c, tools, codexNodeFactory)).ToArray();
Version = new DebugInfoVersion();
Nodes = containers.Containers().Select(c => CreateOnlineCodexNode(c, tools, codexNodeFactory)).ToArray();
Version = new CodexDebugVersionResponse();
}
public ICodexNode this[int index]
@@ -31,24 +31,17 @@ 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!;
}
public void Stop(CodexNode node, bool waitTillStopped)
{
starter.Stop(node.Pod, waitTillStopped);
Nodes = Nodes.Where(n => n != node).ToArray();
Containers = Containers.Where(c => c != node.Pod).ToArray();
}
public RunningPod[] Containers { get; private set; }
public RunningContainers[] Containers { get; private set; }
public CodexNode[] Nodes { get; private set; }
public DebugInfoVersion Version { get; private set; }
public CodexDebugVersionResponse Version { get; private set; }
public IMetricsScrapeTarget[] ScrapeTargets => Nodes.Select(n => n.MetricsScrapeTarget).ToArray();
public IEnumerator<ICodexNode> GetEnumerator()
@@ -72,7 +65,7 @@ namespace CodexPlugin
var versionResponses = Nodes.Select(n => n.Version);
var first = versionResponses.First();
if (!versionResponses.All(v => v.Version == first.Version && v.Revision == first.Revision))
if (!versionResponses.All(v => v.version == first.version && v.revision == first.revision))
{
throw new Exception("Inconsistent version information received from one or more Codex nodes: " +
string.Join(",", versionResponses.Select(v => v.ToString())));
@@ -81,9 +74,9 @@ namespace CodexPlugin
Version = first;
}
private CodexNode CreateOnlineCodexNode(RunningPod c, IPluginTools tools, ICodexNodeFactory factory)
private CodexNode CreateOnlineCodexNode(RunningContainer c, IPluginTools tools, ICodexNodeFactory factory)
{
var watcher = factory.CreateCrashWatcher(c.Containers.Single());
var watcher = factory.CreateCrashWatcher(c);
var access = new CodexAccess(tools, c, watcher);
return factory.CreateOnlineCodexNode(access, this);
}
+5 -5
View File
@@ -1,4 +1,4 @@
using Core;
using Core;
using KubernetesWorkflow.Types;
namespace CodexPlugin
@@ -32,13 +32,13 @@ namespace CodexPlugin
{
}
public RunningPod[] DeployCodexNodes(int numberOfNodes, Action<ICodexSetup> setup)
public RunningContainers[] DeployCodexNodes(int numberOfNodes, Action<ICodexSetup> setup)
{
var codexSetup = GetSetup(numberOfNodes, setup);
return codexStarter.BringOnline(codexSetup);
}
public ICodexNodeGroup WrapCodexContainers(CoreInterface coreInterface, RunningPod[] containers)
public ICodexNodeGroup WrapCodexContainers(CoreInterface coreInterface, RunningContainers[] containers)
{
containers = containers.Select(c => SerializeGate.Gate(c)).ToArray();
return codexStarter.WrapCodexContainers(coreInterface, containers);
@@ -52,8 +52,8 @@ namespace CodexPlugin
var mconfig = codexSetup.MarketplaceConfig;
foreach (var node in result)
{
mconfig.GethNode.SendEth(node, mconfig.MarketplaceSetup.InitialEth);
mconfig.CodexContracts.MintTestTokens(node, mconfig.MarketplaceSetup.InitialTestTokens);
mconfig.GethNode.SendEth(node, mconfig.InitialEth);
mconfig.CodexContracts.MintTestTokens(node, mconfig.InitialTokens);
}
}
@@ -7,23 +7,7 @@
</PropertyGroup>
<ItemGroup>
<None Remove="openapi.yaml" />
</ItemGroup>
<ItemGroup>
<OpenApiReference Include="openapi.yaml" CodeGenerator="NSwagCSharp" Namespace="CodexOpenApi" ClassName="CodexApi" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.ApiDescription.Client" Version="7.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NSwag.ApiDescription.Client" Version="13.18.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
@@ -34,8 +18,4 @@
<ProjectReference Include="..\MetricsPlugin\MetricsPlugin.csproj" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Command="dotnet run --project $(ProjectDir)\..\CodexPluginPrebuild" />
</Target>
</Project>
+5 -62
View File
@@ -17,7 +17,7 @@ namespace CodexPlugin
ICodexSetup WithBlockMaintenanceInterval(TimeSpan duration);
ICodexSetup WithBlockMaintenanceNumber(int numberOfBlocks);
ICodexSetup EnableMetrics();
ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, 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>
@@ -25,14 +25,6 @@ namespace CodexPlugin
ICodexSetup AsPublicTestNet(CodexTestNetConfig testNetConfig);
}
public interface IMarketplaceSetup
{
IMarketplaceSetup WithInitial(Ether eth, TestToken tokens);
IMarketplaceSetup WithAccount(EthAccount account);
IMarketplaceSetup AsStorageNode();
IMarketplaceSetup AsValidator();
}
public class CodexLogCustomTopics
{
public CodexLogCustomTopics(CodexLogLevel discV5, CodexLogLevel libp2p, CodexLogLevel blockExchange)
@@ -50,7 +42,6 @@ namespace CodexPlugin
public CodexLogLevel DiscV5 { get; set; }
public CodexLogLevel Libp2p { get; set; }
public CodexLogLevel ContractClock { get; set; } = CodexLogLevel.Warn;
public CodexLogLevel? BlockExchange { get; }
}
@@ -77,7 +68,7 @@ namespace CodexPlugin
public ICodexSetup WithBootstrapNode(ICodexNode node)
{
BootstrapSpr = node.GetDebugInfo().Spr;
BootstrapSpr = node.GetDebugInfo().spr;
return this;
}
@@ -124,12 +115,9 @@ namespace CodexPlugin
return this;
}
public ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, Action<IMarketplaceSetup> marketplaceSetup)
public ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens, bool isValidator = false)
{
var ms = new MarketplaceSetup();
marketplaceSetup(ms);
MarketplaceConfig = new MarketplaceInitialConfig(ms, gethNode, codexContracts);
MarketplaceConfig = new MarketplaceInitialConfig(gethNode, codexContracts, initialEth, initialTokens, isValidator);
return this;
}
@@ -158,52 +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}";
}
}
public class MarketplaceSetup : IMarketplaceSetup
{
public bool IsStorageNode { get; private set; }
public bool IsValidator { get; private set; }
public Ether InitialEth { get; private set; } = 0.Eth();
public TestToken InitialTestTokens { get; private set; } = 0.TestTokens();
public EthAccount EthAccount { get; private set; } = EthAccount.GenerateNew();
public IMarketplaceSetup AsStorageNode()
{
IsStorageNode = true;
return this;
}
public IMarketplaceSetup AsValidator()
{
IsValidator = true;
return this;
}
public IMarketplaceSetup WithAccount(EthAccount account)
{
EthAccount = account;
return this;
}
public IMarketplaceSetup WithInitial(Ether eth, TestToken tokens)
{
InitialEth = eth;
InitialTestTokens = tokens;
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 += $"Address: '{EthAccount.EthAddress}' ";
result += $"InitialEth/TT({InitialEth.Eth}/{InitialTestTokens.Amount})";
result += "] ";
return result;
if (MarketplaceConfig != null) yield return $"IsValidator={MarketplaceConfig.IsValidator}";
}
}
}
+16 -32
View File
@@ -9,17 +9,14 @@ namespace CodexPlugin
{
private readonly IPluginTools pluginTools;
private readonly CodexContainerRecipe recipe = new CodexContainerRecipe();
private readonly ApiChecker apiChecker;
private DebugInfoVersion? versionResponse;
private CodexDebugVersionResponse? versionResponse;
public CodexStarter(IPluginTools pluginTools)
{
this.pluginTools = pluginTools;
apiChecker = new ApiChecker(pluginTools);
}
public RunningPod[] BringOnline(CodexSetup codexSetup)
public RunningContainers[] BringOnline(CodexSetup codexSetup)
{
LogSeparator();
Log($"Starting {codexSetup.Describe()}...");
@@ -28,20 +25,18 @@ namespace CodexPlugin
var containers = StartCodexContainers(startupConfig, codexSetup.NumberOfNodes, codexSetup.Location);
apiChecker.CheckCompatibility(containers);
foreach (var rc in containers)
{
var podInfo = GetPodInfo(rc);
var podInfos = string.Join(", ", rc.Containers.Select(c => $"Container: '{c.Name}' runs at '{podInfo.K8SNodeName}'={podInfo.Ip}"));
Log($"Started {codexSetup.NumberOfNodes} nodes of image '{containers.First().Containers.First().Recipe.Image}'. ({podInfos})");
Log($"Started {codexSetup.NumberOfNodes} nodes of image '{containers.Containers().First().Recipe.Image}'. ({podInfos})");
}
LogSeparator();
return containers;
}
public ICodexNodeGroup WrapCodexContainers(CoreInterface coreInterface, RunningPod[] containers)
public ICodexNodeGroup WrapCodexContainers(CoreInterface coreInterface, RunningContainers[] containers)
{
var codexNodeFactory = new CodexNodeFactory(pluginTools);
@@ -53,35 +48,27 @@ 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.");
}
public void Stop(RunningPod pod, bool waitTillStopped)
{
Log($"Stopping node...");
var workflow = pluginTools.CreateWorkflow();
workflow.Stop(pod, waitTillStopped);
Log("Stopped.");
}
public string GetCodexId()
{
if (versionResponse != null) return versionResponse.Version;
if (versionResponse != null) return versionResponse.version;
return recipe.Image;
}
public string GetCodexRevision()
{
if (versionResponse != null) return versionResponse.Revision;
if (versionResponse != null) return versionResponse.revision;
return "unknown";
}
@@ -93,27 +80,24 @@ namespace CodexPlugin
return startupConfig;
}
private RunningPod[] StartCodexContainers(StartupConfig startupConfig, int numberOfNodes, ILocation location)
private RunningContainers[] StartCodexContainers(StartupConfig startupConfig, int numberOfNodes, ILocation location)
{
var futureContainers = new List<FutureContainers>();
var result = new List<RunningContainers>();
for (var i = 0; i < numberOfNodes; i++)
{
var workflow = pluginTools.CreateWorkflow();
futureContainers.Add(workflow.Start(1, location, recipe, startupConfig));
result.Add(workflow.Start(1, location, recipe, startupConfig));
}
return futureContainers
.Select(f => f.WaitForOnline())
.ToArray();
return result.ToArray();
}
private PodInfo GetPodInfo(RunningPod rc)
private PodInfo GetPodInfo(RunningContainers rc)
{
var workflow = pluginTools.CreateWorkflow();
return workflow.GetPodInfo(rc);
}
private CodexNodeGroup CreateCodexGroup(CoreInterface coreInterface, RunningPod[] runningContainers, CodexNodeFactory codexNodeFactory)
private CodexNodeGroup CreateCodexGroup(CoreInterface coreInterface, RunningContainers[] runningContainers, CodexNodeFactory codexNodeFactory)
{
var group = new CodexNodeGroup(this, pluginTools, runningContainers, codexNodeFactory);
@@ -130,10 +114,10 @@ namespace CodexPlugin
return group;
}
private void CodexNodesNotOnline(CoreInterface coreInterface, RunningPod[] runningContainers)
private void CodexNodesNotOnline(CoreInterface coreInterface, RunningContainers[] runningContainers)
{
Log("Codex nodes failed to start");
foreach (var container in runningContainers.First().Containers) coreInterface.DownloadLog(container);
foreach (var container in runningContainers.Containers()) coreInterface.DownloadLog(container);
}
private void LogSeparator()
@@ -68,16 +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)};" +
$"{CustomTopics.ContractClock.ToString().ToLowerInvariant()}:{string.Join(",", contractClockTopics)}";
$"{CustomTopics.Libp2p.ToString()!.ToLowerInvariant()}:{string.Join(",", libp2pTopics)}";
if (CustomTopics.BlockExchange != null)
{
-108
View File
@@ -1,108 +0,0 @@
using Newtonsoft.Json;
using Utils;
namespace CodexPlugin
{
public class DebugInfo
{
public string[] Addrs { get; set; } = Array.Empty<string>();
public string Spr { get; set; } = string.Empty;
public string Id { get; set; } = string.Empty;
public string[] AnnounceAddresses { get; set; } = Array.Empty<string>();
public DebugInfoVersion Version { get; set; } = new();
public DebugInfoTable Table { get; set; } = new();
}
public class DebugInfoVersion
{
public string Version { get; set; } = string.Empty;
public string Revision { get; set; } = string.Empty;
public bool IsValid()
{
return !string.IsNullOrEmpty(Version) && !string.IsNullOrEmpty(Revision);
}
public override string ToString()
{
return JsonConvert.SerializeObject(this);
}
}
public class DebugInfoTable
{
public DebugInfoTableNode LocalNode { get; set; } = new();
public DebugInfoTableNode[] Nodes { get; set; } = Array.Empty<DebugInfoTableNode>();
}
public class DebugInfoTableNode
{
public string NodeId { get; set; } = string.Empty;
public string PeerId { get; set; } = string.Empty;
public string Record { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public bool Seen { get; set; }
}
public class DebugPeer
{
public bool IsPeerFound { get; set; }
public string PeerId { get; set; } = string.Empty;
public string[] Addresses { get; set; } = Array.Empty<string>();
}
public class LocalDatasetList
{
public LocalDataset[] Content { get; set; } = Array.Empty<LocalDataset>();
}
public class LocalDataset
{
public ContentId Cid { get; set; } = new();
public Manifest Manifest { get; set; } = new();
}
public class Manifest
{
public string RootHash { get; set; } = string.Empty;
public ByteSize OriginalBytes { get; set; } = ByteSize.Zero;
public ByteSize BlockSize { get; set; } = ByteSize.Zero;
public bool Protected { get; set; }
}
public class SalesRequestStorageRequest
{
public string Duration { get; set; } = string.Empty;
public string ProofProbability { get; set; } = string.Empty;
public string Reward { get; set; } = string.Empty;
public string Collateral { get; set; } = string.Empty;
public string? Expiry { get; set; }
public uint? Nodes { get; set; }
public uint? Tolerance { get; set; }
}
public class ContentId
{
public ContentId()
{
Id = string.Empty;
}
public ContentId(string id)
{
Id = id;
}
public string Id { get; }
public override bool Equals(object? obj)
{
return obj is ContentId id && Id == id.Id;
}
public override int GetHashCode()
{
return HashCode.Combine(Id);
}
}
}
@@ -5,12 +5,12 @@ namespace CodexPlugin
{
public static class CoreInterfaceExtensions
{
public static RunningPod[] DeployCodexNodes(this CoreInterface ci, int number, Action<ICodexSetup> setup)
public static RunningContainers[] DeployCodexNodes(this CoreInterface ci, int number, Action<ICodexSetup> setup)
{
return Plugin(ci).DeployCodexNodes(number, setup);
}
public static ICodexNodeGroup WrapCodexContainers(this CoreInterface ci, RunningPod[] containers)
public static ICodexNodeGroup WrapCodexContainers(this CoreInterface ci, RunningContainers[] containers)
{
return Plugin(ci).WrapCodexContainers(ci, containers);
}
-172
View File
@@ -1,172 +0,0 @@
using CodexContractsPlugin;
using Newtonsoft.Json.Linq;
using System.Numerics;
using Utils;
namespace CodexPlugin
{
public class Mapper
{
public DebugInfo Map(CodexOpenApi.DebugInfo debugInfo)
{
return new DebugInfo
{
Id = debugInfo.Id,
Spr = debugInfo.Spr,
Addrs = debugInfo.Addrs.ToArray(),
AnnounceAddresses = JArray(debugInfo.AdditionalProperties, "announceAddresses").Select(x => x.ToString()).ToArray(),
Version = MapDebugInfoVersion(JObject(debugInfo.AdditionalProperties, "codex")),
Table = MapDebugInfoTable(JObject(debugInfo.AdditionalProperties, "table"))
};
}
public LocalDatasetList Map(CodexOpenApi.DataList dataList)
{
return new LocalDatasetList
{
Content = dataList.Content.Select(Map).ToArray()
};
}
public LocalDataset Map(CodexOpenApi.DataItem dataItem)
{
return new LocalDataset
{
Cid = new ContentId(dataItem.Cid),
Manifest = MapManifest(dataItem.Manifest)
};
}
public CodexOpenApi.SalesAvailabilityCREATE Map(StorageAvailability availability)
{
return new CodexOpenApi.SalesAvailabilityCREATE
{
Duration = ToDecInt(availability.MaxDuration.TotalSeconds),
MinPrice = ToDecInt(availability.MinPriceForTotalSpace),
MaxCollateral = ToDecInt(availability.MaxCollateral),
TotalSize = ToDecInt(availability.TotalSpace.SizeInBytes)
};
}
public CodexOpenApi.StorageRequestCreation Map(StoragePurchaseRequest purchase)
{
return new CodexOpenApi.StorageRequestCreation
{
Duration = ToDecInt(purchase.Duration.TotalSeconds),
ProofProbability = ToDecInt(purchase.ProofProbability),
Reward = ToDecInt(purchase.PricePerSlotPerSecond),
Collateral = ToDecInt(purchase.RequiredCollateral),
Expiry = ToDecInt(DateTimeOffset.UtcNow.ToUnixTimeSeconds() + purchase.Expiry.TotalSeconds),
Nodes = Convert.ToInt32(purchase.MinRequiredNumberOfNodes),
Tolerance = Convert.ToInt32(purchase.NodeFailureTolerance)
};
}
public StoragePurchase Map(CodexOpenApi.Purchase purchase)
{
return new StoragePurchase
{
State = purchase.State,
Error = purchase.Error
};
}
public StorageAvailability Map(CodexOpenApi.SalesAvailabilityREAD read)
{
return new StorageAvailability(
totalSpace: new Utils.ByteSize(Convert.ToInt64(read.TotalSize)),
maxDuration: TimeSpan.FromSeconds(Convert.ToDouble(read.Duration)),
minPriceForTotalSpace: new TestToken(Convert.ToDecimal(read.MinPrice)),
maxCollateral: new TestToken(Convert.ToDecimal(read.MaxCollateral))
)
{
Id = read.Id
};
}
private DebugInfoVersion MapDebugInfoVersion(JObject obj)
{
return new DebugInfoVersion
{
Version = StringOrEmpty(obj, "version"),
Revision = StringOrEmpty(obj, "revision")
};
}
private DebugInfoTable MapDebugInfoTable(JObject obj)
{
return new DebugInfoTable
{
LocalNode = MapDebugInfoTableNode(obj.GetValue("localNode")),
Nodes = new DebugInfoTableNode[0]
};
}
private DebugInfoTableNode MapDebugInfoTableNode(JToken? token)
{
var obj = token as JObject;
if (obj == null) return new DebugInfoTableNode();
return new DebugInfoTableNode
{
Address = StringOrEmpty(obj, "address"),
NodeId = StringOrEmpty(obj, "nodeId"),
PeerId = StringOrEmpty(obj, "peerId"),
Record = StringOrEmpty(obj, "record"),
Seen = Bool(obj, "seen")
};
}
private Manifest MapManifest(CodexOpenApi.ManifestItem manifest)
{
return new Manifest
{
BlockSize = new ByteSize(Convert.ToInt64(manifest.BlockSize)),
OriginalBytes = new ByteSize(Convert.ToInt64(manifest.OriginalBytes)),
RootHash = manifest.RootHash,
Protected = manifest.Protected
};
}
private JArray JArray(IDictionary<string, object> map, string name)
{
return (JArray)map[name];
}
private JObject JObject(IDictionary<string, object> map, string name)
{
return (JObject)map[name];
}
private string StringOrEmpty(JObject obj, string name)
{
if (obj.TryGetValue(name, out var token))
{
var str = (string?)token;
if (!string.IsNullOrEmpty(str)) return str;
}
return string.Empty;
}
private bool Bool(JObject obj, string name)
{
if (obj.TryGetValue(name, out var token))
{
return (bool)token;
}
return false;
}
private string ToDecInt(double d)
{
var i = new BigInteger(d);
return i.ToString("D");
}
private string ToDecInt(TestToken t)
{
var i = new BigInteger(t.Amount);
return i.ToString("D");
}
}
}
+112 -30
View File
@@ -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(StoragePurchaseRequest 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,14 +24,37 @@ namespace CodexPlugin
this.codexAccess = codexAccess;
}
public StoragePurchaseContract RequestStorage(StoragePurchaseRequest purchase)
public StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration)
{
purchase.Log(log);
return RequestStorage(contentId, pricePerSlotPerSecond, requiredCollateral, minRequiredNumberOfNodes, proofProbability, duration, duration / 2);
}
var response = codexAccess.RequestStorage(purchase);
public StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration, TimeSpan expiry)
{
var expireUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + expiry.TotalSeconds;
if (string.IsNullOrEmpty(response) ||
response == "Purchasing not available" ||
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" ||
response == "Expiry needs to be in future" ||
response == "Expiry has to be before the request's end (now + duration)")
@@ -38,18 +64,42 @@ 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 = new CodexSalesAvailabilityRequest
{
size = ToDecInt(totalSpace.SizeInBytes),
duration = ToDecInt(maxDuration.TotalSeconds),
maxCollateral = ToDecInt(maxCollateral),
minPrice = ToDecInt(minPriceForTotalSpace)
};
var response = codexAccess.SalesAvailability(availability);
Log($"Making storage available... (" +
$"size: {totalSpace}, " +
$"minPriceForTotalSpace: {minPriceForTotalSpace}, " +
$"maxCollateral: {maxCollateral}, " +
$"maxDuration: {Time.FormatDuration(maxDuration)})");
Log($"Storage successfully made available. Id: {response.Id}");
var response = codexAccess.SalesAvailability(request);
return response.Id;
Log($"Storage successfully made available. Id: {response.id}");
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)
@@ -60,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(StoragePurchaseRequest purchase)
public string MakeStorageAvailable(ByteSize size, TestToken minPricePerBytePerSecond, TestToken maxCollateral, TimeSpan duration)
{
Unavailable();
throw new NotImplementedException();
return string.Empty;
}
private void Unavailable()
@@ -83,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, StoragePurchaseRequest 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 StoragePurchaseRequest 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()
@@ -111,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 StoragePurchase 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)
@@ -131,9 +208,9 @@ namespace CodexPlugin
{
var purchaseStatus = codexAccess.GetPurchaseStatus(PurchaseId);
var statusJson = JsonConvert.SerializeObject(purchaseStatus);
if (purchaseStatus != null && purchaseStatus.State != lastState)
if (purchaseStatus != null && purchaseStatus.state != lastState)
{
lastState = purchaseStatus.State;
lastState = purchaseStatus.state;
log.Debug("Purchase status: " + statusJson);
}
@@ -151,5 +228,10 @@ namespace CodexPlugin
}
log.Log($"Contract '{desiredState}'.");
}
public CodexStoragePurchase GetPurchaseStatus(string purchaseId)
{
return codexAccess.GetPurchaseStatus(purchaseId);
}
}
}
@@ -5,15 +5,19 @@ namespace CodexPlugin
{
public class MarketplaceInitialConfig
{
public MarketplaceInitialConfig(MarketplaceSetup marketplaceSetup, IGethNode gethNode, ICodexContracts codexContracts)
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; }
}
}
@@ -0,0 +1,17 @@
using GethPlugin;
namespace CodexPlugin
{
[Serializable]
public class MarketplaceStartResults
{
public MarketplaceStartResults(EthAddress ethAddress, string privateKey)
{
EthAddress = ethAddress;
PrivateKey = privateKey;
}
public EthAddress EthAddress { get; }
public string PrivateKey { get; }
}
}
@@ -0,0 +1,19 @@
using GethPlugin;
using Nethereum.Hex.HexConvertors.Extensions;
using Nethereum.Web3.Accounts;
namespace CodexPlugin
{
public class MarketplaceStarter
{
public MarketplaceStartResults Start()
{
var ecKey = Nethereum.Signer.EthECKey.GenerateKey();
var privateKey = ecKey.GetPrivateKeyAsBytes().ToHex();
var account = new Account(privateKey);
var ethAddress = new EthAddress(account.Address);
return new MarketplaceStartResults(ethAddress, account.PrivateKey);
}
}
}
@@ -1,67 +0,0 @@
using CodexContractsPlugin;
using Logging;
using Utils;
namespace CodexPlugin
{
public class StoragePurchaseRequest
{
public StoragePurchaseRequest(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 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 StoragePurchase
{
public string State { get; set; } = string.Empty;
public string Error { get; set; } = string.Empty;
}
public class StorageAvailability
{
public StorageAvailability(ByteSize totalSpace, TimeSpan maxDuration, TestToken minPriceForTotalSpace, TestToken maxCollateral)
{
TotalSpace = totalSpace;
MaxDuration = maxDuration;
MinPriceForTotalSpace = minPriceForTotalSpace;
MaxCollateral = maxCollateral;
}
public string Id { get; set; } = string.Empty;
public ByteSize TotalSpace { get; }
public TimeSpan MaxDuration { get; }
public TestToken MinPriceForTotalSpace { get; }
public TestToken MaxCollateral { get; }
public void Log(ILog log)
{
log.Log($"Making storage available... (" +
$"totalSize: {TotalSpace}, " +
$"maxDuration: {Time.FormatDuration(MaxDuration)}, " +
$"minPriceForTotalSpace: {MinPriceForTotalSpace}, " +
$"maxCollateral: {MaxCollateral})");
}
}
}
-769
View File
@@ -1,769 +0,0 @@
openapi: 3.0.3
info:
version: 0.0.1
title: Codex API
description: "List of endpoints and interfaces available to Codex API users"
security:
- { }
components:
schemas:
MultiAddress:
type: string
description: Address of node as specified by the multi-address specification https://multiformats.io/multiaddr/
example: /ip4/127.0.0.1/tcp/8080
PeerId:
type: string
description: Peer Identity reference as specified at https://docs.libp2p.io/concepts/fundamentals/peers/
example: QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N
Id:
type: string
description: 32bits identifier encoded in hex-decimal string.
example: 0x...
BigInt:
type: string
description: Integer represented as decimal string
Cid:
type: string
description: Content Identifier as specified at https://github.com/multiformats/cid
example: QmYyQSo1c1Ym7orWxLYvCrM2EmxFTANf8wXmmE7DWjhx5N
SlotId:
type: string
description: Keccak hash of the abi encoded tuple (RequestId, slot index)
example: 268a781e0db3f7cf36b18e5f4fdb7f586ec9edd08e5500b17c0e518a769f114a
LogLevel:
type: string
description: "One of the log levels: TRACE, DEBUG, INFO, NOTICE, WARN, ERROR or FATAL"
example: DEBUG
EthereumAddress:
type: string
description: Address of Ethereum address
Reward:
type: string
description: The maximum amount of tokens paid per second per slot to hosts the client is willing to pay
Duration:
type: string
description: The duration of the request in seconds as decimal string
ProofProbability:
type: string
description: How often storage proofs are required as decimal string
Expiry:
type: string
description: A timestamp as seconds since unix epoch at which this request expires if the Request does not find requested amount of nodes to host the data.
default: 10 minutes
SPR:
type: string
description: Signed Peer Record (libp2p)
SPRRead:
type: object
properties:
spr:
$ref: "#/components/schemas/SPR"
PeerIdRead:
type: object
properties:
id:
$ref: "#/components/schemas/PeerId"
ErasureParameters:
type: object
properties:
totalChunks:
type: integer
PoRParameters:
description: Parameters for Proof of Retrievability
type: object
properties:
u:
type: string
publicKey:
type: string
name:
type: string
Content:
type: object
description: Parameters specifying the content
properties:
cid:
$ref: "#/components/schemas/Cid"
erasure:
$ref: "#/components/schemas/ErasureParameters"
por:
$ref: "#/components/schemas/PoRParameters"
DebugInfo:
type: object
properties:
id:
$ref: "#/components/schemas/PeerId"
addrs:
type: array
items:
$ref: "#/components/schemas/MultiAddress"
repo:
type: string
description: Path of the data repository where all nodes data are stored
spr:
$ref: "#/components/schemas/SPR"
SalesAvailability:
type: object
properties:
id:
$ref: "#/components/schemas/Id"
totalSize:
type: string
description: Total size of availability's storage in bytes as decimal string
duration:
$ref: "#/components/schemas/Duration"
minPrice:
type: string
description: Minimum price to be paid (in amount of tokens) as decimal string
maxCollateral:
type: string
description: Maximum collateral user is willing to pay per filled Slot (in amount of tokens) as decimal string
SalesAvailabilityREAD:
allOf:
- $ref: "#/components/schemas/SalesAvailability"
- type: object
properties:
freeSize:
type: string
description: Unused size of availability's storage in bytes as decimal string
SalesAvailabilityCREATE:
allOf:
- $ref: "#/components/schemas/SalesAvailability"
- required:
- totalSize
- minPrice
- maxCollateral
- duration
Slot:
type: object
properties:
id:
$ref: "#/components/schemas/SlotId"
request:
$ref: "#/components/schemas/StorageRequest"
slotIndex:
type: string
description: Slot Index as hexadecimal string
Reservation:
type: object
properties:
id:
$ref: "#/components/schemas/Id"
availabilityId:
$ref: "#/components/schemas/Id"
size:
$ref: "#/components/schemas/BigInt"
requestId:
$ref: "#/components/schemas/Id"
slotIndex:
type: string
description: Slot Index as hexadecimal string
StorageRequestCreation:
type: object
required:
- reward
- duration
- proofProbability
- collateral
- expiry
properties:
duration:
$ref: "#/components/schemas/Duration"
reward:
$ref: "#/components/schemas/Reward"
proofProbability:
$ref: "#/components/schemas/ProofProbability"
nodes:
description: Minimal number of nodes the content should be stored on
type: integer
default: 1
tolerance:
description: Additional number of nodes on top of the `nodes` property that can be lost before pronouncing the content lost
type: integer
default: 0
collateral:
type: string
description: Number as decimal string that represents how much collateral is asked from hosts that wants to fill a slots
expiry:
type: string
description: Number as decimal string that represents expiry time of the request (in unix timestamp)
StorageAsk:
type: object
required:
- reward
properties:
slots:
description: Number of slots (eq. hosts) that the Request want to have the content spread over
type: integer
slotSize:
type: string
description: Amount of storage per slot (in bytes) as decimal string
duration:
$ref: "#/components/schemas/Duration"
proofProbability:
$ref: "#/components/schemas/ProofProbability"
reward:
$ref: "#/components/schemas/Reward"
maxSlotLoss:
type: integer
description: Max slots that can be lost without data considered to be lost
StorageRequest:
type: object
properties:
id:
type: string
description: Request ID
client:
$ref: "#/components/schemas/EthereumAddress"
ask:
$ref: "#/components/schemas/StorageAsk"
content:
$ref: "#/components/schemas/Content"
expiry:
$ref: "#/components/schemas/Expiry"
nonce:
type: string
description: Random data
Purchase:
type: object
properties:
state:
type: string
description: Description of the Request's state
error:
type: string
description: If Request failed, then here is presented the error message
request:
$ref: "#/components/schemas/StorageRequest"
DataList:
type: object
properties:
content:
type: array
items:
$ref: "#/components/schemas/DataItem"
DataItem:
type: object
properties:
cid:
$ref: "#/components/schemas/Cid"
manifest:
$ref: "#/components/schemas/ManifestItem"
ManifestItem:
type: object
properties:
rootHash:
$ref: "#/components/schemas/Cid"
description: "Root hash of the content"
originalBytes:
type: integer
description: "Length of original content in bytes"
blockSize:
type: integer
description: "Size of blocks"
protected:
type: boolean
description: "Indicates if content is protected by erasure-coding"
Space:
type: object
properties:
totalBlocks:
description: "Number of blocks stored by the node"
type: integer
quotaMaxBytes:
type: integer
description: "Maximum storage space used by the node"
quotaUsedBytes:
type: integer
description: "Amount of storage space currently in use"
quotaReservedBytes:
type: integer
description: "Amount of storage space reserved"
servers:
- url: "http://localhost:8080/api/codex/v1"
tags:
- name: Marketplace
description: Marketplace information and operations
- name: Data
description: Data operations
- name: Node
description: Node management
- name: Debug
description: Debugging configuration
paths:
"/connect/{peerId}":
get:
summary: "Connect to a peer"
description: |
If `addrs` param is supplied, it will be used to dial the peer, otherwise the `peerId` is used
to invoke peer discovery, if it succeeds the returned addresses will be used to dial.
tags: [ Node ]
operationId: connectPeer
parameters:
- in: path
name: peerId
required: true
schema:
$ref: "#/components/schemas/PeerId"
description: Peer that should be dialed.
- in: query
name: addrs
schema:
type: array
nullable: true
items:
$ref: "#/components/schemas/MultiAddress"
description: |
If supplied, it will be used to dial the peer.
The address has to target the listening address of the peer,
which is specified with the `--listen-addrs` CLI flag.
responses:
"200":
description: Successfully connected to peer
"400":
description: Peer either not found or was not possible to dial
"/data":
get:
summary: "Lists manifest CIDs stored locally in node."
tags: [ Data ]
operationId: listData
responses:
"200":
description: Retrieved list of content CIDs
content:
application/json:
schema:
$ref: "#/components/schemas/DataList"
"400":
description: Invalid CID is specified
"404":
description: Content specified by the CID is not found
"500":
description: Well it was bad-bad
post:
summary: "Upload a file in a streaming manner. Once finished, the file is stored in the node and can be retrieved by any node in the network using the returned CID."
tags: [ Data ]
operationId: upload
requestBody:
content:
application/octet-stream:
schema:
type: string
format: binary
responses:
"200":
description: CID of uploaded file
content:
text/plain:
schema:
type: string
"500":
description: Well it was bad-bad and the upload did not work out
"/data/{cid}":
get:
summary: "Download a file from the local node in a streaming manner. If the file is not available locally, a 404 is returned."
tags: [ Data ]
operationId: downloadLocal
parameters:
- in: path
name: cid
required: true
schema:
$ref: "#/components/schemas/Cid"
description: File to be downloaded.
responses:
"200":
description: Retrieved content specified by CID
content:
application/octet-stream:
schema:
type: string
format: binary
"400":
description: Invalid CID is specified
"404":
description: Content specified by the CID is unavailable locally
"500":
description: Well it was bad-bad
"/data/{cid}/network":
get:
summary: "Download a file from the network in a streaming manner. If the file is not available locally, it will be retrieved from other nodes in the network if able."
tags: [ Data ]
operationId: downloadNetwork
parameters:
- in: path
name: cid
required: true
schema:
$ref: "#/components/schemas/Cid"
description: "File to be downloaded."
responses:
"200":
description: Retrieved content specified by CID
content:
application/octet-stream:
schema:
type: string
format: binary
"400":
description: Invalid CID is specified
"404":
description: Content specified by the CID is not found
"500":
description: Well it was bad-bad
"/space":
get:
summary: "Gets a summary of the storage space allocation of the node."
tags: [ Data ]
operationId: space
responses:
"200":
description: "Summary of storage allocation"
content:
application/json:
schema:
$ref: "#/components/schemas/Space"
"500":
description: "It's not working as planned"
"/sales/slots":
get:
summary: "Returns active slots"
tags: [ Marketplace ]
operationId: getActiveSlots
responses:
"200":
description: Retrieved active slots
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Slot"
"503":
description: Sales are unavailable
"/sales/slots/{slotId}":
get:
summary: "Returns active slot with id {slotId} for the host"
tags: [ Marketplace ]
operationId: getActiveSlotById
parameters:
- in: path
name: slotId
required: true
schema:
$ref: "#/components/schemas/Cid"
description: File to be downloaded.
responses:
"200":
description: Retrieved active slot
content:
application/json:
schema:
$ref: "#/components/schemas/Slot"
"400":
description: Invalid or missing SlotId
"404":
description: Host is not in an active sale for the slot
"503":
description: Sales are unavailable
"/sales/availability":
get:
summary: "Returns storage that is for sale"
tags: [ Marketplace ]
operationId: getOfferedStorage
responses:
"200":
description: Retrieved storage availabilities of the node
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/SalesAvailability"
"500":
description: Error getting unused availabilities
"503":
description: Sales are unavailable
post:
summary: "Offers storage for sale"
operationId: offerStorage
tags: [ Marketplace ]
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/SalesAvailabilityCREATE"
responses:
"201":
description: Created storage availability
content:
application/json:
schema:
$ref: "#/components/schemas/SalesAvailabilityREAD"
"400":
description: Invalid data input
"422":
description: Not enough node's storage quota available
"500":
description: Error reserving availability
"503":
description: Sales are unavailable
"/sales/availability/{id}":
patch:
summary: "Updates availability"
description: |
The new parameters will be only considered for new requests.
Existing Requests linked to this Availability will continue as is.
operationId: updateOfferedStorage
tags: [ Marketplace ]
parameters:
- in: path
name: id
required: true
schema:
type: string
description: ID of Availability
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/SalesAvailability"
responses:
"204":
description: Availability successfully updated
"400":
description: Invalid data input
"404":
description: Availability not found
"422":
description: Not enough node's storage quota available
"500":
description: Error reserving availability
"503":
description: Sales are unavailable
"/sales/availability/{id}/reservations":
patch:
summary: "Get availability's reservations"
description: Return's list of Reservations for ongoing Storage Requests that the node hosts.
operationId: getReservations
tags: [ Marketplace ]
parameters:
- in: path
name: id
required: true
schema:
type: string
description: ID of Availability
responses:
"200":
description: Retrieved storage availabilities of the node
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Reservation"
"400":
description: Invalid Availability ID
"404":
description: Availability not found
"500":
description: Error getting reservations
"503":
description: Sales are unavailable
"/storage/request/{cid}":
post:
summary: "Creates a new Request for storage"
tags: [ Marketplace ]
operationId: createStorageRequest
parameters:
- in: path
name: cid
required: true
schema:
$ref: "#/components/schemas/Cid"
description: CID of the uploaded data that should be stored
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/StorageRequestCreation"
responses:
"200":
description: Returns the Request ID as decimal string
content:
text/plain:
schema:
type: string
"400":
description: Invalid or missing Request ID
"404":
description: Request ID not found
"503":
description: Purchasing is unavailable
"/storage/purchases":
get:
summary: "Returns list of purchase IDs"
tags: [ Marketplace ]
operationId: getPurchases
responses:
"200":
description: Gets all purchase IDs stored in node
content:
application/json:
schema:
type: array
items:
type: string
"503":
description: Purchasing is unavailable
"/storage/purchases/{id}":
get:
summary: "Returns purchase details"
tags: [ Marketplace ]
operationId: getPurchase
parameters:
- in: path
name: id
required: true
schema:
type: string
description: Hexadecimal ID of a Purchase
responses:
"200":
description: Purchase details
content:
application/json:
schema:
$ref: "#/components/schemas/Purchase"
"400":
description: Invalid or missing Purchase ID
"404":
description: Purchase not found
"503":
description: Purchasing is unavailable
"/node/spr":
get:
summary: "Get Node's SPR"
operationId: getSPR
tags: [ Node ]
responses:
"200":
description: Node's SPR
content:
plain/text:
schema:
$ref: "#/components/schemas/SPR"
application/json:
schema:
$ref: "#/components/schemas/SPRRead"
"503":
description: Node SPR not ready, try again later
"/node/peerid":
get:
summary: "Get Node's PeerID"
operationId: getPeerId
tags: [ Node ]
responses:
"200":
description: Node's Peer ID
content:
plain/text:
schema:
$ref: "#/components/schemas/PeerId"
application/json:
schema:
$ref: "#/components/schemas/PeerIdRead"
"/debug/chronicles/loglevel":
post:
summary: "Set log level at run time"
tags: [ Debug ]
operationId: setDebugLogLevel
parameters:
- in: query
name: level
required: true
schema:
$ref: "#/components/schemas/LogLevel"
responses:
"200":
description: Successfully log level set
"400":
description: Invalid or missing log level
"500":
description: Well it was bad-bad
"/debug/info":
get:
summary: "Gets node information"
operationId: getDebugInfo
tags: [ Debug ]
responses:
"200":
description: Node's information
content:
application/json:
schema:
$ref: "#/components/schemas/DebugInfo"
@@ -1,10 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
@@ -1,65 +0,0 @@
using System.Security.Cryptography;
using System.Text;
public static class Program
{
private const string OpenApiFile = "../CodexPlugin/openapi.yaml";
private const string ClientFile = "../CodexPlugin/obj/openapiClient.cs";
private const string Search = "<INSERT-OPENAPI-YAML-HASH>";
private const string TargetFile = "ApiChecker.cs";
public static void Main(string[] args)
{
Console.WriteLine("Injecting hash of 'openapi.yaml'...");
// Force client rebuild by deleting previous artifact.
File.Delete(ClientFile);
var hash = CreateHash();
// This hash is used to verify that the Codex docker image being used is compatible
// with the openapi.yaml being used by the Codex plugin.
// If the openapi.yaml files don't match, an exception is thrown.
SearchAndInject(hash);
// This program runs as the pre-build trigger for "CodexPlugin".
// You might be wondering why this work isn't done by a shell script.
// This is because this project is being run on many different platforms.
// (Mac, Unix, Win, but also desktop/cloud containers.)
// In order to not go insane trying to make a shell script that works in all possible cases,
// instead we use the one tool that's definitely installed in all platforms and locations
// when you're trying to run this plugin.
Console.WriteLine("Done!");
}
private static string CreateHash()
{
var file = File.ReadAllText(OpenApiFile);
var fileBytes = Encoding.ASCII.GetBytes(file
.Replace(Environment.NewLine, ""));
var sha = SHA256.Create();
var hash = sha.ComputeHash(fileBytes);
return BitConverter.ToString(hash);
}
private static void SearchAndInject(string hash)
{
var lines = File.ReadAllLines(TargetFile);
Inject(lines, hash);
File.WriteAllLines(TargetFile, lines);
}
private static void Inject(string[] lines, string hash)
{
for (var i = 0; i < lines.Length; i++)
{
if (lines[i].Contains(Search))
{
lines[i + 1] = $" private const string OpenApiYamlHash = \"{hash}\";";
return;
}
}
}
}
@@ -30,7 +30,7 @@ namespace DeployAndRunPlugin
startupConfig.Add(config);
var location = workflow.GetAvailableLocations().Get("fixed-s-4vcpu-16gb-amd-yz8rd");
var containers = workflow.Start(1, location, new DeployAndRunContainerRecipe(), startupConfig).WaitForOnline();
var containers = workflow.Start(1, location, new DeployAndRunContainerRecipe(), startupConfig);
return containers.Containers.Single();
}
}
-28
View File
@@ -1,28 +0,0 @@
using Nethereum.Hex.HexConvertors.Extensions;
using Nethereum.Web3.Accounts;
namespace GethPlugin
{
[Serializable]
public class EthAccount
{
public EthAccount(EthAddress ethAddress, string privateKey)
{
EthAddress = ethAddress;
PrivateKey = privateKey;
}
public EthAddress EthAddress { get; }
public string PrivateKey { get; }
public static EthAccount GenerateNew()
{
var ecKey = Nethereum.Signer.EthECKey.GenerateKey();
var privateKey = ecKey.GetPrivateKeyAsBytes().ToHex();
var account = new Account(privateKey);
var ethAddress = new EthAddress(account.Address);
return new EthAccount(ethAddress, account.PrivateKey);
}
}
}
-1
View File
@@ -5,7 +5,6 @@
EthAddress EthAddress { get; }
}
[Serializable]
public class EthAddress
{
public EthAddress(string address)
+4 -4
View File
@@ -7,9 +7,9 @@ namespace GethPlugin
{
public class GethDeployment : IHasContainer
{
public GethDeployment(RunningPod pod, Port discoveryPort, Port httpPort, Port wsPort, GethAccount account, string pubKey)
public GethDeployment(RunningContainers containers, Port discoveryPort, Port httpPort, Port wsPort, GethAccount account, string pubKey)
{
Pod = pod;
Containers = containers;
DiscoveryPort = discoveryPort;
HttpPort = httpPort;
WsPort = wsPort;
@@ -17,9 +17,9 @@ namespace GethPlugin
PubKey = pubKey;
}
public RunningPod Pod { get; }
public RunningContainers Containers { get; }
[JsonIgnore]
public RunningContainer Container { get { return Pod.Containers.Single(); } }
public RunningContainer Container { get { return Containers.Containers.Single(); } }
public Port DiscoveryPort { get; }
public Port HttpPort { get; }
public Port WsPort { get; }
+4 -17
View File
@@ -5,7 +5,6 @@ using Nethereum.ABI.FunctionEncoding.Attributes;
using Nethereum.Contracts;
using Nethereum.RPC.Eth.DTOs;
using NethereumWorkflow;
using NethereumWorkflow.BlockUtils;
using Utils;
namespace GethPlugin
@@ -25,10 +24,8 @@ namespace GethPlugin
decimal? GetSyncedBlockNumber();
bool IsContractAvailable(string abi, string contractAddress);
GethBootstrapNode GetBootstrapRecord();
List<EventLog<TEvent>> GetEvents<TEvent>(string address, BlockInterval blockRange) where TEvent : IEventDTO, new();
List<EventLog<TEvent>> GetEvents<TEvent>(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new();
List<EventLog<TEvent>> GetEvents<TEvent>(string address, TimeRange timeRange) where TEvent : IEventDTO, new();
BlockInterval ConvertTimeRangeToBlockRange(TimeRange timeRange);
BlockTimeEntry GetBlockForNumber(ulong number);
}
public class DeploymentGethNode : BaseGethNode, IGethNode
@@ -147,24 +144,14 @@ namespace GethPlugin
return StartInteraction().IsContractAvailable(abi, contractAddress);
}
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, BlockInterval blockRange) where TEvent : IEventDTO, new()
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new()
{
return StartInteraction().GetEvents<TEvent>(address, blockRange);
return StartInteraction().GetEvents<TEvent>(address, fromBlockNumber, toBlockNumber);
}
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, TimeRange timeRange) where TEvent : IEventDTO, new()
{
return StartInteraction().GetEvents<TEvent>(address, ConvertTimeRangeToBlockRange(timeRange));
}
public BlockInterval ConvertTimeRangeToBlockRange(TimeRange timeRange)
{
return StartInteraction().ConvertTimeRangeToBlockRange(timeRange);
}
public BlockTimeEntry GetBlockForNumber(ulong number)
{
return StartInteraction().GetBlockForNumber(number);
return StartInteraction().GetEvents<TEvent>(address, timeRange);
}
protected abstract NethereumInteraction StartInteraction();
+1 -1
View File
@@ -21,7 +21,7 @@ namespace GethPlugin
startupConfig.NameOverride = gethStartupConfig.NameOverride;
var workflow = tools.CreateWorkflow();
var containers = workflow.Start(1, new GethContainerRecipe(), startupConfig).WaitForOnline();
var containers = workflow.Start(1, new GethContainerRecipe(), startupConfig);
if (containers.Containers.Length != 1) throw new InvalidOperationException("Expected 1 Geth bootstrap node to be created. Test infra failure.");
var container = containers.Containers[0];
@@ -6,24 +6,24 @@ namespace MetricsPlugin
{
public static class CoreInterfaceExtensions
{
public static RunningPod DeployMetricsCollector(this CoreInterface ci, params IHasMetricsScrapeTarget[] scrapeTargets)
public static RunningContainers DeployMetricsCollector(this CoreInterface ci, params IHasMetricsScrapeTarget[] scrapeTargets)
{
return Plugin(ci).DeployMetricsCollector(scrapeTargets.Select(t => t.MetricsScrapeTarget).ToArray());
}
public static RunningPod DeployMetricsCollector(this CoreInterface ci, params IMetricsScrapeTarget[] scrapeTargets)
public static RunningContainers DeployMetricsCollector(this CoreInterface ci, params IMetricsScrapeTarget[] scrapeTargets)
{
return Plugin(ci).DeployMetricsCollector(scrapeTargets);
}
public static IMetricsAccess WrapMetricsCollector(this CoreInterface ci, RunningPod metricsPod, IHasMetricsScrapeTarget scrapeTarget)
public static IMetricsAccess WrapMetricsCollector(this CoreInterface ci, RunningContainers metricsContainer, IHasMetricsScrapeTarget scrapeTarget)
{
return ci.WrapMetricsCollector(metricsPod, scrapeTarget.MetricsScrapeTarget);
return ci.WrapMetricsCollector(metricsContainer, scrapeTarget.MetricsScrapeTarget);
}
public static IMetricsAccess WrapMetricsCollector(this CoreInterface ci, RunningPod metricsPod, IMetricsScrapeTarget scrapeTarget)
public static IMetricsAccess WrapMetricsCollector(this CoreInterface ci, RunningContainers metricsContainer, IMetricsScrapeTarget scrapeTarget)
{
return Plugin(ci).WrapMetricsCollectorDeployment(metricsPod, scrapeTarget);
return Plugin(ci).WrapMetricsCollectorDeployment(metricsContainer, scrapeTarget);
}
public static IMetricsAccess[] GetMetricsFor(this CoreInterface ci, params IHasManyMetricScrapeTargets[] manyScrapeTargets)
@@ -31,15 +31,15 @@ namespace MetricsPlugin
{
}
public RunningPod DeployMetricsCollector(IMetricsScrapeTarget[] scrapeTargets)
public RunningContainers DeployMetricsCollector(IMetricsScrapeTarget[] scrapeTargets)
{
return starter.CollectMetricsFor(scrapeTargets);
}
public IMetricsAccess WrapMetricsCollectorDeployment(RunningPod runningPod, IMetricsScrapeTarget target)
public IMetricsAccess WrapMetricsCollectorDeployment(RunningContainers runningContainer, IMetricsScrapeTarget target)
{
runningPod = SerializeGate.Gate(runningPod);
return starter.CreateAccessForTarget(runningPod, target);
runningContainer = SerializeGate.Gate(runningContainer);
return starter.CreateAccessForTarget(runningContainer, target);
}
public LogFile? DownloadAllMetrics(IMetricsAccess metricsAccess, string targetName)
+5 -7
View File
@@ -7,16 +7,14 @@ namespace MetricsPlugin
{
public class MetricsQuery
{
private readonly IEndpoint endpoint;
private readonly IHttp http;
private readonly ILog log;
public MetricsQuery(IPluginTools tools, RunningContainer runningContainer)
{
RunningContainer = runningContainer;
log = tools.GetLog();
endpoint = tools
.CreateHttp()
.CreateEndpoint(RunningContainer.GetAddress(log, PrometheusContainerRecipe.PortTag), "/api/v1/");
http = tools.CreateHttp(RunningContainer.GetAddress(log, PrometheusContainerRecipe.PortTag), "api/v1");
}
public RunningContainer RunningContainer { get; }
@@ -53,7 +51,7 @@ namespace MetricsPlugin
public Metrics? GetAllMetricsForNode(IMetricsScrapeTarget target)
{
var response = endpoint.HttpGetJson<PrometheusQueryResponse>($"query?query={GetInstanceStringForNode(target)}{GetQueryTimeRange()}");
var response = http.HttpGetJson<PrometheusQueryResponse>($"query?query={GetInstanceStringForNode(target)}{GetQueryTimeRange()}");
if (response.status != "success") return null;
var result = MapResponseToMetrics(response);
Log(target, result);
@@ -62,14 +60,14 @@ namespace MetricsPlugin
private PrometheusQueryResponse? GetLastOverTime(string metricName, string instanceString)
{
var response = endpoint.HttpGetJson<PrometheusQueryResponse>($"query?query=last_over_time({metricName}{instanceString}{GetQueryTimeRange()})");
var response = http.HttpGetJson<PrometheusQueryResponse>($"query?query=last_over_time({metricName}{instanceString}{GetQueryTimeRange()})");
if (response.status != "success") return null;
return response;
}
private PrometheusQueryResponse? GetAll(string metricName)
{
var response = endpoint.HttpGetJson<PrometheusQueryResponse>($"query?query={metricName}{GetQueryTimeRange()}");
var response = http.HttpGetJson<PrometheusQueryResponse>($"query?query={metricName}{GetQueryTimeRange()}");
if (response.status != "success") return null;
return response;
}
@@ -16,7 +16,7 @@ namespace MetricsPlugin
this.tools = tools;
}
public RunningPod CollectMetricsFor(IMetricsScrapeTarget[] targets)
public RunningContainers CollectMetricsFor(IMetricsScrapeTarget[] targets)
{
if (!targets.Any()) throw new ArgumentException(nameof(targets) + " must not be empty.");
@@ -25,16 +25,16 @@ namespace MetricsPlugin
startupConfig.Add(new PrometheusStartupConfig(GeneratePrometheusConfig(targets)));
var workflow = tools.CreateWorkflow();
var runningContainers = workflow.Start(1, recipe, startupConfig).WaitForOnline();
var runningContainers = workflow.Start(1, recipe, startupConfig);
if (runningContainers.Containers.Length != 1) throw new InvalidOperationException("Expected only 1 Prometheus container to be created.");
Log("Metrics server started.");
return runningContainers;
}
public MetricsAccess CreateAccessForTarget(RunningPod metricsPod, IMetricsScrapeTarget target)
public MetricsAccess CreateAccessForTarget(RunningContainers metricsContainer, IMetricsScrapeTarget target)
{
var metricsQuery = new MetricsQuery(tools, metricsPod.Containers.Single());
var metricsQuery = new MetricsQuery(tools, metricsContainer.Containers.Single());
return new MetricsAccess(metricsQuery, target);
}
+4 -11
View File
@@ -1,22 +1,15 @@
# Distributed System Tests
# Distributed System Tests for Nim-Codex
This project allows you to write tools and tests that control and interact with container-based applications to form a distributed system in a controlled, reproducible environment.
Using a common dotnet unit-test framework and a few other libraries, this project allows you to write tests that use multiple Codex node instances in various configurations to test the distributed system in a controlled, reproducible environment.
Nim-Codex: https://github.com/codex-storage/nim-codex
Dotnet: v7.0
Kubernetes: v1.25.4
Dotnet-kubernetes SDK: v10.1.4 https://github.com/kubernetes-client/csharp
Nethereum: v4.14.0
Currently, this project is mainly used for distributed testing of [Nim-Codex](https://github.com/codex-storage/nim-codex). However, its plugin-structure allows for other projects to be on-boarded (relatively) easily. (See 'contribute a plugin`.)
## Tests/DistTestCore
Library with generic distributed-testing functionality. Uses NUnit3. Reference this project to build unit-test style scenarios: setup, run test, teardown. The DistTestCore responds to the following env-vars:
- `LOGPATH` = Path where log files will be written.
- `DATAFILEPATH` = Path where (temporary) data files will be stored.
- `ALWAYS_LOGS` = When set, DistTestCore will always download all container logs at the end of a test run. By default, logs are only downloaded on test failure.
## Tests/CodexTests and Tests/CodexLongTests
These are test assemblies that use DistTestCore to perform tests against transient Codex nodes.
These are test assemblies that use NUnit3 to perform tests against transient Codex nodes.
Read more [HERE](/Tests/CodexTests/README.md)
## Tests/ContinuousTests
+23 -7
View File
@@ -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;
@@ -34,11 +34,11 @@ namespace ContinuousTests
$"{startUtc.ToString("o")} - {endUtc.ToString("o")}");
log.Log(openingLine);
var endpoint = CreateElasticSearchEndpoint();
var http = CreateElasticSearchHttp();
var queryTemplate = CreateQueryTemplate(container, startUtc, endUtc);
targetFile.Write($"Downloading '{container.Name}' to '{targetFile.FullFilename}'.");
var reconstructor = new LogReconstructor(targetFile, endpoint, queryTemplate);
var reconstructor = new LogReconstructor(targetFile, http, queryTemplate);
reconstructor.DownloadFullLog();
log.Log("Log download finished.");
@@ -49,8 +49,8 @@ namespace ContinuousTests
var start = startUtc.ToString("o");
var end = endUtc.ToString("o");
var containerName = container.RunningPod.StartResult.Deployment.Name;
var namespaceName = container.RunningPod.StartResult.Cluster.Configuration.KubernetesNamespace;
var containerName = container.RunningContainers.StartResult.Deployment.Name;
var namespaceName = container.RunningContainers.StartResult.Cluster.Configuration.KubernetesNamespace;
//container_name : codex3-5 - deploymentName as stored in pod
// pod_namespace : codex - continuous - nolimits - tests - 1
@@ -64,36 +64,34 @@ namespace ContinuousTests
.Replace("<NAMESPACENAME>", namespaceName);
}
private IEndpoint CreateElasticSearchEndpoint()
private IHttp CreateElasticSearchHttp()
{
var serviceName = "elasticsearch";
var k8sNamespace = "monitoring";
var address = new Address($"http://{serviceName}.{k8sNamespace}.svc.cluster.local", 9200);
var baseUrl = "";
var http = tools.CreateHttp(client =>
return tools.CreateHttp(address, baseUrl, client =>
{
client.DefaultRequestHeaders.Add("kbn-xsrf", "reporting");
});
return http.CreateEndpoint(address, baseUrl);
}
public class LogReconstructor
{
private readonly List<LogQueueEntry> queue = new List<LogQueueEntry>();
private readonly LogFile targetFile;
private readonly IEndpoint endpoint;
private readonly IHttp http;
private readonly string queryTemplate;
private const int sizeOfPage = 2000;
private string searchAfter = "";
private int lastHits = 1;
private ulong? lastLogLine;
public LogReconstructor(LogFile targetFile, IEndpoint endpoint, string queryTemplate)
public LogReconstructor(LogFile targetFile, IHttp http, string queryTemplate)
{
this.targetFile = targetFile;
this.endpoint = endpoint;
this.http = http;
this.queryTemplate = queryTemplate;
}
@@ -112,7 +110,7 @@ namespace ContinuousTests
.Replace("<SIZE>", sizeOfPage.ToString())
.Replace("<SEARCHAFTER>", searchAfter);
var response = endpoint.HttpPostString<SearchResponse>("_search", query);
var response = http.HttpPostString<SearchResponse>("_search", query);
lastHits = response.hits.hits.Length;
if (lastHits > 0)
@@ -25,7 +25,6 @@ namespace ContinuousTests
);
return new EntryPoint(log, lifecycleConfig, dataFilePath);
//DefaultContainerRecipe.TestsType = "continuous-tests";
}
private static string? GetKubeConfig(string kubeConfigFile)
+19 -23
View File
@@ -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);
}
}
}
+6 -6
View File
@@ -125,10 +125,10 @@ namespace ContinuousTests
foreach (var node in nodes)
{
var container = node.Container;
var deploymentName = container.RunningPod.StartResult.Deployment.Name;
var namespaceName = container.RunningPod.StartResult.Cluster.Configuration.KubernetesNamespace;
var deploymentName = container.RunningContainers.StartResult.Deployment.Name;
var namespaceName = container.RunningContainers.StartResult.Cluster.Configuration.KubernetesNamespace;
var openingLine =
$"{namespaceName} - {deploymentName} = {node.Container.Name} = {node.GetDebugInfo().Id}";
$"{namespaceName} - {deploymentName} = {node.Container.Name} = {node.GetDebugInfo().id}";
elasticSearchLogDownloader.Download(fixtureLog.CreateSubfile(), node.Container, effectiveStart,
effectiveEnd, openingLine);
}
@@ -295,13 +295,13 @@ namespace ContinuousTests
return entryPoint.CreateInterface().WrapCodexContainers(containers).ToArray();
}
private RunningPod[] SelectRandomContainers()
private RunningContainers[] SelectRandomContainers()
{
var number = handle.Test.RequiredNumberOfNodes;
var containers = config.CodexDeployment.CodexInstances.Select(i => i.Pod).ToList();
var containers = config.CodexDeployment.CodexInstances.Select(i => i.Containers).ToList();
if (number == -1) return containers.ToArray();
var result = new RunningPod[number];
var result = new RunningContainers[number];
for (var i = 0; i < number; i++)
{
result[i] = containers.PickOneRandom();
+7 -7
View File
@@ -43,13 +43,13 @@ namespace ContinuousTests
var workflow = entryPoint.Tools.CreateWorkflow();
foreach (var instance in deployment.CodexInstances)
{
foreach (var container in instance.Pod.Containers)
foreach (var container in instance.Containers.Containers)
{
var podInfo = workflow.GetPodInfo(container);
log.Log($"Codex environment variables for '{container.Name}':");
log.Log(
$"Namespace: {container.RunningPod.StartResult.Cluster.Configuration.KubernetesNamespace} - " +
$"Pod name: {podInfo.Name} - Deployment name: {instance.Pod.StartResult.Deployment.Name}");
$"Namespace: {container.RunningContainers.StartResult.Cluster.Configuration.KubernetesNamespace} - " +
$"Pod name: {podInfo.Name} - Deployment name: {instance.Containers.StartResult.Deployment.Name}");
var codexVars = container.Recipe.EnvVars;
foreach (var vars in codexVars) log.Log(vars.ToString());
log.Log("");
@@ -92,7 +92,7 @@ namespace ContinuousTests
private void CheckCodexNodes(BaseLog log, Configuration config)
{
var nodes = entryPoint.CreateInterface()
.WrapCodexContainers(config.CodexDeployment.CodexInstances.Select(i => i.Pod).ToArray());
.WrapCodexContainers(config.CodexDeployment.CodexInstances.Select(i => i.Containers).ToArray());
var pass = true;
foreach (var n in nodes)
{
@@ -123,10 +123,10 @@ namespace ContinuousTests
try
{
var info = n.GetDebugInfo();
if (info == null || string.IsNullOrEmpty(info.Id)) return false;
if (info == null || string.IsNullOrEmpty(info.id)) return false;
log.Log($"Codex version: '{info.Version.Version}' revision: '{info.Version.Revision}'");
LogReplacements.Add(new BaseLogStringReplacement(info.Id, n.GetName()));
log.Log($"Codex version: '{info.codex.version}' revision: '{info.codex.revision}'");
LogReplacements.Add(new BaseLogStringReplacement(info.id, n.GetName()));
}
catch
{
@@ -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);
});
}
}
}
@@ -24,12 +24,12 @@ namespace CodexContinuousTests.Tests
var allInfos = Nodes.Select(n =>
{
var info = n.GetDebugInfo();
Log.Log($"{n.GetName()} = {info.Table.LocalNode.NodeId}");
Log.AddStringReplace(info.Table.LocalNode.NodeId, n.GetName());
Log.Log($"{n.GetName()} = {info.table.localNode.nodeId}");
Log.AddStringReplace(info.table.localNode.nodeId, n.GetName());
return info;
}).ToArray();
var allIds = allInfos.Select(i => i.Table.LocalNode.NodeId).ToArray();
var allIds = allInfos.Select(i => i.table.localNode.nodeId).ToArray();
var errors = Nodes.Select(n => AreAllPresent(n, allIds)).Where(s => !string.IsNullOrEmpty(s)).ToArray();
if (errors.Any())
@@ -41,13 +41,13 @@ namespace CodexContinuousTests.Tests
private string AreAllPresent(ICodexNode n, string[] allIds)
{
var info = n.GetDebugInfo();
var known = info.Table.Nodes.Select(n => n.NodeId).ToArray();
var expected = allIds.Where(i => i != info.Table.LocalNode.NodeId).ToArray();
var known = info.table.nodes.Select(n => n.nodeId).ToArray();
var expected = allIds.Where(i => i != info.table.localNode.nodeId).ToArray();
if (!expected.All(ex => known.Contains(ex)))
{
var nl = Environment.NewLine;
return $"{nl}At node '{info.Table.LocalNode.NodeId}'{nl}" +
return $"{nl}At node '{info.table.localNode.nodeId}'{nl}" +
$"Not all of{nl}'{string.Join(",", expected)}'{nl}" +
$"were present in routing table:{nl}'{string.Join(",", known)}'";
}
@@ -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)}");
}
}
}
@@ -11,7 +11,7 @@ namespace CodexLongTests.BasicTests
{
var group = AddCodex(1000, s => s.EnableMetrics());
var nodeIds = group.Select(n => n.GetDebugInfo().Id).ToArray();
var nodeIds = group.Select(n => n.GetDebugInfo().id).ToArray();
Assert.That(nodeIds.Length, Is.EqualTo(nodeIds.Distinct().Count()),
"Not all created nodes provided a unique id.");
@@ -24,7 +24,7 @@ namespace CodexLongTests.BasicTests
{
var n = AddCodex();
Assert.That(!string.IsNullOrEmpty(n.GetDebugInfo().Id));
Assert.That(!string.IsNullOrEmpty(n.GetDebugInfo().id));
}
}
}
@@ -0,0 +1,63 @@
using CodexPlugin;
using NUnit.Framework;
using Utils;
namespace CodexTests.BasicTests
{
[TestFixture]
public class BlockExchangeTests : CodexDistTest
{
[Test]
public void EmptyAfterExchange()
{
var bootstrap = AddCodex(s => s.WithName("bootstrap"));
var node = AddCodex(s => s.WithName("node").WithBootstrapNode(bootstrap));
AssertExchangeIsEmpty(bootstrap, node);
var file = GenerateTestFile(1.MB());
var cid = bootstrap.UploadFile(file);
node.DownloadContent(cid);
AssertExchangeIsEmpty(bootstrap, node);
}
[Test]
public void EmptyAfterExchangeWithBystander()
{
var bootstrap = AddCodex(s => s.WithName("bootstrap"));
var node = AddCodex(s => s.WithName("node").WithBootstrapNode(bootstrap));
var bystander = AddCodex(s => s.WithName("bystander").WithBootstrapNode(bootstrap));
AssertExchangeIsEmpty(bootstrap, node, bystander);
var file = GenerateTestFile(1.MB());
var cid = bootstrap.UploadFile(file);
node.DownloadContent(cid);
AssertExchangeIsEmpty(bootstrap, node, bystander);
}
private void AssertExchangeIsEmpty(params ICodexNode[] nodes)
{
foreach (var node in nodes)
{
// API Call not available in master-line Codex image.
//Time.Retry(() => AssertBlockExchangeIsEmpty(node), nameof(AssertExchangeIsEmpty));
}
}
//private void AssertBlockExchangeIsEmpty(ICodexNode node)
//{
// var msg = $"BlockExchange for {node.GetName()}: ";
// var response = node.GetDebugBlockExchange();
// foreach (var peer in response.peers)
// {
// var activeWants = peer.wants.Where(w => !w.cancel).ToArray();
// Assert.That(activeWants.Length, Is.EqualTo(0), msg + "thinks a peer has active wants.");
// }
// Assert.That(response.taskQueue, Is.EqualTo(0), msg + "has tasks in queue.");
// Assert.That(response.pendingBlocks, Is.EqualTo(0), msg + "has pending blocks.");
//}
}
}
@@ -21,10 +21,7 @@ namespace CodexTests.BasicTests
var group = AddCodex(5, o => o
.EnableMetrics()
.EnableMarketplace(geth, contract, s => s
.WithInitial(10.Eth(), 100000.TestTokens())
.AsStorageNode()
.AsValidator())
.EnableMarketplace(geth, contract, 10.Eth(), 100000.TestTokens(), isValidator: true)
.WithBlockTTL(TimeSpan.FromMinutes(5))
.WithBlockMaintenanceInterval(TimeSpan.FromSeconds(10))
.WithBlockMaintenanceNumber(100)
@@ -34,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);
@@ -104,7 +98,7 @@ namespace CodexTests.BasicTests
private void CheckRoutingTables(IEnumerable<ICodexNode> nodes)
{
var all = nodes.ToArray();
var allIds = all.Select(n => n.GetDebugInfo().Table.LocalNode.NodeId).ToArray();
var allIds = all.Select(n => n.GetDebugInfo().table.localNode.nodeId).ToArray();
var errors = all.Select(n => AreAllPresent(n, allIds)).Where(s => !string.IsNullOrEmpty(s)).ToArray();
@@ -117,8 +111,8 @@ namespace CodexTests.BasicTests
private string AreAllPresent(ICodexNode n, string[] allIds)
{
var info = n.GetDebugInfo();
var known = info.Table.Nodes.Select(n => n.NodeId).ToArray();
var expected = allIds.Where(i => i != info.Table.LocalNode.NodeId).ToArray();
var known = info.table.nodes.Select(n => n.nodeId).ToArray();
var expected = allIds.Where(i => i != info.table.localNode.nodeId).ToArray();
if (!expected.All(ex => known.Contains(ex)))
{
@@ -132,7 +126,7 @@ namespace CodexTests.BasicTests
private const string BytesStoredMetric = "codexRepostoreBytesUsed";
private void PerformTest(ICodexNode primary, ICodexNode secondary, RunningPod rc)
private void PerformTest(ICodexNode primary, ICodexNode secondary, RunningContainers rc)
{
ScopedTestFiles(() =>
{
@@ -154,7 +148,7 @@ namespace CodexTests.BasicTests
var newBytes = Convert.ToInt64(afterBytesStored.Values.Last().Value - beforeBytesStored.Values.Last().Value);
return high > newBytes && newBytes > low;
}, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(2), nameof(ContinuousSubstitute));
}, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(2));
FileUtils.TrackedFile? downloadedFile = null;
LogBytesPerMillisecond(() => downloadedFile = secondary.DownloadContent(contentId));
@@ -1,110 +0,0 @@
using CodexContractsPlugin;
using CodexDiscordBotPlugin;
using CodexPlugin;
using GethPlugin;
using NUnit.Framework;
using Utils;
namespace CodexTests.BasicTests
{
[TestFixture]
public class DiscordBotTests : AutoBootstrapDistTest
{
[Test]
public void BotRewardTest()
{
var myAccount = EthAccount.GenerateNew();
var sellerInitialBalance = 234.TestTokens();
var buyerInitialBalance = 100000.TestTokens();
var fileSize = 11.MB();
var geth = Ci.StartGethNode(s => s.IsMiner().WithName("disttest-geth"));
var contracts = Ci.StartCodexContracts(geth);
// start bot and rewarder
var gethInfo = new DiscordBotGethInfo(
host: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Host,
port: geth.Container.GetInternalAddress(GethContainerRecipe.HttpPortTag).Port,
privKey: geth.StartResult.Account.PrivateKey,
marketplaceAddress: contracts.Deployment.MarketplaceAddress,
tokenAddress: contracts.Deployment.TokenAddress,
abi: contracts.Deployment.Abi
);
var bot = Ci.DeployCodexDiscordBot(new DiscordBotStartupConfig(
name: "bot",
token: "aaa",
serverName: "ThatBen's server",
adminRoleName: "bottest-admins",
adminChannelName: "admin-channel",
rewardChannelName: "rewards-channel",
kubeNamespace: "notneeded",
gethInfo: gethInfo
));
var botContainer = bot.Containers.Single();
Ci.DeployRewarderBot(new RewarderBotStartupConfig(
//discordBotHost: "http://" + botContainer.GetAddress(GetTestLog(), DiscordBotContainerRecipe.RewardsPort).Host,
//discordBotPort: botContainer.GetAddress(GetTestLog(), DiscordBotContainerRecipe.RewardsPort).Port,
discordBotHost: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Host,
discordBotPort: botContainer.GetInternalAddress(DiscordBotContainerRecipe.RewardsPort).Port,
intervalMinutes: "1",
historyStartUtc: GetTestRunTimeRange().From - TimeSpan.FromMinutes(3),
gethInfo: gethInfo,
dataPath: null
));
var numberOfHosts = 3;
for (var i = 0; i < numberOfHosts; i++)
{
var seller = AddCodex(s => s
.WithName("Seller")
.WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn)
{
ContractClock = CodexLogLevel.Trace,
})
.WithStorageQuota(11.GB())
.EnableMarketplace(geth, contracts, m => m
.WithAccount(myAccount)
.WithInitial(10.Eth(), sellerInitialBalance)
.AsStorageNode()
.AsValidator()));
var availability = new StorageAvailability(
totalSpace: 10.GB(),
maxDuration: TimeSpan.FromMinutes(30),
minPriceForTotalSpace: 1.TestTokens(),
maxCollateral: 20.TestTokens()
);
seller.Marketplace.MakeStorageAvailable(availability);
}
var testFile = GenerateTestFile(fileSize);
var buyer = AddCodex(s => s
.WithName("Buyer")
.EnableMarketplace(geth, contracts, m => m
.WithAccount(myAccount)
.WithInitial(10.Eth(), buyerInitialBalance)));
var contentId = buyer.UploadFile(testFile);
var purchase = new StoragePurchaseRequest(contentId)
{
PricePerSlotPerSecond = 2.TestTokens(),
RequiredCollateral = 10.TestTokens(),
MinRequiredNumberOfNodes = 5,
NodeFailureTolerance = 2,
ProofProbability = 5,
Duration = TimeSpan.FromMinutes(6),
Expiry = TimeSpan.FromMinutes(5)
};
var purchaseContract = buyer.Marketplace.RequestStorage(purchase);
purchaseContract.WaitForStorageContractStarted();
purchaseContract.WaitForStorageContractFinished();
}
}
}
+98 -3
View File
@@ -1,9 +1,12 @@
using CodexPlugin;
using CodexContractsPlugin;
using CodexPlugin;
using DistTestCore;
using GethPlugin;
using MetricsPlugin;
using Nethereum.Hex.HexConvertors.Extensions;
using NUnit.Framework;
using Utils;
using Request = CodexContractsPlugin.Marketplace.Request;
namespace CodexTests.BasicTests
{
@@ -17,8 +20,8 @@ namespace CodexTests.BasicTests
var cid = primary.UploadFile(GenerateTestFile(5.MB()));
var localDatasets = primary.LocalFiles();
CollectionAssert.Contains(localDatasets.Content.Select(c => c.Cid), cid);
var content = primary.LocalFiles();
CollectionAssert.Contains(content.Select(c => c.Cid), cid);
var log = Ci.DownloadLog(primary);
@@ -47,6 +50,65 @@ namespace CodexTests.BasicTests
metrics[1].AssertThat("libp2p_peers", Is.EqualTo(1));
}
[Test]
public void MarketplaceExample()
{
var sellerInitialBalance = 234.TestTokens();
var buyerInitialBalance = 1000.TestTokens();
var fileSize = 10.MB();
var geth = Ci.StartGethNode(s => s.IsMiner().WithName("disttest-geth"));
var contracts = Ci.StartCodexContracts(geth);
var seller = AddCodex(s => s
.WithName("Seller")
.WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn))
.WithStorageQuota(11.GB())
.EnableMarketplace(geth, contracts, initialEth: 10.Eth(), initialTokens: sellerInitialBalance, isValidator: true)
.WithSimulateProofFailures(failEveryNProofs: 3));
AssertBalance(contracts, seller, Is.EqualTo(sellerInitialBalance));
seller.Marketplace.MakeStorageAvailable(
size: 10.GB(),
minPriceForTotalSpace: 1.TestTokens(),
maxCollateral: 20.TestTokens(),
maxDuration: TimeSpan.FromMinutes(3));
var testFile = GenerateTestFile(fileSize);
var buyer = AddCodex(s => s
.WithName("Buyer")
.WithBootstrapNode(seller)
.EnableMarketplace(geth, contracts, initialEth: 10.Eth(), initialTokens: buyerInitialBalance));
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));
purchaseContract.WaitForStorageContractStarted(fileSize);
AssertBalance(contracts, seller, Is.LessThan(sellerInitialBalance), "Collateral was not placed.");
var request = GetOnChainStorageRequest(contracts);
AssertStorageRequest(request, contracts, buyer);
AssertSlotFilledEvents(contracts, request, seller);
AssertContractSlot(contracts, request, 0, seller);
purchaseContract.WaitForStorageContractFinished();
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]
public void GethBootstrapTest()
{
@@ -63,5 +125,38 @@ namespace CodexTests.BasicTests
Assert.That(bootN, Is.EqualTo(followN));
Assert.That(discN, Is.LessThan(bootN));
}
private void AssertSlotFilledEvents(ICodexContracts contracts, Request request, ICodexNode seller)
{
var requestFulfilledEvents = contracts.GetRequestFulfilledEvents(GetTestRunTimeRange());
Assert.That(requestFulfilledEvents.Length, Is.EqualTo(1));
CollectionAssert.AreEqual(request.RequestId, requestFulfilledEvents[0].RequestId);
var filledSlotEvents = contracts.GetSlotFilledEvents(GetTestRunTimeRange());
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, 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(1));
}
private Request GetOnChainStorageRequest(ICodexContracts contracts)
{
var requests = contracts.GetStorageRequests(GetTestRunTimeRange());
Assert.That(requests.Length, Is.EqualTo(1));
return requests.Single();
}
private void AssertContractSlot(ICodexContracts contracts, Request request, int contractSlotIndex, ICodexNode expectedSeller)
{
var slotHost = contracts.GetSlotHost(request, contractSlotIndex);
Assert.That(slotHost, Is.EqualTo(expectedSeller.EthAddress));
}
}
}
@@ -1,121 +0,0 @@
using CodexContractsPlugin;
using CodexContractsPlugin.Marketplace;
using CodexPlugin;
using GethPlugin;
using NUnit.Framework;
using Utils;
namespace CodexTests.BasicTests
{
[TestFixture]
public class MarketplaceTests : AutoBootstrapDistTest
{
[Test]
public void MarketplaceExample()
{
var hostInitialBalance = 234.TestTokens();
var clientInitialBalance = 100000.TestTokens();
var fileSize = 10.MB();
var geth = Ci.StartGethNode(s => s.IsMiner().WithName("disttest-geth"));
var contracts = Ci.StartCodexContracts(geth);
var numberOfHosts = 5;
var hosts = AddCodex(numberOfHosts, s => s
.WithName("Host")
.WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn)
{
ContractClock = CodexLogLevel.Trace,
})
.WithStorageQuota(11.GB())
.EnableMarketplace(geth, contracts, m => m
.WithInitial(10.Eth(), hostInitialBalance)
.AsStorageNode()
.AsValidator()));
var expectedHostBalance = (numberOfHosts * hostInitialBalance.Amount).TestTokens();
foreach (var host in hosts)
{
AssertBalance(contracts, host, Is.EqualTo(expectedHostBalance));
var availability = new StorageAvailability(
totalSpace: 10.GB(),
maxDuration: TimeSpan.FromMinutes(30),
minPriceForTotalSpace: 1.TestTokens(),
maxCollateral: 20.TestTokens()
);
host.Marketplace.MakeStorageAvailable(availability);
}
var testFile = GenerateTestFile(fileSize);
var client = AddCodex(s => s
.WithName("Client")
.EnableMarketplace(geth, contracts, m => m
.WithInitial(10.Eth(), clientInitialBalance)));
AssertBalance(contracts, client, Is.EqualTo(clientInitialBalance));
var contentId = client.UploadFile(testFile);
var purchase = new StoragePurchaseRequest(contentId)
{
PricePerSlotPerSecond = 2.TestTokens(),
RequiredCollateral = 10.TestTokens(),
MinRequiredNumberOfNodes = 5,
NodeFailureTolerance = 2,
ProofProbability = 5,
Duration = TimeSpan.FromMinutes(5),
Expiry = TimeSpan.FromMinutes(4)
};
var purchaseContract = client.Marketplace.RequestStorage(purchase);
WaitForAllSlotFilledEvents(contracts, purchase, geth);
purchaseContract.WaitForStorageContractStarted();
var request = GetOnChainStorageRequest(contracts, geth);
AssertStorageRequest(request, purchase, contracts, client);
AssertContractSlot(contracts, request, 0);
purchaseContract.WaitForStorageContractFinished();
AssertBalance(contracts, client, Is.LessThan(clientInitialBalance), "Buyer was not charged for storage.");
Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Finished));
}
private void WaitForAllSlotFilledEvents(ICodexContracts contracts, StoragePurchaseRequest purchase, IGethNode geth)
{
Time.Retry(() =>
{
var blockRange = geth.ConvertTimeRangeToBlockRange(GetTestRunTimeRange());
var slotFilledEvents = contracts.GetSlotFilledEvents(blockRange);
Log($"SlotFilledEvents: {slotFilledEvents.Length} - NumSlots: {purchase.MinRequiredNumberOfNodes}");
if (slotFilledEvents.Length != purchase.MinRequiredNumberOfNodes) throw new Exception();
}, Convert.ToInt32(purchase.Duration.TotalSeconds / 5) + 10, TimeSpan.FromSeconds(5), "Checking SlotFilled events");
}
private void AssertStorageRequest(Request 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(request.Ask.Slots, Is.EqualTo(purchase.MinRequiredNumberOfNodes));
}
private Request GetOnChainStorageRequest(ICodexContracts contracts, IGethNode geth)
{
var requests = contracts.GetStorageRequests(geth.ConvertTimeRangeToBlockRange(GetTestRunTimeRange()));
Assert.That(requests.Length, Is.EqualTo(1));
return requests.Single();
}
private void AssertContractSlot(ICodexContracts contracts, Request request, int contractSlotIndex)
{
var slotHost = contracts.GetSlotHost(request, contractSlotIndex);
Assert.That(slotHost?.Address, Is.Not.Null);
}
}
}
@@ -19,7 +19,7 @@ namespace CodexTests.BasicTests
{
node = Ci.StartCodexNode();
Time.WaitUntil(() => node == null, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(5), nameof(SetUpANodeAndWait));
Time.WaitUntil(() => node == null, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(5));
}
[Test]
@@ -27,7 +27,7 @@ namespace CodexTests.BasicTests
{
var myNode = Ci.StartCodexNode();
Time.WaitUntil(() => node != null, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5), nameof(ForeignNodeConnects));
Time.WaitUntil(() => node != null, TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5));
try
{
@@ -21,7 +21,7 @@ namespace CodexTests.BasicTests
{
var primary = Ci.StartCodexNode();
primary.Stop(waitTillStopped: true);
primary.Stop();
primary = Ci.StartCodexNode();
+16 -12
View File
@@ -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);
}
}
}
-1
View File
@@ -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");

Some files were not shown because too many files have changed in this diff Show More