From 65473d8c15be1cc0bda1a6d6a133c70666f7a8ec Mon Sep 17 00:00:00 2001 From: GoodDaisy <90915921+GoodDaisy@users.noreply.github.com> Date: Thu, 26 Oct 2023 20:26:45 +0800 Subject: [PATCH 01/20] fix Continous typo --- CONTRIBUTINGPLUGINS.MD | 2 +- CONTRIBUTINGTESTS.MD | 2 +- README.md | 2 +- Tests/CodexContinuousTests/Program.cs | 2 +- .../reports/CodexTestNetReport-August2023.md | 2 +- .../CodexContinuousTests/reports/CodexTestNetReport-July2023.md | 2 +- .../reports/CodexTestNetReport-September2023.md | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTINGPLUGINS.MD b/CONTRIBUTINGPLUGINS.MD index 71619fb8..231ba6c5 100644 --- a/CONTRIBUTINGPLUGINS.MD +++ b/CONTRIBUTINGPLUGINS.MD @@ -82,5 +82,5 @@ public static class CoreInterfaceExtensions } ``` -The primary reason to decouple deploying and wrapping functionalities is that some use cases require these steps to be performed by separate applications, and different moments in time. For this reason, whatever is returned by the deploy methods should be serializable. After deserialization at some later time, it should then be valid input for the wrap method. The Codex continuous tests system is a clear example of this use case: The `CodexNetDeployer` tool uses deploy methods to create Codex nodes. Then it writes the returned objects to a JSON file. Some time later, the `CodexContinousTests` application uses this JSON file to reconstruct the objects created by the deploy methods. It then uses the wrap methods to create accessors and interactors, which are used for testing. +The primary reason to decouple deploying and wrapping functionalities is that some use cases require these steps to be performed by separate applications, and different moments in time. For this reason, whatever is returned by the deploy methods should be serializable. After deserialization at some later time, it should then be valid input for the wrap method. The Codex continuous tests system is a clear example of this use case: The `CodexNetDeployer` tool uses deploy methods to create Codex nodes. Then it writes the returned objects to a JSON file. Some time later, the `CodexContinuousTests` application uses this JSON file to reconstruct the objects created by the deploy methods. It then uses the wrap methods to create accessors and interactors, which are used for testing. diff --git a/CONTRIBUTINGTESTS.MD b/CONTRIBUTINGTESTS.MD index 070e192a..bcad8a6d 100644 --- a/CONTRIBUTINGTESTS.MD +++ b/CONTRIBUTINGTESTS.MD @@ -21,7 +21,7 @@ Do you want to write some tests using this distributed test setup? Great! Here's 1. When using the auto-bootstrap, you have no control over the bootstrap node from your tests. You can't (for example) shut it down during the course of the test. If you need this level of control for your scenario, use the `CodexDistTest` instead. 1. If your test needs a long time to run, add the `[UseLongTimeouts]` function attribute. This will greatly increase maximum time-out values for operations like for example uploading and downloading files. ### Continuous tests -1. Add new code files to `Tests/CodexContinousTests/Tests` +1. Add new code files to `Tests/CodexContinuousTests/Tests` 1. Inherrit from `ContinuousTest` 1. Define one or more methods and decorate them with the `[TestMoment(...)]` attribute. 1. The TestMoment takes a number of seconds as argument. Each moment will be executed by the continuous test runner applying the given seconds as delay. (Non-cumulative. So two moments at T:10 will be executed one after another without delay, in this case the order of execution should not be depended upon.) diff --git a/README.md b/README.md index 7b22bd21..0aba7b78 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Nethereum: v4.14.0 These are test assemblies that use NUnit3 to perform tests against transient Codex nodes. Read more [HERE](/Tests/CodexTests/README.md) -## Tests/ContinousTests +## Tests/ContinuousTests A console application that runs tests in an endless loop against a persistent deployment of Codex nodes. Read more [HERE](/Tests/CodexContinuousTests/README.md) diff --git a/Tests/CodexContinuousTests/Program.cs b/Tests/CodexContinuousTests/Program.cs index 3db3b15e..a99dc008 100644 --- a/Tests/CodexContinuousTests/Program.cs +++ b/Tests/CodexContinuousTests/Program.cs @@ -4,7 +4,7 @@ public class Program { public static void Main(string[] args) { - Console.WriteLine("Codex Continous-Test-Runner."); + Console.WriteLine("Codex Continuous-Test-Runner."); var runner = new ContinuousTestRunner(args, Cancellation.Cts.Token); diff --git a/Tests/CodexContinuousTests/reports/CodexTestNetReport-August2023.md b/Tests/CodexContinuousTests/reports/CodexTestNetReport-August2023.md index 2190d3b2..82ea08ad 100644 --- a/Tests/CodexContinuousTests/reports/CodexTestNetReport-August2023.md +++ b/Tests/CodexContinuousTests/reports/CodexTestNetReport-August2023.md @@ -13,7 +13,7 @@ Report for: 08-2023 (Stopped: The number of tests that can successfully run on the test-net is not high enough to justify the cost of leaving it running.) ## Deployment Configuration -Continous Test-net is deployed to the kubernetes cluster with the following configuration: +Continuous Test-net is deployed to the kubernetes cluster with the following configuration: 5x Codex Nodes: - Log-level: Trace diff --git a/Tests/CodexContinuousTests/reports/CodexTestNetReport-July2023.md b/Tests/CodexContinuousTests/reports/CodexTestNetReport-July2023.md index 3bb80d9a..9b48b96d 100644 --- a/Tests/CodexContinuousTests/reports/CodexTestNetReport-July2023.md +++ b/Tests/CodexContinuousTests/reports/CodexTestNetReport-July2023.md @@ -11,7 +11,7 @@ Report for: 07-2023 (Faulted: Tests fail with such frequency that the information gathered does not justify the cost of leaving the test-net running.) ## Deployment Configuration -Continous Test-net is deployed to the kubernetes cluster with the following configuration: +Continuous Test-net is deployed to the kubernetes cluster with the following configuration: 5x Codex Nodes: - Log-level: Trace diff --git a/Tests/CodexContinuousTests/reports/CodexTestNetReport-September2023.md b/Tests/CodexContinuousTests/reports/CodexTestNetReport-September2023.md index 65bc44db..15a6639e 100644 --- a/Tests/CodexContinuousTests/reports/CodexTestNetReport-September2023.md +++ b/Tests/CodexContinuousTests/reports/CodexTestNetReport-September2023.md @@ -11,7 +11,7 @@ Report for: 09-2023 (Stopped: The number of tests that can successfully run on the test-net is not high enough to justify the cost of leaving it running.) ## Deployment Configuration -Continous Test-net is deployed to the kubernetes cluster with the following configuration: +Continuous Test-net is deployed to the kubernetes cluster with the following configuration: 5x Codex Nodes: - Log-level: Trace From e42f1ddbd74bc5d8f67b5e121ac91e9241108ff9 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 13 Mar 2024 10:01:14 +0100 Subject: [PATCH 02/20] Adds support for command overrides to container recipes. --- Framework/KubernetesWorkflow/K8sController.cs | 9 ++++++++- .../KubernetesWorkflow/Recipe/CommandOverride.cs | 12 ++++++++++++ .../KubernetesWorkflow/Recipe/ContainerRecipe.cs | 4 +++- .../Recipe/ContainerRecipeFactory.cs | 9 ++++++++- 4 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 Framework/KubernetesWorkflow/Recipe/CommandOverride.cs diff --git a/Framework/KubernetesWorkflow/K8sController.cs b/Framework/KubernetesWorkflow/K8sController.cs index bfea181b..8af6fd86 100644 --- a/Framework/KubernetesWorkflow/K8sController.cs +++ b/Framework/KubernetesWorkflow/K8sController.cs @@ -498,10 +498,17 @@ namespace KubernetesWorkflow Ports = CreateContainerPorts(recipe), Env = CreateEnv(recipe), VolumeMounts = CreateContainerVolumeMounts(recipe), - Resources = CreateResourceLimits(recipe) + Resources = CreateResourceLimits(recipe), + Command = CreateCommandList(recipe) }; } + private IList 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 diff --git a/Framework/KubernetesWorkflow/Recipe/CommandOverride.cs b/Framework/KubernetesWorkflow/Recipe/CommandOverride.cs new file mode 100644 index 00000000..7821c813 --- /dev/null +++ b/Framework/KubernetesWorkflow/Recipe/CommandOverride.cs @@ -0,0 +1,12 @@ +namespace KubernetesWorkflow.Recipe +{ + public class CommandOverride + { + public CommandOverride(params string[] command) + { + Command = command; + } + + public string[] Command { get; } + } +} diff --git a/Framework/KubernetesWorkflow/Recipe/ContainerRecipe.cs b/Framework/KubernetesWorkflow/Recipe/ContainerRecipe.cs index fb7c8a43..78fcb63e 100644 --- a/Framework/KubernetesWorkflow/Recipe/ContainerRecipe.cs +++ b/Framework/KubernetesWorkflow/Recipe/ContainerRecipe.cs @@ -2,13 +2,14 @@ { public class ContainerRecipe { - 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) + 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) { Number = number; NameOverride = nameOverride; Image = image; Resources = resources; SchedulingAffinity = schedulingAffinity; + CommandOverride = commandOverride; SetCriticalPriority = setCriticalPriority; ExposedPorts = exposedPorts; InternalPorts = internalPorts; @@ -35,6 +36,7 @@ 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; } diff --git a/Framework/KubernetesWorkflow/Recipe/ContainerRecipeFactory.cs b/Framework/KubernetesWorkflow/Recipe/ContainerRecipeFactory.cs index 931013dc..6b6ae2df 100644 --- a/Framework/KubernetesWorkflow/Recipe/ContainerRecipeFactory.cs +++ b/Framework/KubernetesWorkflow/Recipe/ContainerRecipeFactory.cs @@ -14,6 +14,7 @@ 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) @@ -24,7 +25,7 @@ namespace KubernetesWorkflow.Recipe Initialize(config); - var recipe = new ContainerRecipe(containerNumber, config.NameOverride, Image, resources, schedulingAffinity, setCriticalPriority, + var recipe = new ContainerRecipe(containerNumber, config.NameOverride, Image, resources, schedulingAffinity, commandOverride, setCriticalPriority, exposedPorts.ToArray(), internalPorts.ToArray(), envVars.ToArray(), @@ -43,6 +44,7 @@ namespace KubernetesWorkflow.Recipe this.factory = null!; resources = new ContainerResources(); schedulingAffinity = new SchedulingAffinity(); + commandOverride = new CommandOverride(); setCriticalPriority = false; return recipe; @@ -130,6 +132,11 @@ namespace KubernetesWorkflow.Recipe schedulingAffinity = new SchedulingAffinity(notIn); } + protected void OverrideCommand(params string[] command) + { + commandOverride = new CommandOverride(command); + } + protected void SetSystemCriticalPriority() { setCriticalPriority = true; From b7904f7ee0afb43c2fcaa3c36720e417c6bc5e83 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 13 Mar 2024 10:14:11 +0100 Subject: [PATCH 03/20] Updates codexSetup and Marketplace configuration --- ProjectPlugins/CodexPlugin/CodexSetup.cs | 54 +++++++++++++++++-- .../CodexPlugin/MarketplaceInitialConfig.cs | 6 +-- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/ProjectPlugins/CodexPlugin/CodexSetup.cs b/ProjectPlugins/CodexPlugin/CodexSetup.cs index ccac2527..7e421216 100644 --- a/ProjectPlugins/CodexPlugin/CodexSetup.cs +++ b/ProjectPlugins/CodexPlugin/CodexSetup.cs @@ -17,7 +17,7 @@ namespace CodexPlugin ICodexSetup WithBlockMaintenanceInterval(TimeSpan duration); ICodexSetup WithBlockMaintenanceNumber(int numberOfBlocks); ICodexSetup EnableMetrics(); - ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens, bool isValidator = false); + ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens, Action marketplaceSetup); /// /// Provides an invalid proof every N proofs /// @@ -25,6 +25,13 @@ namespace CodexPlugin ICodexSetup AsPublicTestNet(CodexTestNetConfig testNetConfig); } + public interface IMarketplaceSetup + { + IMarketplaceSetup AsStorageNode(); + IMarketplaceSetup AsClientNode(); + IMarketplaceSetup AsValidator(); + } + public class CodexLogCustomTopics { public CodexLogCustomTopics(CodexLogLevel discV5, CodexLogLevel libp2p, CodexLogLevel blockExchange) @@ -115,9 +122,12 @@ namespace CodexPlugin return this; } - public ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens, bool isValidator = false) + public ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens, Action marketplaceSetup) { - MarketplaceConfig = new MarketplaceInitialConfig(gethNode, codexContracts, initialEth, initialTokens, isValidator); + var ms = new MarketplaceSetup(); + marketplaceSetup(ms); + + MarketplaceConfig = new MarketplaceInitialConfig(ms, gethNode, codexContracts, initialEth, initialTokens); return this; } @@ -146,7 +156,43 @@ 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 $"IsValidator={MarketplaceConfig.IsValidator}"; + if (MarketplaceConfig != null) yield return $"MarketplaceSetup={MarketplaceConfig.MarketplaceSetup}"; } } + + public class MarketplaceSetup : IMarketplaceSetup + { + public bool IsClientNode { get; private set; } + public bool IsStorageNode { get; private set; } + public bool IsValidator { get; private set; } + + public IMarketplaceSetup AsClientNode() + { + IsClientNode = true; + return this; + } + + public IMarketplaceSetup AsStorageNode() + { + IsStorageNode = true; + return this; + } + + public IMarketplaceSetup AsValidator() + { + IsValidator = true; + return this; + } + + public override string ToString() + { + var result = "["; + result += IsClientNode ? "(clientNode)" : "()"; + result += IsStorageNode ? "(storageNode)" : "()"; + result += IsValidator ? "(validator)" : "()"; + result += "]"; + return result; + } + } + } diff --git a/ProjectPlugins/CodexPlugin/MarketplaceInitialConfig.cs b/ProjectPlugins/CodexPlugin/MarketplaceInitialConfig.cs index 7c925916..6bd49999 100644 --- a/ProjectPlugins/CodexPlugin/MarketplaceInitialConfig.cs +++ b/ProjectPlugins/CodexPlugin/MarketplaceInitialConfig.cs @@ -5,19 +5,19 @@ namespace CodexPlugin { public class MarketplaceInitialConfig { - public MarketplaceInitialConfig(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens, bool isValidator) + public MarketplaceInitialConfig(MarketplaceSetup marketplaceSetup, IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens) { + 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; } } } From a6a8c3f1e745a476f8a88ff201799ba99e8d994c Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 13 Mar 2024 10:18:10 +0100 Subject: [PATCH 04/20] Update codex container recipe --- .../CodexPlugin/CodexContainerRecipe.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs index 87d7f06c..78bb4f2a 100644 --- a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs +++ b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs @@ -105,7 +105,6 @@ namespace CodexPlugin AddEnvVar("CODEX_ETH_PROVIDER", $"{wsAddress.Host.Replace("http://", "ws://")}:{wsAddress.Port}"); AddEnvVar("CODEX_MARKETPLACE_ADDRESS", marketplaceAddress); - AddEnvVar("CODEX_PERSISTENCE", "true"); // Custom scripting in the Codex test image will write this variable to a private-key file, // and pass the correct filename to Codex. @@ -113,7 +112,9 @@ namespace CodexPlugin AddEnvVar("PRIV_KEY", mStart.PrivateKey); Additional(mStart); - if (config.MarketplaceConfig.IsValidator) + var marketplaceSetup = config.MarketplaceConfig.MarketplaceSetup; + SetCommandOverride(marketplaceSetup); + if (marketplaceSetup.IsValidator) { AddEnvVar("CODEX_VALIDATOR", "true"); } @@ -125,6 +126,21 @@ namespace CodexPlugin } } + private void SetCommandOverride(MarketplaceSetup ms) + { + var persistenceRequired = ms.IsClientNode || ms.IsStorageNode || ms.IsValidator; + var proverRequired = ms.IsStorageNode; + + if (persistenceRequired && proverRequired) + { + OverrideCommand("codex", "persistence", "prover"); + } + else if (persistenceRequired) + { + OverrideCommand("codex", "persistence"); + } + } + private Port CreateApiPort(CodexStartupConfig config, string tag) { if (config.PublicTestNet == null) return AddExposedPort(tag); From 90b90be3cbf0d95dcc29f781a654fb35ad039d66 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 13 Mar 2024 10:29:26 +0100 Subject: [PATCH 05/20] Updates usages of EnableMarketplace --- .../CodexPlugin/CodexContainerRecipe.cs | 7 ++----- ProjectPlugins/CodexPlugin/CodexSetup.cs | 17 +++++++---------- .../BasicTests/ContinuousSubstitute.cs | 4 +++- Tests/CodexTests/BasicTests/ExampleTests.cs | 4 +++- Tools/CodexNetDeployer/CodexNodeStarter.cs | 6 +++++- 5 files changed, 20 insertions(+), 18 deletions(-) diff --git a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs index 78bb4f2a..b48f6058 100644 --- a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs +++ b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs @@ -128,14 +128,11 @@ namespace CodexPlugin private void SetCommandOverride(MarketplaceSetup ms) { - var persistenceRequired = ms.IsClientNode || ms.IsStorageNode || ms.IsValidator; - var proverRequired = ms.IsStorageNode; - - if (persistenceRequired && proverRequired) + if (ms.IsStorageNode) { OverrideCommand("codex", "persistence", "prover"); } - else if (persistenceRequired) + else { OverrideCommand("codex", "persistence"); } diff --git a/ProjectPlugins/CodexPlugin/CodexSetup.cs b/ProjectPlugins/CodexPlugin/CodexSetup.cs index 7e421216..54be6a9f 100644 --- a/ProjectPlugins/CodexPlugin/CodexSetup.cs +++ b/ProjectPlugins/CodexPlugin/CodexSetup.cs @@ -17,6 +17,7 @@ namespace CodexPlugin ICodexSetup WithBlockMaintenanceInterval(TimeSpan duration); ICodexSetup WithBlockMaintenanceNumber(int numberOfBlocks); ICodexSetup EnableMetrics(); + ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens); ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens, Action marketplaceSetup); /// /// Provides an invalid proof every N proofs @@ -28,7 +29,6 @@ namespace CodexPlugin public interface IMarketplaceSetup { IMarketplaceSetup AsStorageNode(); - IMarketplaceSetup AsClientNode(); IMarketplaceSetup AsValidator(); } @@ -122,6 +122,11 @@ namespace CodexPlugin return this; } + public ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens) + { + return EnableMarketplace(gethNode, codexContracts, initialEth, initialTokens, s => { }); + } + public ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens, Action marketplaceSetup) { var ms = new MarketplaceSetup(); @@ -162,16 +167,9 @@ namespace CodexPlugin public class MarketplaceSetup : IMarketplaceSetup { - public bool IsClientNode { get; private set; } public bool IsStorageNode { get; private set; } public bool IsValidator { get; private set; } - public IMarketplaceSetup AsClientNode() - { - IsClientNode = true; - return this; - } - public IMarketplaceSetup AsStorageNode() { IsStorageNode = true; @@ -186,8 +184,7 @@ namespace CodexPlugin public override string ToString() { - var result = "["; - result += IsClientNode ? "(clientNode)" : "()"; + var result = "[(clientNode)"; // When marketplace is enabled, being a clientNode is implicit. result += IsStorageNode ? "(storageNode)" : "()"; result += IsValidator ? "(validator)" : "()"; result += "]"; diff --git a/Tests/CodexTests/BasicTests/ContinuousSubstitute.cs b/Tests/CodexTests/BasicTests/ContinuousSubstitute.cs index 9974d7d9..ad22bac3 100644 --- a/Tests/CodexTests/BasicTests/ContinuousSubstitute.cs +++ b/Tests/CodexTests/BasicTests/ContinuousSubstitute.cs @@ -21,7 +21,9 @@ namespace CodexTests.BasicTests var group = AddCodex(5, o => o .EnableMetrics() - .EnableMarketplace(geth, contract, 10.Eth(), 100000.TestTokens(), isValidator: true) + .EnableMarketplace(geth, contract, 10.Eth(), 100000.TestTokens(), s => s + .AsStorageNode() + .AsValidator()) .WithBlockTTL(TimeSpan.FromMinutes(5)) .WithBlockMaintenanceInterval(TimeSpan.FromSeconds(10)) .WithBlockMaintenanceNumber(100) diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index fb2edcf4..c70b5aa3 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -64,7 +64,9 @@ namespace CodexTests.BasicTests .WithName("Seller") .WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn)) .WithStorageQuota(11.GB()) - .EnableMarketplace(geth, contracts, initialEth: 10.Eth(), initialTokens: sellerInitialBalance, isValidator: true) + .EnableMarketplace(geth, contracts, initialEth: 10.Eth(), initialTokens: sellerInitialBalance, s => s + .AsStorageNode() + .AsValidator()) .WithSimulateProofFailures(failEveryNProofs: 3)); AssertBalance(contracts, seller, Is.EqualTo(sellerInitialBalance)); diff --git a/Tools/CodexNetDeployer/CodexNodeStarter.cs b/Tools/CodexNetDeployer/CodexNodeStarter.cs index 7b540013..71e7a6a6 100644 --- a/Tools/CodexNetDeployer/CodexNodeStarter.cs +++ b/Tools/CodexNetDeployer/CodexNodeStarter.cs @@ -41,7 +41,11 @@ namespace CodexNetDeployer if (config.ShouldMakeStorageAvailable) { - s.EnableMarketplace(gethNode, contracts, 100.Eth(), config.InitialTestTokens.TestTokens(), validatorsLeft > 0); + s.EnableMarketplace(gethNode, contracts, 100.Eth(), config.InitialTestTokens.TestTokens(), s => + { + if (validatorsLeft > 0) s.AsValidator(); + if (config.ShouldMakeStorageAvailable) s.AsStorageNode(); + }); } if (bootstrapNode != null) s.WithBootstrapNode(bootstrapNode); From c5fb066c75320756bef8ba7568f65b6fde447ad7 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 13 Mar 2024 10:57:26 +0100 Subject: [PATCH 06/20] Allows for non-blocking stop of containers --- Framework/KubernetesWorkflow/K8sController.cs | 5 +++-- Framework/KubernetesWorkflow/StartupWorkflow.cs | 6 +++--- .../CodexContractsPlugin/CodexContractsStarter.cs | 2 +- ProjectPlugins/CodexPlugin/CodexNode.cs | 6 +++--- ProjectPlugins/CodexPlugin/CodexNodeGroup.cs | 6 +++--- ProjectPlugins/CodexPlugin/CodexStarter.cs | 4 ++-- Tests/CodexTests/BasicTests/OneClientTests.cs | 2 +- 7 files changed, 16 insertions(+), 15 deletions(-) diff --git a/Framework/KubernetesWorkflow/K8sController.cs b/Framework/KubernetesWorkflow/K8sController.cs index 8af6fd86..b14267d3 100644 --- a/Framework/KubernetesWorkflow/K8sController.cs +++ b/Framework/KubernetesWorkflow/K8sController.cs @@ -49,13 +49,14 @@ namespace KubernetesWorkflow return CreatePodInfo(pod); } - public void Stop(StartResult startResult) + public void Stop(StartResult startResult, bool waitTillStopped) { log.Debug(); if (startResult.InternalService != null) DeleteService(startResult.InternalService); if (startResult.ExternalService != null) DeleteService(startResult.ExternalService); DeleteDeployment(startResult.Deployment); - WaitUntilPodsForDeploymentAreOffline(startResult.Deployment); + + if (waitTillStopped) WaitUntilPodsForDeploymentAreOffline(startResult.Deployment); } public void DownloadPodLog(RunningContainer container, ILogHandler logHandler, int? tailLines) diff --git a/Framework/KubernetesWorkflow/StartupWorkflow.cs b/Framework/KubernetesWorkflow/StartupWorkflow.cs index 15bc3e30..4d1a2f67 100644 --- a/Framework/KubernetesWorkflow/StartupWorkflow.cs +++ b/Framework/KubernetesWorkflow/StartupWorkflow.cs @@ -14,7 +14,7 @@ namespace KubernetesWorkflow PodInfo GetPodInfo(RunningContainer container); PodInfo GetPodInfo(RunningContainers containers); CrashWatcher CreateCrashWatcher(RunningContainer container); - void Stop(RunningContainers containers); + void Stop(RunningContainers containers, bool waitTillStopped); void DownloadContainerLog(RunningContainer container, ILogHandler logHandler, int? tailLines = null); string ExecuteCommand(RunningContainer container, string command, params string[] args); void DeleteNamespace(); @@ -86,11 +86,11 @@ namespace KubernetesWorkflow return K8s(c => c.CreateCrashWatcher(container)); } - public void Stop(RunningContainers runningContainers) + public void Stop(RunningContainers runningContainers, bool waitTillStopped) { K8s(controller => { - controller.Stop(runningContainers.StartResult); + controller.Stop(runningContainers.StartResult, waitTillStopped); cluster.Configuration.Hooks.OnContainersStopped(runningContainers); }); } diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsStarter.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsStarter.cs index f7864de3..a6f6641e 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsStarter.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsStarter.cs @@ -33,7 +33,7 @@ namespace CodexContractsPlugin try { var result = DeployContract(container, workflow, gethNode); - workflow.Stop(containers); + workflow.Stop(containers, waitTillStopped: false); Log("Container stopped."); return result; } diff --git a/ProjectPlugins/CodexPlugin/CodexNode.cs b/ProjectPlugins/CodexPlugin/CodexNode.cs index 2c79459e..de7f10fc 100644 --- a/ProjectPlugins/CodexPlugin/CodexNode.cs +++ b/ProjectPlugins/CodexPlugin/CodexNode.cs @@ -26,7 +26,7 @@ namespace CodexPlugin CrashWatcher CrashWatcher { get; } PodInfo GetPodInfo(); ITransferSpeeds TransferSpeeds { get; } - void Stop(); + void Stop(bool waitTillStopped); } public class CodexNode : ICodexNode @@ -153,13 +153,13 @@ namespace CodexPlugin return CodexAccess.GetPodInfo(); } - public void Stop() + public void Stop(bool 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(); + Group.BringOffline(waitTillStopped); } public void EnsureOnlineGetVersionResponse() diff --git a/ProjectPlugins/CodexPlugin/CodexNodeGroup.cs b/ProjectPlugins/CodexPlugin/CodexNodeGroup.cs index 0b6dee7d..e9eceb75 100644 --- a/ProjectPlugins/CodexPlugin/CodexNodeGroup.cs +++ b/ProjectPlugins/CodexPlugin/CodexNodeGroup.cs @@ -7,7 +7,7 @@ namespace CodexPlugin { public interface ICodexNodeGroup : IEnumerable, IHasManyMetricScrapeTargets { - void BringOffline(); + void BringOffline(bool waitTillStopped); ICodexNode this[int index] { get; } } @@ -31,9 +31,9 @@ namespace CodexPlugin } } - public void BringOffline() + public void BringOffline(bool waitTillStopped) { - starter.BringOffline(this); + starter.BringOffline(this, waitTillStopped); // Clear everything. Prevent accidental use. Nodes = Array.Empty(); Containers = null!; diff --git a/ProjectPlugins/CodexPlugin/CodexStarter.cs b/ProjectPlugins/CodexPlugin/CodexStarter.cs index af815a8d..57045cc2 100644 --- a/ProjectPlugins/CodexPlugin/CodexStarter.cs +++ b/ProjectPlugins/CodexPlugin/CodexStarter.cs @@ -48,14 +48,14 @@ namespace CodexPlugin return group; } - public void BringOffline(CodexNodeGroup group) + public void BringOffline(CodexNodeGroup group, bool waitTillStopped) { Log($"Stopping {group.Describe()}..."); StopCrashWatcher(group); var workflow = pluginTools.CreateWorkflow(); foreach (var c in group.Containers) { - workflow.Stop(c); + workflow.Stop(c, waitTillStopped); } Log("Stopped."); } diff --git a/Tests/CodexTests/BasicTests/OneClientTests.cs b/Tests/CodexTests/BasicTests/OneClientTests.cs index 26cf84e1..d8045862 100644 --- a/Tests/CodexTests/BasicTests/OneClientTests.cs +++ b/Tests/CodexTests/BasicTests/OneClientTests.cs @@ -21,7 +21,7 @@ namespace CodexTests.BasicTests { var primary = Ci.StartCodexNode(); - primary.Stop(); + primary.Stop(waitTillStopped: true); primary = Ci.StartCodexNode(); From 956cdbcfbeb2fe62e91da460c72d1edf3642e25f Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 13 Mar 2024 10:57:35 +0100 Subject: [PATCH 07/20] sets contracts image to latest --- .../CodexContractsPlugin/CodexContractsContainerRecipe.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProjectPlugins/CodexContractsPlugin/CodexContractsContainerRecipe.cs b/ProjectPlugins/CodexContractsPlugin/CodexContractsContainerRecipe.cs index 946d6652..e3814142 100644 --- a/ProjectPlugins/CodexContractsPlugin/CodexContractsContainerRecipe.cs +++ b/ProjectPlugins/CodexContractsPlugin/CodexContractsContainerRecipe.cs @@ -7,7 +7,7 @@ namespace CodexContractsPlugin { public class CodexContractsContainerRecipe : ContainerRecipeFactory { - public static string DockerImage { get; } = "codexstorage/codex-contracts-eth:sha-965529d-dist-tests"; + public static string DockerImage { get; } = "codexstorage/codex-contracts-eth:latest-dist-tests"; public const string MarketplaceAddressFilename = "/hardhat/deployments/codexdisttestnetwork/Marketplace.json"; public const string MarketplaceArtifactFilename = "/hardhat/artifacts/contracts/Marketplace.sol/Marketplace.json"; From 209eb31c5888d2c6a11ea3a623737487e9fea756 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 13 Mar 2024 12:09:25 +0100 Subject: [PATCH 08/20] fixes issue where kubernetes does not honor container entrypoints when command override is used --- ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs index b48f6058..c8d387a4 100644 --- a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs +++ b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs @@ -130,11 +130,11 @@ namespace CodexPlugin { if (ms.IsStorageNode) { - OverrideCommand("codex", "persistence", "prover"); + OverrideCommand("sh", "/docker-entrypoint.sh", "codex", "persistence", "prover"); } else { - OverrideCommand("codex", "persistence"); + OverrideCommand("sh", "/docker-entrypoint.sh", "codex", "persistence"); } } From db5c5444c51f7867632a01b0a091cda9ba464513 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 14 Mar 2024 14:29:28 +0100 Subject: [PATCH 09/20] Removes contract clock from logging --- ProjectPlugins/CodexPlugin/CodexStartupConfig.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs b/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs index 12109684..72d154b5 100644 --- a/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs +++ b/ProjectPlugins/CodexPlugin/CodexStartupConfig.cs @@ -68,10 +68,17 @@ 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.Libp2p.ToString()!.ToLowerInvariant()}:{string.Join(",", libp2pTopics)};" + + // Contract clock is always set to warn. It logs a trace every second. + $"{CodexLogLevel.Warn.ToString().ToLowerInvariant()}:{string.Join(",", contractClockTopics)}"; if (CustomTopics.BlockExchange != null) { From c204ab09d1b6402a4c58150d7ca5b0d8a6de126b Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 14 Mar 2024 14:30:00 +0100 Subject: [PATCH 10/20] combinatorial tests for finding issue --- Tests/CodexTests/BasicTests/TwoClientTests.cs | 188 ++++++++++++++++-- 1 file changed, 171 insertions(+), 17 deletions(-) diff --git a/Tests/CodexTests/BasicTests/TwoClientTests.cs b/Tests/CodexTests/BasicTests/TwoClientTests.cs index 458f1e1e..9f6cb9ed 100644 --- a/Tests/CodexTests/BasicTests/TwoClientTests.cs +++ b/Tests/CodexTests/BasicTests/TwoClientTests.cs @@ -1,4 +1,6 @@ -using CodexPlugin; +using CodexContractsPlugin; +using CodexPlugin; +using GethPlugin; using NUnit.Framework; using Utils; @@ -7,15 +9,169 @@ namespace CodexTests.BasicTests [TestFixture] public class TwoClientTests : CodexDistTest { + [Test] + [Combinatorial] + public void TwoClient( + [Values(0, 1, 2, 3)] int upmode, + [Values(0, 1, 2, 3)] int downmode) + { + var geth = Ci.StartGethNode(g => g.IsMiner()); + var contracts = Ci.StartCodexContracts(geth); + + var uploader = AddCodex(s => + { + s.WithName("Uploader"); + s.WithStorageQuota(10.GB()); + + if (upmode == 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens()); + if (upmode > 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens(), s => s.AsStorageNode()); + }); + + var downloader = AddCodex(s => + { + s.WithName("Downloader"); + s.WithStorageQuota(10.GB()); + s.WithBootstrapNode(uploader); + + if (downmode == 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens()); + if (downmode > 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens(), s => s.AsStorageNode()); + }); + + if (upmode == 3) + { + uploader.Marketplace.MakeStorageAvailable( + size: 2.GB(), + minPriceForTotalSpace: 1.TestTokens(), + maxCollateral: 20.TestTokens(), + maxDuration: TimeSpan.FromMinutes(3)); + } + if (downmode == 3) + { + downloader.Marketplace.MakeStorageAvailable( + size: 2.GB(), + minPriceForTotalSpace: 1.TestTokens(), + maxCollateral: 20.TestTokens(), + maxDuration: TimeSpan.FromMinutes(3)); + } + + PerformTwoClientTest(uploader, downloader); + } + + + [Test] + [Combinatorial] + public void ConnectivityOverGit( + [Values(0)] int upmode, + [Values(0, 1)] int downmode, + [Values(0, 1, 2, 3, 4, 5, 6)] int gitIndex) + { + var gits = new[] + { + "" + }; + + CodexContainerRecipe.DockerImageOverride = gits[gitIndex]; + + var geth = Ci.StartGethNode(g => g.IsMiner()); + var contracts = Ci.StartCodexContracts(geth); + + var uploader = AddCodex(s => + { + s.WithName("Uploader"); + s.WithStorageQuota(10.GB()); + + if (upmode == 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens()); + if (upmode > 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens(), s => s.AsStorageNode()); + }); + + var downloader = AddCodex(s => + { + s.WithName("Downloader"); + s.WithStorageQuota(10.GB()); + s.WithBootstrapNode(uploader); + + if (downmode == 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens()); + if (downmode > 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens(), s => s.AsStorageNode()); + }); + + if (upmode == 3) + { + uploader.Marketplace.MakeStorageAvailable( + size: 2.GB(), + minPriceForTotalSpace: 1.TestTokens(), + maxCollateral: 20.TestTokens(), + maxDuration: TimeSpan.FromMinutes(3)); + } + if (downmode == 3) + { + downloader.Marketplace.MakeStorageAvailable( + size: 2.GB(), + minPriceForTotalSpace: 1.TestTokens(), + maxCollateral: 20.TestTokens(), + maxDuration: TimeSpan.FromMinutes(3)); + } + + CreatePeerConnectionTestHelpers().AssertFullyConnected(new[] { uploader, downloader }); + } + + + + [Test] + [Combinatorial] + public void Connectivity( + [Values(0, 1, 2, 3)] int upmode, + [Values(0, 1, 2, 3)] int downmode) + { + var geth = Ci.StartGethNode(g => g.IsMiner()); + var contracts = Ci.StartCodexContracts(geth); + + var uploader = AddCodex(s => + { + s.WithName("Uploader"); + s.WithStorageQuota(10.GB()); + + if (upmode == 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens()); + if (upmode > 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens(), s => s.AsStorageNode()); + }); + + var downloader = AddCodex(s => + { + s.WithName("Downloader"); + s.WithStorageQuota(10.GB()); + s.WithBootstrapNode(uploader); + + if (downmode == 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens()); + if (downmode > 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens(), s => s.AsStorageNode()); + }); + + if (upmode == 3) + { + uploader.Marketplace.MakeStorageAvailable( + size: 2.GB(), + minPriceForTotalSpace: 1.TestTokens(), + maxCollateral: 20.TestTokens(), + maxDuration: TimeSpan.FromMinutes(3)); + } + if (downmode == 3) + { + downloader.Marketplace.MakeStorageAvailable( + size: 2.GB(), + minPriceForTotalSpace: 1.TestTokens(), + maxCollateral: 20.TestTokens(), + maxDuration: TimeSpan.FromMinutes(3)); + } + + CreatePeerConnectionTestHelpers().AssertFullyConnected(new[] { uploader, downloader }); + } + + [Test] public void TwoClientTest() { - var group = Ci.StartCodexNodes(2); + var uploader = AddCodex(s => s.WithName("Uploader")); + var downloader = AddCodex(s => s.WithName("Downloader").WithBootstrapNode(uploader)); - var primary = group[0]; - var secondary = group[1]; - - PerformTwoClientTest(primary, secondary); + PerformTwoClientTest(uploader, downloader); } [Test] @@ -28,29 +184,27 @@ namespace CodexTests.BasicTests return; } - var primary = Ci.StartCodexNode(s => s.At(locations.Get(0))); - var secondary = Ci.StartCodexNode(s => s.At(locations.Get(1))); + var uploader = Ci.StartCodexNode(s => s.At(locations.Get(0))); + var downloader = Ci.StartCodexNode(s => s.WithBootstrapNode(uploader).At(locations.Get(1))); - PerformTwoClientTest(primary, secondary); + PerformTwoClientTest(uploader, downloader); } - private void PerformTwoClientTest(ICodexNode primary, ICodexNode secondary) + private void PerformTwoClientTest(ICodexNode uploader, ICodexNode downloader) { - PerformTwoClientTest(primary, secondary, 1.MB()); + PerformTwoClientTest(uploader, downloader, 10.MB()); } - private void PerformTwoClientTest(ICodexNode primary, ICodexNode secondary, ByteSize size) + private void PerformTwoClientTest(ICodexNode uploader, ICodexNode downloader, ByteSize size) { - primary.ConnectToPeer(secondary); - var testFile = GenerateTestFile(size); - var contentId = primary.UploadFile(testFile); + var contentId = uploader.UploadFile(testFile); - var downloadedFile = secondary.DownloadContent(contentId); + var downloadedFile = downloader.DownloadContent(contentId); testFile.AssertIsEqual(downloadedFile); - CheckLogForErrors(primary, secondary); + CheckLogForErrors(uploader, downloader); } } } From 188f765b5c6664c83128d794c89a69f41bfd4459 Mon Sep 17 00:00:00 2001 From: benbierens Date: Fri, 15 Mar 2024 12:48:25 +0100 Subject: [PATCH 11/20] bash, not sh --- ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs index c8d387a4..3479f34c 100644 --- a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs +++ b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs @@ -130,11 +130,11 @@ namespace CodexPlugin { if (ms.IsStorageNode) { - OverrideCommand("sh", "/docker-entrypoint.sh", "codex", "persistence", "prover"); + OverrideCommand("bash", "/docker-entrypoint.sh", "codex", "persistence", "prover"); } else { - OverrideCommand("sh", "/docker-entrypoint.sh", "codex", "persistence"); + OverrideCommand("bash", "/docker-entrypoint.sh", "codex", "persistence"); } } From f757e64ba4587f4a5af16a727636b2c56ab19363 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 15 Mar 2024 13:50:39 +0100 Subject: [PATCH 12/20] wip --- .../CodexPlugin/CodexContainerRecipe.cs | 3 +- Tests/CodexTests/BasicTests/ExampleTests.cs | 6 +- Tests/CodexTests/BasicTests/TwoClientTests.cs | 164 +----------------- 3 files changed, 8 insertions(+), 165 deletions(-) diff --git a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs index 3479f34c..4681259f 100644 --- a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs +++ b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs @@ -9,7 +9,8 @@ namespace CodexPlugin { private readonly MarketplaceStarter marketplaceStarter = new MarketplaceStarter(); - private const string DefaultDockerImage = "codexstorage/nim-codex:latest-dist-tests"; + private const string DefaultDockerImage = "thatbenbierens/nim-codex:prover2"; + //"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"; diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index c70b5aa3..6e9ab5a3 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -74,7 +74,7 @@ namespace CodexTests.BasicTests size: 10.GB(), minPriceForTotalSpace: 1.TestTokens(), maxCollateral: 20.TestTokens(), - maxDuration: TimeSpan.FromMinutes(3)); + maxDuration: TimeSpan.FromMinutes(30)); var testFile = GenerateTestFile(fileSize); @@ -91,7 +91,7 @@ namespace CodexTests.BasicTests requiredCollateral: 10.TestTokens(), minRequiredNumberOfNodes: 1, proofProbability: 5, - duration: TimeSpan.FromMinutes(1)); + duration: TimeSpan.FromMinutes(5)); purchaseContract.WaitForStorageContractStarted(fileSize); @@ -108,7 +108,7 @@ namespace CodexTests.BasicTests 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); + CheckLogForErrors(seller, buyer); } [Test] diff --git a/Tests/CodexTests/BasicTests/TwoClientTests.cs b/Tests/CodexTests/BasicTests/TwoClientTests.cs index 9f6cb9ed..546fbda3 100644 --- a/Tests/CodexTests/BasicTests/TwoClientTests.cs +++ b/Tests/CodexTests/BasicTests/TwoClientTests.cs @@ -1,6 +1,4 @@ -using CodexContractsPlugin; -using CodexPlugin; -using GethPlugin; +using CodexPlugin; using NUnit.Framework; using Utils; @@ -9,162 +7,6 @@ namespace CodexTests.BasicTests [TestFixture] public class TwoClientTests : CodexDistTest { - [Test] - [Combinatorial] - public void TwoClient( - [Values(0, 1, 2, 3)] int upmode, - [Values(0, 1, 2, 3)] int downmode) - { - var geth = Ci.StartGethNode(g => g.IsMiner()); - var contracts = Ci.StartCodexContracts(geth); - - var uploader = AddCodex(s => - { - s.WithName("Uploader"); - s.WithStorageQuota(10.GB()); - - if (upmode == 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens()); - if (upmode > 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens(), s => s.AsStorageNode()); - }); - - var downloader = AddCodex(s => - { - s.WithName("Downloader"); - s.WithStorageQuota(10.GB()); - s.WithBootstrapNode(uploader); - - if (downmode == 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens()); - if (downmode > 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens(), s => s.AsStorageNode()); - }); - - if (upmode == 3) - { - uploader.Marketplace.MakeStorageAvailable( - size: 2.GB(), - minPriceForTotalSpace: 1.TestTokens(), - maxCollateral: 20.TestTokens(), - maxDuration: TimeSpan.FromMinutes(3)); - } - if (downmode == 3) - { - downloader.Marketplace.MakeStorageAvailable( - size: 2.GB(), - minPriceForTotalSpace: 1.TestTokens(), - maxCollateral: 20.TestTokens(), - maxDuration: TimeSpan.FromMinutes(3)); - } - - PerformTwoClientTest(uploader, downloader); - } - - - [Test] - [Combinatorial] - public void ConnectivityOverGit( - [Values(0)] int upmode, - [Values(0, 1)] int downmode, - [Values(0, 1, 2, 3, 4, 5, 6)] int gitIndex) - { - var gits = new[] - { - "" - }; - - CodexContainerRecipe.DockerImageOverride = gits[gitIndex]; - - var geth = Ci.StartGethNode(g => g.IsMiner()); - var contracts = Ci.StartCodexContracts(geth); - - var uploader = AddCodex(s => - { - s.WithName("Uploader"); - s.WithStorageQuota(10.GB()); - - if (upmode == 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens()); - if (upmode > 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens(), s => s.AsStorageNode()); - }); - - var downloader = AddCodex(s => - { - s.WithName("Downloader"); - s.WithStorageQuota(10.GB()); - s.WithBootstrapNode(uploader); - - if (downmode == 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens()); - if (downmode > 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens(), s => s.AsStorageNode()); - }); - - if (upmode == 3) - { - uploader.Marketplace.MakeStorageAvailable( - size: 2.GB(), - minPriceForTotalSpace: 1.TestTokens(), - maxCollateral: 20.TestTokens(), - maxDuration: TimeSpan.FromMinutes(3)); - } - if (downmode == 3) - { - downloader.Marketplace.MakeStorageAvailable( - size: 2.GB(), - minPriceForTotalSpace: 1.TestTokens(), - maxCollateral: 20.TestTokens(), - maxDuration: TimeSpan.FromMinutes(3)); - } - - CreatePeerConnectionTestHelpers().AssertFullyConnected(new[] { uploader, downloader }); - } - - - - [Test] - [Combinatorial] - public void Connectivity( - [Values(0, 1, 2, 3)] int upmode, - [Values(0, 1, 2, 3)] int downmode) - { - var geth = Ci.StartGethNode(g => g.IsMiner()); - var contracts = Ci.StartCodexContracts(geth); - - var uploader = AddCodex(s => - { - s.WithName("Uploader"); - s.WithStorageQuota(10.GB()); - - if (upmode == 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens()); - if (upmode > 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens(), s => s.AsStorageNode()); - }); - - var downloader = AddCodex(s => - { - s.WithName("Downloader"); - s.WithStorageQuota(10.GB()); - s.WithBootstrapNode(uploader); - - if (downmode == 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens()); - if (downmode > 1) s.EnableMarketplace(geth, contracts, 10.Eth(), 10.TestTokens(), s => s.AsStorageNode()); - }); - - if (upmode == 3) - { - uploader.Marketplace.MakeStorageAvailable( - size: 2.GB(), - minPriceForTotalSpace: 1.TestTokens(), - maxCollateral: 20.TestTokens(), - maxDuration: TimeSpan.FromMinutes(3)); - } - if (downmode == 3) - { - downloader.Marketplace.MakeStorageAvailable( - size: 2.GB(), - minPriceForTotalSpace: 1.TestTokens(), - maxCollateral: 20.TestTokens(), - maxDuration: TimeSpan.FromMinutes(3)); - } - - CreatePeerConnectionTestHelpers().AssertFullyConnected(new[] { uploader, downloader }); - } - - [Test] public void TwoClientTest() { @@ -184,8 +26,8 @@ namespace CodexTests.BasicTests return; } - var uploader = Ci.StartCodexNode(s => s.At(locations.Get(0))); - var downloader = Ci.StartCodexNode(s => s.WithBootstrapNode(uploader).At(locations.Get(1))); + 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))); PerformTwoClientTest(uploader, downloader); } From 32f56e82130b82c3aa936a26c6272e3ef422bb0a Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 15 Mar 2024 16:07:10 +0100 Subject: [PATCH 13/20] Marketplace test fails: submitted proof is invalid. --- ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs | 3 +-- ProjectPlugins/CodexPlugin/MarketplaceAccess.cs | 11 ----------- Tests/CodexTests/BasicTests/ExampleTests.cs | 8 ++++---- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs index 4681259f..4356556a 100644 --- a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs +++ b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs @@ -9,8 +9,7 @@ namespace CodexPlugin { private readonly MarketplaceStarter marketplaceStarter = new MarketplaceStarter(); - private const string DefaultDockerImage = "thatbenbierens/nim-codex:prover2"; - //"codexstorage/nim-codex:latest-dist-tests"; + private const string DefaultDockerImage = "codexstorage/nim-codex:sha-ba0e7d9-dist-tests"; public const string ApiPortTag = "codex_api_port"; public const string ListenPortTag = "codex_listen_port"; public const string MetricsPortTag = "codex_metrics_port"; diff --git a/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs b/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs index 462f4681..6077cee5 100644 --- a/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs +++ b/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs @@ -181,17 +181,6 @@ namespace CodexPlugin WaitForStorageContractState(timeout, "finished"); } - /// - /// Wait for contract to start. Max timeout depends on contract filesize. Allows more time for larger files. - /// - 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"); diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index 6e9ab5a3..8db821af 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -66,8 +66,7 @@ namespace CodexTests.BasicTests .WithStorageQuota(11.GB()) .EnableMarketplace(geth, contracts, initialEth: 10.Eth(), initialTokens: sellerInitialBalance, s => s .AsStorageNode() - .AsValidator()) - .WithSimulateProofFailures(failEveryNProofs: 3)); + .AsValidator())); AssertBalance(contracts, seller, Is.EqualTo(sellerInitialBalance)); seller.Marketplace.MakeStorageAvailable( @@ -91,9 +90,10 @@ namespace CodexTests.BasicTests requiredCollateral: 10.TestTokens(), minRequiredNumberOfNodes: 1, proofProbability: 5, - duration: TimeSpan.FromMinutes(5)); + duration: TimeSpan.FromMinutes(5), + expiry: TimeSpan.FromMinutes(4)); - purchaseContract.WaitForStorageContractStarted(fileSize); + purchaseContract.WaitForStorageContractStarted(TimeSpan.FromMinutes(4)); AssertBalance(contracts, seller, Is.LessThan(sellerInitialBalance), "Collateral was not placed."); From a6c2bf5230a634fc733437e53a4e211e7eff64a3 Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 20 Mar 2024 11:11:41 +0100 Subject: [PATCH 14/20] Cleanup marketplace-access methods --- .../CodexPlugin/MarketplaceAccess.cs | 90 ++++------------ .../CodexPlugin/MarketplaceTypes.cs | 101 ++++++++++++++++++ .../BasicTests/ContinuousSubstitute.cs | 13 ++- Tests/CodexTests/BasicTests/ExampleTests.cs | 39 ++++--- Tools/CodexNetDeployer/CodexNodeStarter.cs | 11 +- 5 files changed, 158 insertions(+), 96 deletions(-) create mode 100644 ProjectPlugins/CodexPlugin/MarketplaceTypes.cs diff --git a/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs b/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs index 6077cee5..f999bffe 100644 --- a/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs +++ b/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs @@ -2,15 +2,13 @@ using Logging; using Newtonsoft.Json; using Utils; -using System.Numerics; namespace CodexPlugin { public interface IMarketplaceAccess { - 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); + string MakeStorageAvailable(StorageAvailability availability); + StoragePurchaseContract RequestStorage(StoragePurchase purchase); } public class MarketplaceAccess : IMarketplaceAccess @@ -24,35 +22,12 @@ namespace CodexPlugin this.codexAccess = codexAccess; } - public StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration) + public StoragePurchaseContract RequestStorage(StoragePurchase purchase) { - return RequestStorage(contentId, pricePerSlotPerSecond, requiredCollateral, minRequiredNumberOfNodes, proofProbability, duration, duration / 2); - } + purchase.Log(log); + var request = purchase.ToApiRequest(); - public StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration, TimeSpan expiry) - { - var expireUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + expiry.TotalSeconds; - - var request = new CodexSalesRequestStorageRequest - { - duration = ToDecInt(duration.TotalSeconds), - proofProbability = ToDecInt(proofProbability), - reward = ToDecInt(pricePerSlotPerSecond), - collateral = ToDecInt(requiredCollateral), - expiry = ToDecInt(expireUtc), - nodes = minRequiredNumberOfNodes, - tolerance = null, - }; - - Log($"Requesting storage for: {contentId.Id}... (" + - $"pricePerSlotPerSecond: {pricePerSlotPerSecond}, " + - $"requiredCollateral: {requiredCollateral}, " + - $"minRequiredNumberOfNodes: {minRequiredNumberOfNodes}, " + - $"proofProbability: {proofProbability}, " + - $"expiry: {Time.FormatDuration(expiry)}, " + - $"duration: {Time.FormatDuration(duration)})"); - - var response = codexAccess.RequestStorage(request, contentId.Id); + var response = codexAccess.RequestStorage(request, purchase.ContentId.Id); if (response == "Purchasing not available" || response == "Expiry required" || @@ -64,24 +39,13 @@ namespace CodexPlugin Log($"Storage requested successfully. PurchaseId: '{response}'."); - return new StoragePurchaseContract(log, codexAccess, response, duration); + return new StoragePurchaseContract(log, codexAccess, response, purchase); } - public string MakeStorageAvailable(ByteSize totalSpace, TestToken minPriceForTotalSpace, TestToken maxCollateral, TimeSpan maxDuration) + public string MakeStorageAvailable(StorageAvailability availability) { - var request = new CodexSalesAvailabilityRequest - { - size = ToDecInt(totalSpace.SizeInBytes), - duration = ToDecInt(maxDuration.TotalSeconds), - maxCollateral = ToDecInt(maxCollateral), - minPrice = ToDecInt(minPriceForTotalSpace) - }; - - Log($"Making storage available... (" + - $"size: {totalSpace}, " + - $"minPriceForTotalSpace: {minPriceForTotalSpace}, " + - $"maxCollateral: {maxCollateral}, " + - $"maxDuration: {Time.FormatDuration(maxDuration)})"); + availability.Log(log); + var request = availability.ToApiRequest(); var response = codexAccess.SalesAvailability(request); @@ -90,18 +54,6 @@ namespace CodexPlugin return response.id; } - private string ToDecInt(double d) - { - var i = new BigInteger(d); - return i.ToString("D"); - } - - public string ToDecInt(TestToken t) - { - var i = new BigInteger(t.Amount); - return i.ToString("D"); - } - private void Log(string msg) { log.Log($"{codexAccess.Container.Name} {msg}"); @@ -110,22 +62,16 @@ namespace CodexPlugin public class MarketplaceUnavailable : IMarketplaceAccess { - 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) + public string MakeStorageAvailable(StorageAvailability availability) { Unavailable(); throw new NotImplementedException(); } - public string MakeStorageAvailable(ByteSize size, TestToken minPricePerBytePerSecond, TestToken maxCollateral, TimeSpan duration) + public StoragePurchaseContract RequestStorage(StoragePurchase purchase) { Unavailable(); - return string.Empty; + throw new NotImplementedException(); } private void Unavailable() @@ -141,16 +87,16 @@ namespace CodexPlugin private readonly CodexAccess codexAccess; private DateTime? contractStartUtc; - public StoragePurchaseContract(ILog log, CodexAccess codexAccess, string purchaseId, TimeSpan contractDuration) + public StoragePurchaseContract(ILog log, CodexAccess codexAccess, string purchaseId, StoragePurchase purchase) { this.log = log; this.codexAccess = codexAccess; PurchaseId = purchaseId; - ContractDuration = contractDuration; + Purchase = purchase; } public string PurchaseId { get; } - public TimeSpan ContractDuration { get; } + public StoragePurchase Purchase { get; } public void WaitForStorageContractStarted() { @@ -165,7 +111,7 @@ namespace CodexPlugin } var gracePeriod = TimeSpan.FromSeconds(10); var currentContractTime = DateTime.UtcNow - contractStartUtc!.Value; - var timeout = (ContractDuration - currentContractTime) + gracePeriod; + var timeout = (Purchase.Duration - currentContractTime) + gracePeriod; WaitForStorageContractState(timeout, "finished"); } @@ -177,7 +123,7 @@ namespace CodexPlugin } var gracePeriod = TimeSpan.FromSeconds(10); var currentContractTime = DateTime.UtcNow - contractStartUtc!.Value; - var timeout = (ContractDuration - currentContractTime) + gracePeriod; + var timeout = (Purchase.Duration - currentContractTime) + gracePeriod; WaitForStorageContractState(timeout, "finished"); } diff --git a/ProjectPlugins/CodexPlugin/MarketplaceTypes.cs b/ProjectPlugins/CodexPlugin/MarketplaceTypes.cs new file mode 100644 index 00000000..5987a4ee --- /dev/null +++ b/ProjectPlugins/CodexPlugin/MarketplaceTypes.cs @@ -0,0 +1,101 @@ +using CodexContractsPlugin; +using Logging; +using System.Numerics; +using Utils; + +namespace CodexPlugin +{ + public class StoragePurchase : MarketplaceType + { + public StoragePurchase(ContentId cid) + { + ContentId = cid; + } + + public ContentId ContentId { get; set; } + public TestToken PricePerSlotPerSecond { get; set; } = 1.TestTokens(); + public TestToken RequiredCollateral { get; set; } = 1.TestTokens(); + public uint MinRequiredNumberOfNodes { get; set; } + public uint NodeFailureTolerance { get; set; } + public int ProofProbability { get; set; } + public TimeSpan Duration { get; set; } + public TimeSpan Expiry { get; set; } + + public CodexSalesRequestStorageRequest ToApiRequest() + { + return new CodexSalesRequestStorageRequest + { + duration = ToDecInt(Duration.TotalSeconds), + proofProbability = ToDecInt(ProofProbability), + reward = ToDecInt(PricePerSlotPerSecond), + collateral = ToDecInt(RequiredCollateral), + expiry = ToDecInt(DateTimeOffset.UtcNow.ToUnixTimeSeconds() + Expiry.TotalSeconds), + nodes = MinRequiredNumberOfNodes, + tolerance = NodeFailureTolerance + }; + } + + public void Log(ILog log) + { + log.Log($"Requesting storage for: {ContentId.Id}... (" + + $"pricePerSlotPerSecond: {PricePerSlotPerSecond}, " + + $"requiredCollateral: {RequiredCollateral}, " + + $"minRequiredNumberOfNodes: {MinRequiredNumberOfNodes}, " + + $"nodeFailureTolerance: {NodeFailureTolerance}, " + + $"proofProbability: {ProofProbability}, " + + $"expiry: {Time.FormatDuration(Expiry)}, " + + $"duration: {Time.FormatDuration(Duration)})"); + } + } + + public class StorageAvailability : MarketplaceType + { + public StorageAvailability(ByteSize totalSpace, TimeSpan maxDuration, TestToken minPriceForTotalSpace, TestToken maxCollateral) + { + TotalSpace = totalSpace; + MaxDuration = maxDuration; + MinPriceForTotalSpace = minPriceForTotalSpace; + MaxCollateral = maxCollateral; + } + + public ByteSize TotalSpace { get; } + public TimeSpan MaxDuration { get; } + public TestToken MinPriceForTotalSpace { get; } + public TestToken MaxCollateral { get; } + + public CodexSalesAvailabilityRequest ToApiRequest() + { + return new CodexSalesAvailabilityRequest + { + size = ToDecInt(TotalSpace.SizeInBytes), + duration = ToDecInt(MaxDuration.TotalSeconds), + maxCollateral = ToDecInt(MaxCollateral), + minPrice = ToDecInt(MinPriceForTotalSpace) + }; + } + + public void Log(ILog log) + { + log.Log($"Making storage available... (" + + $"size: {TotalSpace}, " + + $"maxDuration: {Time.FormatDuration(MaxDuration)})" + + $"minPriceForTotalSpace: {MinPriceForTotalSpace}, " + + $"maxCollateral: {MaxCollateral}, "); + } + } + + public abstract class MarketplaceType + { + protected string ToDecInt(double d) + { + var i = new BigInteger(d); + return i.ToString("D"); + } + + protected string ToDecInt(TestToken t) + { + var i = new BigInteger(t.Amount); + return i.ToString("D"); + } + } +} diff --git a/Tests/CodexTests/BasicTests/ContinuousSubstitute.cs b/Tests/CodexTests/BasicTests/ContinuousSubstitute.cs index ad22bac3..5f4c3002 100644 --- a/Tests/CodexTests/BasicTests/ContinuousSubstitute.cs +++ b/Tests/CodexTests/BasicTests/ContinuousSubstitute.cs @@ -33,13 +33,16 @@ 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( - size: 500.MB(), - minPriceForTotalSpace: 500.TestTokens(), - maxCollateral: 1024.TestTokens(), - maxDuration: TimeSpan.FromMinutes(5)); + node.Marketplace.MakeStorageAvailable(availability); } var endTime = DateTime.UtcNow + TimeSpan.FromHours(10); diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index 8db821af..6b46917d 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -54,7 +54,7 @@ namespace CodexTests.BasicTests public void MarketplaceExample() { var sellerInitialBalance = 234.TestTokens(); - var buyerInitialBalance = 1000.TestTokens(); + var buyerInitialBalance = 100000.TestTokens(); var fileSize = 10.MB(); var geth = Ci.StartGethNode(s => s.IsMiner().WithName("disttest-geth")); @@ -69,11 +69,14 @@ namespace CodexTests.BasicTests .AsValidator())); AssertBalance(contracts, seller, Is.EqualTo(sellerInitialBalance)); - seller.Marketplace.MakeStorageAvailable( - size: 10.GB(), + + var availability = new StorageAvailability( + totalSpace: 10.GB(), + maxDuration: TimeSpan.FromMinutes(30), minPriceForTotalSpace: 1.TestTokens(), - maxCollateral: 20.TestTokens(), - maxDuration: TimeSpan.FromMinutes(30)); + maxCollateral: 20.TestTokens() + ); + seller.Marketplace.MakeStorageAvailable(availability); var testFile = GenerateTestFile(fileSize); @@ -85,20 +88,26 @@ namespace CodexTests.BasicTests AssertBalance(contracts, buyer, Is.EqualTo(buyerInitialBalance)); var contentId = buyer.UploadFile(testFile); - var purchaseContract = buyer.Marketplace.RequestStorage(contentId, - pricePerSlotPerSecond: 2.TestTokens(), - requiredCollateral: 10.TestTokens(), - minRequiredNumberOfNodes: 1, - proofProbability: 5, - duration: TimeSpan.FromMinutes(5), - expiry: TimeSpan.FromMinutes(4)); + + var purchase = new StoragePurchase(contentId) + { + PricePerSlotPerSecond = 2.TestTokens(), + RequiredCollateral = 10.TestTokens(), + MinRequiredNumberOfNodes = 5, + NodeFailureTolerance = 2, + ProofProbability = 5, + Duration = TimeSpan.FromMinutes(5), + Expiry = TimeSpan.FromMinutes(4) + }; + + var purchaseContract = buyer.Marketplace.RequestStorage(purchase); purchaseContract.WaitForStorageContractStarted(TimeSpan.FromMinutes(4)); AssertBalance(contracts, seller, Is.LessThan(sellerInitialBalance), "Collateral was not placed."); var request = GetOnChainStorageRequest(contracts); - AssertStorageRequest(request, contracts, buyer); + AssertStorageRequest(request, purchase, contracts, buyer); AssertSlotFilledEvents(contracts, request, seller); AssertContractSlot(contracts, request, 0, seller); @@ -141,11 +150,11 @@ namespace CodexTests.BasicTests Assert.That(filledSlotEvent.Host, Is.EqualTo(seller.EthAddress)); } - private void AssertStorageRequest(Request request, ICodexContracts contracts, ICodexNode buyer) + private void AssertStorageRequest(Request request, StoragePurchase 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(1)); + Assert.That(request.Ask.Slots, Is.EqualTo(purchase.MinRequiredNumberOfNodes)); } private Request GetOnChainStorageRequest(ICodexContracts contracts) diff --git a/Tools/CodexNetDeployer/CodexNodeStarter.cs b/Tools/CodexNetDeployer/CodexNodeStarter.cs index 71e7a6a6..663659b4 100644 --- a/Tools/CodexNetDeployer/CodexNodeStarter.cs +++ b/Tools/CodexNetDeployer/CodexNodeStarter.cs @@ -67,11 +67,14 @@ namespace CodexNetDeployer if (config.ShouldMakeStorageAvailable) { - var response = codexNode.Marketplace.MakeStorageAvailable( - size: config.StorageSell!.Value.MB(), + var availability = new StorageAvailability( + totalSpace: config.StorageSell!.Value.MB(), + maxDuration: TimeSpan.FromSeconds(config.MaxDuration), minPriceForTotalSpace: config.MinPrice.TestTokens(), - maxCollateral: config.MaxCollateral.TestTokens(), - maxDuration: TimeSpan.FromSeconds(config.MaxDuration)); + maxCollateral: config.MaxCollateral.TestTokens() + ); + + var response = codexNode.Marketplace.MakeStorageAvailable(availability); if (!string.IsNullOrEmpty(response)) { From ef53fe02a3af18836ca554dee3684304a658804f Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 20 Mar 2024 11:11:59 +0100 Subject: [PATCH 15/20] Improve logfile checking --- Framework/Core/DownloadedLog.cs | 13 +++++++----- Tests/CodexTests/BasicTests/ExampleTests.cs | 20 ++++++++++++------- Tests/CodexTests/CodexDistTest.cs | 1 + Tests/DistTestCore/DownloadedLogExtensions.cs | 7 ++++--- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/Framework/Core/DownloadedLog.cs b/Framework/Core/DownloadedLog.cs index 9242d9ba..923bcbc8 100644 --- a/Framework/Core/DownloadedLog.cs +++ b/Framework/Core/DownloadedLog.cs @@ -4,7 +4,7 @@ namespace Core { public interface IDownloadedLog { - bool DoesLogContain(string expectedString); + string[] GetLinesContaining(string expectedString); string[] FindLinesThatContain(params string[] tags); void DeleteFile(); } @@ -18,20 +18,23 @@ namespace Core this.logFile = logFile; } - public bool DoesLogContain(string expectedString) + public string[] GetLinesContaining(string expectedString) { using var file = File.OpenRead(logFile.FullFilename); using var streamReader = new StreamReader(file); + var lines = new List(); var line = streamReader.ReadLine(); while (line != null) { - if (line.Contains(expectedString)) return true; + if (line.Contains(expectedString)) + { + lines.Add(line); + } line = streamReader.ReadLine(); } - //Assert.Fail($"{owner} Unable to find string '{expectedString}' in CodexNode log file {logFile.FullFilename}"); - return false; + return lines.ToArray(); ; } public string[] FindLinesThatContain(params string[] tags) diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index 6b46917d..c910b3d2 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -108,7 +108,7 @@ namespace CodexTests.BasicTests var request = GetOnChainStorageRequest(contracts); AssertStorageRequest(request, purchase, contracts, buyer); - AssertSlotFilledEvents(contracts, request, seller); + AssertSlotFilledEvents(contracts, purchase, request, seller); AssertContractSlot(contracts, request, 0, seller); purchaseContract.WaitForStorageContractFinished(); @@ -137,17 +137,23 @@ namespace CodexTests.BasicTests Assert.That(discN, Is.LessThan(bootN)); } - private void AssertSlotFilledEvents(ICodexContracts contracts, Request request, ICodexNode seller) + private void AssertSlotFilledEvents(ICodexContracts contracts, StoragePurchase purchase, Request request, ICodexNode seller) { + // Expect 1 fulfilled event for the purchase. var requestFulfilledEvents = contracts.GetRequestFulfilledEvents(GetTestRunTimeRange()); Assert.That(requestFulfilledEvents.Length, Is.EqualTo(1)); CollectionAssert.AreEqual(request.RequestId, requestFulfilledEvents[0].RequestId); + + // Expect 1 filled-slot event for each slot in the purchase. var filledSlotEvents = contracts.GetSlotFilledEvents(GetTestRunTimeRange()); - Assert.That(filledSlotEvents.Length, Is.EqualTo(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)); + Assert.That(filledSlotEvents.Length, Is.EqualTo(purchase.MinRequiredNumberOfNodes)); + for (var i = 0; i < purchase.MinRequiredNumberOfNodes; i++) + { + var filledSlotEvent = filledSlotEvents[i]; + Assert.That(filledSlotEvent.SlotIndex, Is.EqualTo(i)); + Assert.That(filledSlotEvent.RequestId.ToHex(), Is.EqualTo(request.RequestId.ToHex())); + Assert.That(filledSlotEvent.Host, Is.EqualTo(seller.EthAddress)); + } } private void AssertStorageRequest(Request request, StoragePurchase purchase, ICodexContracts contracts, ICodexNode buyer) diff --git a/Tests/CodexTests/CodexDistTest.cs b/Tests/CodexTests/CodexDistTest.cs index 2711a838..0578c27a 100644 --- a/Tests/CodexTests/CodexDistTest.cs +++ b/Tests/CodexTests/CodexDistTest.cs @@ -92,6 +92,7 @@ namespace CodexTests public void CheckLogForErrors(ICodexNode node) { + Log($"Checking {node.GetName()} log for errors."); var log = Ci.DownloadLog(node); log.AssertLogDoesNotContain("Block validation failed"); diff --git a/Tests/DistTestCore/DownloadedLogExtensions.cs b/Tests/DistTestCore/DownloadedLogExtensions.cs index b000439f..51ee96f5 100644 --- a/Tests/DistTestCore/DownloadedLogExtensions.cs +++ b/Tests/DistTestCore/DownloadedLogExtensions.cs @@ -7,7 +7,7 @@ namespace DistTestCore { public static void AssertLogContains(this IDownloadedLog log, string expectedString) { - Assert.That(log.DoesLogContain(expectedString), $"Did not find '{expectedString}' in log."); + Assert.That(log.GetLinesContaining(expectedString).Any(), $"Did not find '{expectedString}' in log."); } public static void AssertLogDoesNotContain(this IDownloadedLog log, params string[] unexpectedStrings) @@ -15,9 +15,10 @@ namespace DistTestCore var errors = new List(); foreach (var str in unexpectedStrings) { - if (log.DoesLogContain(str)) + var lines = log.GetLinesContaining(str); + foreach (var line in lines) { - errors.Add($"Did find '{str}' in log."); + errors.Add($"Found '{str}' in line '{line}'."); } } CollectionAssert.IsEmpty(errors); From 2a5b3e0eec029da65513ed7a8c1e75e518ebd8ed Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 20 Mar 2024 11:32:16 +0100 Subject: [PATCH 16/20] fixes marketplace test --- ProjectPlugins/CodexPlugin/MarketplaceAccess.cs | 3 +-- ProjectPlugins/CodexPlugin/MarketplaceTypes.cs | 4 ++-- Tests/CodexTests/BasicTests/ExampleTests.cs | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs b/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs index f999bffe..82b4e3ac 100644 --- a/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs +++ b/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs @@ -1,5 +1,4 @@ -using CodexContractsPlugin; -using Logging; +using Logging; using Newtonsoft.Json; using Utils; diff --git a/ProjectPlugins/CodexPlugin/MarketplaceTypes.cs b/ProjectPlugins/CodexPlugin/MarketplaceTypes.cs index 5987a4ee..bfdf1ff8 100644 --- a/ProjectPlugins/CodexPlugin/MarketplaceTypes.cs +++ b/ProjectPlugins/CodexPlugin/MarketplaceTypes.cs @@ -78,9 +78,9 @@ namespace CodexPlugin { log.Log($"Making storage available... (" + $"size: {TotalSpace}, " + - $"maxDuration: {Time.FormatDuration(MaxDuration)})" + + $"maxDuration: {Time.FormatDuration(MaxDuration)}, " + $"minPriceForTotalSpace: {MinPriceForTotalSpace}, " + - $"maxCollateral: {MaxCollateral}, "); + $"maxCollateral: {MaxCollateral})"); } } diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index c910b3d2..473bf984 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -149,8 +149,7 @@ namespace CodexTests.BasicTests Assert.That(filledSlotEvents.Length, Is.EqualTo(purchase.MinRequiredNumberOfNodes)); for (var i = 0; i < purchase.MinRequiredNumberOfNodes; i++) { - var filledSlotEvent = filledSlotEvents[i]; - Assert.That(filledSlotEvent.SlotIndex, Is.EqualTo(i)); + var filledSlotEvent = filledSlotEvents.Single(e => e.SlotIndex == i); Assert.That(filledSlotEvent.RequestId.ToHex(), Is.EqualTo(request.RequestId.ToHex())); Assert.That(filledSlotEvent.Host, Is.EqualTo(seller.EthAddress)); } From db37143db631fcd6211ca94c887e43ec146a83bd Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 20 Mar 2024 13:36:06 +0100 Subject: [PATCH 17/20] cleanup --- .../CodexPlugin/CodexContainerRecipe.cs | 2 +- .../CodexPlugin/MarketplaceAccess.cs | 29 +++++-------------- Tests/CodexTests/BasicTests/ExampleTests.cs | 4 +-- 3 files changed, 9 insertions(+), 26 deletions(-) diff --git a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs index 4356556a..ca619b29 100644 --- a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs +++ b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs @@ -9,7 +9,7 @@ namespace CodexPlugin { private readonly MarketplaceStarter marketplaceStarter = new MarketplaceStarter(); - private const string DefaultDockerImage = "codexstorage/nim-codex:sha-ba0e7d9-dist-tests"; + private const string DefaultDockerImage = "codexstorage/nim-codex:sha-e4ddb94-dist-tests"; public const string ApiPortTag = "codex_api_port"; public const string ListenPortTag = "codex_listen_port"; public const string MetricsPortTag = "codex_metrics_port"; diff --git a/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs b/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs index 82b4e3ac..411b4814 100644 --- a/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs +++ b/ProjectPlugins/CodexPlugin/MarketplaceAccess.cs @@ -84,6 +84,7 @@ namespace CodexPlugin { private readonly ILog log; private readonly CodexAccess codexAccess; + private readonly TimeSpan gracePeriod = TimeSpan.FromSeconds(10); private DateTime? contractStartUtc; public StoragePurchaseContract(ILog log, CodexAccess codexAccess, string purchaseId, StoragePurchase purchase) @@ -99,7 +100,10 @@ namespace CodexPlugin public void WaitForStorageContractStarted() { - WaitForStorageContractStarted(TimeSpan.FromSeconds(30)); + var timeout = Purchase.Expiry + gracePeriod; + + WaitForStorageContractState(timeout, "started"); + contractStartUtc = DateTime.UtcNow; } public void WaitForStorageContractFinished() @@ -108,28 +112,14 @@ namespace CodexPlugin { WaitForStorageContractStarted(); } - var gracePeriod = TimeSpan.FromSeconds(10); var currentContractTime = DateTime.UtcNow - contractStartUtc!.Value; var timeout = (Purchase.Duration - currentContractTime) + gracePeriod; WaitForStorageContractState(timeout, "finished"); } - public void WaitForStorageContractFinished(ByteSize contractFileSize) + public CodexStoragePurchase GetPurchaseStatus(string purchaseId) { - if (!contractStartUtc.HasValue) - { - WaitForStorageContractStarted(contractFileSize.ToTimeSpan()); - } - var gracePeriod = TimeSpan.FromSeconds(10); - var currentContractTime = DateTime.UtcNow - contractStartUtc!.Value; - var timeout = (Purchase.Duration - currentContractTime) + gracePeriod; - WaitForStorageContractState(timeout, "finished"); - } - - public void WaitForStorageContractStarted(TimeSpan timeout) - { - WaitForStorageContractState(timeout, "started"); - contractStartUtc = DateTime.UtcNow; + return codexAccess.GetPurchaseStatus(purchaseId); } private void WaitForStorageContractState(TimeSpan timeout, string desiredState) @@ -162,10 +152,5 @@ namespace CodexPlugin } log.Log($"Contract '{desiredState}'."); } - - public CodexStoragePurchase GetPurchaseStatus(string purchaseId) - { - return codexAccess.GetPurchaseStatus(purchaseId); - } } } diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index 473bf984..4973e2a0 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -102,7 +102,7 @@ namespace CodexTests.BasicTests var purchaseContract = buyer.Marketplace.RequestStorage(purchase); - purchaseContract.WaitForStorageContractStarted(TimeSpan.FromMinutes(4)); + purchaseContract.WaitForStorageContractStarted(); AssertBalance(contracts, seller, Is.LessThan(sellerInitialBalance), "Collateral was not placed."); @@ -116,8 +116,6 @@ namespace CodexTests.BasicTests AssertBalance(contracts, seller, Is.GreaterThan(sellerInitialBalance), "Seller was not paid for storage."); AssertBalance(contracts, buyer, Is.LessThan(buyerInitialBalance), "Buyer was not charged for storage."); Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Finished)); - - CheckLogForErrors(seller, buyer); } [Test] From 4fdb310ca4763edc61671e6cf14d4d635217edf9 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 21 Mar 2024 12:18:04 +0100 Subject: [PATCH 18/20] Sets up tests for blocktimefinder. --- Framework/NethereumWorkflow/BlockTimeEntry.cs | 23 ++- .../NethereumWorkflow/BlockTimeFinder.cs | 36 ++--- .../NethereumWorkflow/NethereumInteraction.cs | 4 +- Framework/NethereumWorkflow/Web3Wrapper.cs | 46 ++++++ Tests/FrameworkTests/FrameworkTests.csproj | 2 + .../NethereumWorkflow/BlockTimeFinderTests.cs | 137 ++++++++++++++++++ 6 files changed, 208 insertions(+), 40 deletions(-) create mode 100644 Framework/NethereumWorkflow/Web3Wrapper.cs create mode 100644 Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs diff --git a/Framework/NethereumWorkflow/BlockTimeEntry.cs b/Framework/NethereumWorkflow/BlockTimeEntry.cs index 03817ae4..921f5b59 100644 --- a/Framework/NethereumWorkflow/BlockTimeEntry.cs +++ b/Framework/NethereumWorkflow/BlockTimeEntry.cs @@ -1,22 +1,19 @@ namespace NethereumWorkflow { - public partial class BlockTimeFinder + public class BlockTimeEntry { - public class BlockTimeEntry + public BlockTimeEntry(ulong blockNumber, DateTime utc) { - public BlockTimeEntry(ulong blockNumber, DateTime utc) - { - BlockNumber = blockNumber; - Utc = utc; - } + BlockNumber = blockNumber; + Utc = utc; + } - public ulong BlockNumber { get; } - public DateTime Utc { get; } + public ulong BlockNumber { get; } + public DateTime Utc { get; } - public override string ToString() - { - return $"[{BlockNumber}] @ {Utc.ToString("o")}"; - } + public override string ToString() + { + return $"[{BlockNumber}] @ {Utc.ToString("o")}"; } } } diff --git a/Framework/NethereumWorkflow/BlockTimeFinder.cs b/Framework/NethereumWorkflow/BlockTimeFinder.cs index e7b8c4a6..34931e82 100644 --- a/Framework/NethereumWorkflow/BlockTimeFinder.cs +++ b/Framework/NethereumWorkflow/BlockTimeFinder.cs @@ -1,36 +1,33 @@ using Logging; -using Nethereum.RPC.Eth.DTOs; -using Nethereum.Web3; -using Utils; namespace NethereumWorkflow { - public partial class BlockTimeFinder + public class BlockTimeFinder { private const ulong FetchRange = 6; private const int MaxEntries = 1024; private static readonly Dictionary entries = new Dictionary(); - private readonly Web3 web3; + private readonly IWeb3Blocks web3; private readonly ILog log; - public BlockTimeFinder(Web3 web3, ILog log) + public BlockTimeFinder(IWeb3Blocks web3, ILog log) { this.web3 = web3; this.log = log; } - public ulong GetHighestBlockNumberBefore(DateTime moment) + public ulong? GetHighestBlockNumberBefore(DateTime moment) { - log.Log("Looking for highest block before " + moment.ToString("o")); + log.Debug("Looking for highest block before " + moment.ToString("o")); AssertMomentIsInPast(moment); Initialize(); return GetHighestBlockBefore(moment); } - public ulong GetLowestBlockNumberAfter(DateTime moment) + public ulong? GetLowestBlockNumberAfter(DateTime moment) { - log.Log("Looking for lowest block after " + moment.ToString("o")); + log.Debug("Looking for lowest block after " + moment.ToString("o")); AssertMomentIsInPast(moment); Initialize(); @@ -48,7 +45,7 @@ namespace NethereumWorkflow closestAfter.Utc > moment && closestBefore.BlockNumber + 1 == closestAfter.BlockNumber) { - log.Log("Found highest-Before: " + closestBefore); + log.Debug("Found highest-Before: " + closestBefore); return closestBefore.BlockNumber; } @@ -67,7 +64,7 @@ namespace NethereumWorkflow closestAfter.Utc > moment && closestBefore.BlockNumber + 1 == closestAfter.BlockNumber) { - log.Log("Found lowest-after: " + closestAfter); + log.Debug("Found lowest-after: " + closestAfter); return closestAfter.BlockNumber; } @@ -223,24 +220,13 @@ namespace NethereumWorkflow private BlockTimeEntry? AddCurrentBlock() { - var number = Time.Wait(web3.Eth.Blocks.GetBlockNumber.SendRequestAsync()); - var blockNumber = number.ToDecimal(); + var blockNumber = web3.GetCurrentBlockNumber(); 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; - } + return web3.GetTimestampForBlock(blockNumber); } private BlockTimeEntry? FindClosestBeforeEntry(DateTime moment) diff --git a/Framework/NethereumWorkflow/NethereumInteraction.cs b/Framework/NethereumWorkflow/NethereumInteraction.cs index 0a8c2e42..2f588740 100644 --- a/Framework/NethereumWorkflow/NethereumInteraction.cs +++ b/Framework/NethereumWorkflow/NethereumInteraction.cs @@ -3,7 +3,6 @@ using Nethereum.ABI.FunctionEncoding.Attributes; using Nethereum.Contracts; using Nethereum.RPC.Eth.DTOs; using Nethereum.Web3; -using System.Runtime.CompilerServices; using Utils; namespace NethereumWorkflow @@ -88,7 +87,8 @@ namespace NethereumWorkflow public List> GetEvents(string address, TimeRange timeRange) where TEvent : IEventDTO, new() { - var blockTimeFinder = new BlockTimeFinder(web3, log); + var wrapper = new Web3Wrapper(web3, log); + var blockTimeFinder = new BlockTimeFinder(wrapper, log); var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(timeRange.From); var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(timeRange.To); diff --git a/Framework/NethereumWorkflow/Web3Wrapper.cs b/Framework/NethereumWorkflow/Web3Wrapper.cs new file mode 100644 index 00000000..67c8ca91 --- /dev/null +++ b/Framework/NethereumWorkflow/Web3Wrapper.cs @@ -0,0 +1,46 @@ +using Logging; +using Nethereum.RPC.Eth.DTOs; +using Nethereum.Web3; +using Utils; + +namespace NethereumWorkflow +{ + public interface IWeb3Blocks + { + decimal GetCurrentBlockNumber(); + DateTime? GetTimestampForBlock(decimal 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 decimal GetCurrentBlockNumber() + { + var number = Time.Wait(web3.Eth.Blocks.GetBlockNumber.SendRequestAsync()); + return number.ToDecimal(); + } + + public DateTime? GetTimestampForBlock(decimal blockNumber) + { + try + { + var block = Time.Wait(web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(new BlockParameter(Convert.ToUInt64(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; + } + } + } +} diff --git a/Tests/FrameworkTests/FrameworkTests.csproj b/Tests/FrameworkTests/FrameworkTests.csproj index fbad2770..f22fe575 100644 --- a/Tests/FrameworkTests/FrameworkTests.csproj +++ b/Tests/FrameworkTests/FrameworkTests.csproj @@ -7,12 +7,14 @@ + + diff --git a/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs b/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs new file mode 100644 index 00000000..a9506d7f --- /dev/null +++ b/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs @@ -0,0 +1,137 @@ +using Logging; +using Moq; +using NethereumWorkflow; +using NUnit.Framework; + +namespace FrameworkTests.NethereumWorkflow +{ + [TestFixture] + public class BlockTimeFinderTests + { + private Mock web3 = new Mock(); + private Mock log = new Mock(); + private Dictionary blocks = new Dictionary(); + private DateTime start = DateTime.Now; + + private BlockTimeFinder finder = null!; + + private void SetupContinuousBlockchain() + { + start = DateTime.UtcNow.AddDays(-1).AddSeconds(-30); + blocks = new Dictionary(); + + for (var i = 0; i < 30; i++) + { + decimal d = 100 + i; + blocks.Add(d, new Block(d, start + TimeSpan.FromSeconds(i * 2))); + } + } + + [SetUp] + public void SetUp() + { + SetupContinuousBlockchain(); + + web3 = new Mock(); + web3.Setup(w => w.GetCurrentBlockNumber()).Returns(blocks.Keys.Max()); + web3.Setup(w => w.GetTimestampForBlock(It.IsAny())).Returns(d => + { + if (blocks.ContainsKey(d)) return blocks[d].Time; + return null; + }); + + finder = new BlockTimeFinder(web3.Object, log.Object); + } + + [Test] + public void FindsMiddleOfChain() + { + var b1 = blocks[115]; + var b2 = blocks[116]; + + var momentBetween = b1.JustAfter; + + var b1Number = finder.GetHighestBlockNumberBefore(momentBetween); + var b2Number = finder.GetLowestBlockNumberAfter(momentBetween); + + Assert.That(b1Number, Is.EqualTo(b1.Number)); + Assert.That(b2Number, Is.EqualTo(b2.Number)); + } + + [Test] + public void FindsFrontOfChain_Lowest() + { + var first = blocks.First().Value; + + var firstNumber = finder.GetLowestBlockNumberAfter(first.JustBefore); + + Assert.That(firstNumber, Is.EqualTo(first.Number)); + } + + [Test] + public void FindsFrontOfChain_Highest() + { + var first = blocks.First().Value; + + var firstNumber = finder.GetHighestBlockNumberBefore(first.JustAfter); + + Assert.That(firstNumber, Is.EqualTo(first.Number)); + } + + [Test] + public void FindsTailOfChain_Lowest() + { + var last = blocks.Last().Value; + + var lastNumber = finder.GetLowestBlockNumberAfter(last.JustBefore); + + Assert.That(lastNumber, Is.EqualTo(last.Number)); + } + + [Test] + public void FindsTailOfChain_Highest() + { + var last = blocks.Last().Value; + + var lastNumber = finder.GetHighestBlockNumberBefore(last.JustAfter); + + Assert.That(lastNumber, Is.EqualTo(last.Number)); + } + + [Test] + public void FailsToFindBlockBeforeFrontOfChain() + { + var first = blocks.First().Value; + + var notFound = finder.GetHighestBlockNumberBefore(first.Time); + + Assert.That(notFound, Is.Null); + } + + [Test] + public void FailsToFindBlockAfterTailOfChain() + { + var last = blocks.Last().Value; + + var notFound = finder.GetLowestBlockNumberAfter(last.Time); + + Assert.That(notFound, Is.Null); + } + + } + + public class Block + { + public Block(decimal number, DateTime time) + { + Number = number; + Time = time; + } + + public decimal Number { get; } + public DateTime Time { get; } + public DateTime JustBefore { get { return Time.AddSeconds(-1); } } + public DateTime JustAfter { get { return Time.AddSeconds(1); } } + } + +} From 6d4d6fcdb9f8b9663f1923c27f9142984d21c8a8 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 21 Mar 2024 16:11:28 +0100 Subject: [PATCH 19/20] working blocktime finder --- .../NethereumWorkflow/BlockTimeFinder.cs | 379 ++++++++---------- Framework/NethereumWorkflow/Web3Wrapper.cs | 12 +- .../NethereumWorkflow/BlockTimeFinderTests.cs | 25 +- 3 files changed, 182 insertions(+), 234 deletions(-) diff --git a/Framework/NethereumWorkflow/BlockTimeFinder.cs b/Framework/NethereumWorkflow/BlockTimeFinder.cs index 34931e82..829870b4 100644 --- a/Framework/NethereumWorkflow/BlockTimeFinder.cs +++ b/Framework/NethereumWorkflow/BlockTimeFinder.cs @@ -4,9 +4,7 @@ namespace NethereumWorkflow { public class BlockTimeFinder { - private const ulong FetchRange = 6; - private const int MaxEntries = 1024; - private static readonly Dictionary entries = new Dictionary(); + private readonly BlockCache cache; private readonly IWeb3Blocks web3; private readonly ILog log; @@ -14,253 +12,206 @@ namespace NethereumWorkflow { this.web3 = web3; this.log = log; + + cache = new BlockCache(web3); } public ulong? GetHighestBlockNumberBefore(DateTime moment) { - log.Debug("Looking for highest block before " + moment.ToString("o")); - AssertMomentIsInPast(moment); - Initialize(); + cache.Initialize(); + if (moment <= cache.Genesis.Utc) return null; + if (moment >= cache.Current.Utc) return cache.Current.BlockNumber; - return GetHighestBlockBefore(moment); + return Search(cache.Genesis, cache.Current, moment, HighestBeforeSelector); } public ulong? GetLowestBlockNumberAfter(DateTime moment) { - log.Debug("Looking for lowest block after " + moment.ToString("o")); - AssertMomentIsInPast(moment); - Initialize(); + cache.Initialize(); + if (moment >= cache.Current.Utc) return null; + if (moment <= cache.Genesis.Utc) return cache.Genesis.BlockNumber; - return GetLowestBlockAfter(moment); + return Search(cache.Genesis, cache.Current, moment, LowestAfterSelector); } - private ulong GetHighestBlockBefore(DateTime moment) + private ulong Search(BlockTimeEntry lower, BlockTimeEntry upper, DateTime target, Func isWhatIwant) { - var closestBefore = FindClosestBeforeEntry(moment); - var closestAfter = FindClosestAfterEntry(moment); - - if (closestBefore != null && - closestAfter != null && - closestBefore.Utc < moment && - closestAfter.Utc > moment && - closestBefore.BlockNumber + 1 == closestAfter.BlockNumber) + var middle = GetMiddle(lower, upper); + if (middle.BlockNumber == lower.BlockNumber) { - log.Debug("Found highest-Before: " + closestBefore); - return closestBefore.BlockNumber; + if (isWhatIwant(target, upper)) return upper.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) + if (isWhatIwant(target, middle)) { - log.Debug("Found lowest-after: " + closestAfter); - return closestAfter.BlockNumber; + return middle.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 (middle.Utc > target) { - if (!entries.ContainsKey(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 < cache.Genesis.BlockNumber) throw new Exception("Can't fetch block before genesis."); + if (number > cache.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); + } + } + + public class BlockCache + { + private const int MaxEntries = 1024; + private readonly Dictionary entries = new Dictionary(); + private readonly IWeb3Blocks web3; + + public BlockTimeEntry Genesis { get; private set; } = null!; + public BlockTimeEntry Current { get; private set; } = null!; + + public BlockCache(IWeb3Blocks web3) + { + this.web3 = web3; + } + + public void Initialize() + { + AddCurrentBlock(); + LookForGenesisBlock(); + + if (Current.BlockNumber == Genesis.BlockNumber) + { + throw new Exception("Unsupported condition: Current block is genesis block."); + } + } + + 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) { - var newBlock = AddBlockNumber(target); - if (newBlock == null) return; - fetchDown--; + entries.Clear(); + Initialize(); } - target--; - if (target <= 0) return; + entries.Add(entry.BlockNumber, entry); } + + return entries[entry.BlockNumber]; } - private void FetchUp(ulong max, ulong blockDifference) + public BlockTimeEntry? Get(ulong number) { - var target = max - blockDifference; - var fetchUp = FetchRange; - while (fetchUp > 0) + if (!entries.TryGetValue(number, out BlockTimeEntry? value)) return null; + return value; + } + + private void LookForGenesisBlock() + { + if (Genesis != null) return; + + var blockTime = web3.GetTimestampForBlock(0); + if (blockTime != null) { - if (!entries.ContainsKey(target)) + 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) { - 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 blockNumber = web3.GetCurrentBlockNumber(); - return AddBlockNumber(blockNumber); - } - - private DateTime? GetTimestampFromBlock(ulong blockNumber) - { - return web3.GetTimestampForBlock(blockNumber); - } - - private BlockTimeEntry? FindClosestBeforeEntry(DateTime moment) - { - BlockTimeEntry? result = null; - foreach (var entry in entries.Values) - { - if (result == null) - { - if (entry.Utc < moment) result = entry; + AddGenesisBlock(lower, lowTime.Value); } else { - if (entry.Utc > result.Utc && entry.Utc < moment) result = entry; + AddGenesisBlock(upper); } + return; + } + + var current = lower + (range / 2); + + var blockTime = web3.GetTimestampForBlock(current); + if (blockTime != null) + { + var newUpper = Add(current, blockTime.Value); + LookForGenesisBlock(lower, newUpper); + } + else + { + LookForGenesisBlock(current, upper); } - return result; } - private BlockTimeEntry? FindClosestAfterEntry(DateTime moment) + private void AddCurrentBlock() { - 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; + 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); + Add(Current); + } + + private void AddGenesisBlock(ulong number, DateTime dateTime) + { + AddGenesisBlock(new BlockTimeEntry(number, dateTime)); + } + + private void AddGenesisBlock(BlockTimeEntry entry) + { + Genesis = entry; + Add(Genesis); } } } diff --git a/Framework/NethereumWorkflow/Web3Wrapper.cs b/Framework/NethereumWorkflow/Web3Wrapper.cs index 67c8ca91..985bed9f 100644 --- a/Framework/NethereumWorkflow/Web3Wrapper.cs +++ b/Framework/NethereumWorkflow/Web3Wrapper.cs @@ -7,8 +7,8 @@ namespace NethereumWorkflow { public interface IWeb3Blocks { - decimal GetCurrentBlockNumber(); - DateTime? GetTimestampForBlock(decimal blockNumber); + ulong GetCurrentBlockNumber(); + DateTime? GetTimestampForBlock(ulong blockNumber); } public class Web3Wrapper : IWeb3Blocks @@ -22,17 +22,17 @@ namespace NethereumWorkflow this.log = log; } - public decimal GetCurrentBlockNumber() + public ulong GetCurrentBlockNumber() { var number = Time.Wait(web3.Eth.Blocks.GetBlockNumber.SendRequestAsync()); - return number.ToDecimal(); + return Convert.ToUInt64(number.ToDecimal()); } - public DateTime? GetTimestampForBlock(decimal blockNumber) + public DateTime? GetTimestampForBlock(ulong blockNumber) { try { - var block = Time.Wait(web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(new BlockParameter(Convert.ToUInt64(blockNumber)))); + 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; } diff --git a/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs b/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs index a9506d7f..94039811 100644 --- a/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs +++ b/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs @@ -8,21 +8,20 @@ namespace FrameworkTests.NethereumWorkflow [TestFixture] public class BlockTimeFinderTests { + private readonly Mock log = new Mock(); private Mock web3 = new Mock(); - private Mock log = new Mock(); - private Dictionary blocks = new Dictionary(); - private DateTime start = DateTime.Now; + private Dictionary blocks = new Dictionary(); private BlockTimeFinder finder = null!; - private void SetupContinuousBlockchain() + private void SetupBlockchain() { - start = DateTime.UtcNow.AddDays(-1).AddSeconds(-30); - blocks = new Dictionary(); + var start = DateTime.UtcNow.AddDays(-1).AddSeconds(-30); + blocks = new Dictionary(); - for (var i = 0; i < 30; i++) + for (ulong i = 0; i < 30; i++) { - decimal d = 100 + i; + ulong d = 100 + i; blocks.Add(d, new Block(d, start + TimeSpan.FromSeconds(i * 2))); } } @@ -30,11 +29,11 @@ namespace FrameworkTests.NethereumWorkflow [SetUp] public void SetUp() { - SetupContinuousBlockchain(); + SetupBlockchain(); web3 = new Mock(); web3.Setup(w => w.GetCurrentBlockNumber()).Returns(blocks.Keys.Max()); - web3.Setup(w => w.GetTimestampForBlock(It.IsAny())).Returns(d => + web3.Setup(w => w.GetTimestampForBlock(It.IsAny())).Returns(d => { if (blocks.ContainsKey(d)) return blocks[d].Time; return null; @@ -117,21 +116,19 @@ namespace FrameworkTests.NethereumWorkflow Assert.That(notFound, Is.Null); } - } public class Block { - public Block(decimal number, DateTime time) + public Block(ulong number, DateTime time) { Number = number; Time = time; } - public decimal Number { get; } + public ulong Number { get; } public DateTime Time { get; } public DateTime JustBefore { get { return Time.AddSeconds(-1); } } public DateTime JustAfter { get { return Time.AddSeconds(1); } } } - } From 3cc3a2a9ddb168e81d2da4513c0a235e48596cfc Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 21 Mar 2024 16:26:48 +0100 Subject: [PATCH 20/20] cleanup --- .../NethereumWorkflow/BlockTimeFinder.cs | 217 ------------------ .../BlockUtils/BlockCache.cs | 39 ++++ .../{ => BlockUtils}/BlockTimeEntry.cs | 2 +- .../BlockUtils/BlockTimeFinder.cs | 95 ++++++++ .../BlockUtils/BlockchainBounds.cs | 106 +++++++++ .../NethereumWorkflow/NethereumInteraction.cs | 19 +- .../NethereumWorkflow/BlockTimeFinderTests.cs | 3 +- 7 files changed, 260 insertions(+), 221 deletions(-) delete mode 100644 Framework/NethereumWorkflow/BlockTimeFinder.cs create mode 100644 Framework/NethereumWorkflow/BlockUtils/BlockCache.cs rename Framework/NethereumWorkflow/{ => BlockUtils}/BlockTimeEntry.cs (90%) create mode 100644 Framework/NethereumWorkflow/BlockUtils/BlockTimeFinder.cs create mode 100644 Framework/NethereumWorkflow/BlockUtils/BlockchainBounds.cs diff --git a/Framework/NethereumWorkflow/BlockTimeFinder.cs b/Framework/NethereumWorkflow/BlockTimeFinder.cs deleted file mode 100644 index 829870b4..00000000 --- a/Framework/NethereumWorkflow/BlockTimeFinder.cs +++ /dev/null @@ -1,217 +0,0 @@ -using Logging; - -namespace NethereumWorkflow -{ - public class BlockTimeFinder - { - private readonly BlockCache cache; - private readonly IWeb3Blocks web3; - private readonly ILog log; - - public BlockTimeFinder(IWeb3Blocks web3, ILog log) - { - this.web3 = web3; - this.log = log; - - cache = new BlockCache(web3); - } - - public ulong? GetHighestBlockNumberBefore(DateTime moment) - { - cache.Initialize(); - if (moment <= cache.Genesis.Utc) return null; - if (moment >= cache.Current.Utc) return cache.Current.BlockNumber; - - return Search(cache.Genesis, cache.Current, moment, HighestBeforeSelector); - } - - public ulong? GetLowestBlockNumberAfter(DateTime moment) - { - cache.Initialize(); - if (moment >= cache.Current.Utc) return null; - if (moment <= cache.Genesis.Utc) return cache.Genesis.BlockNumber; - - return Search(cache.Genesis, cache.Current, moment, LowestAfterSelector); - } - - private ulong Search(BlockTimeEntry lower, BlockTimeEntry upper, DateTime target, Func 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 < cache.Genesis.BlockNumber) throw new Exception("Can't fetch block before genesis."); - if (number > cache.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); - } - } - - public class BlockCache - { - private const int MaxEntries = 1024; - private readonly Dictionary entries = new Dictionary(); - private readonly IWeb3Blocks web3; - - public BlockTimeEntry Genesis { get; private set; } = null!; - public BlockTimeEntry Current { get; private set; } = null!; - - public BlockCache(IWeb3Blocks web3) - { - this.web3 = web3; - } - - public void Initialize() - { - AddCurrentBlock(); - LookForGenesisBlock(); - - if (Current.BlockNumber == Genesis.BlockNumber) - { - throw new Exception("Unsupported condition: Current block is genesis block."); - } - } - - 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(); - Initialize(); - } - 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; - } - - private void LookForGenesisBlock() - { - if (Genesis != null) 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 = 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); - Add(Current); - } - - private void AddGenesisBlock(ulong number, DateTime dateTime) - { - AddGenesisBlock(new BlockTimeEntry(number, dateTime)); - } - - private void AddGenesisBlock(BlockTimeEntry entry) - { - Genesis = entry; - Add(Genesis); - } - } -} diff --git a/Framework/NethereumWorkflow/BlockUtils/BlockCache.cs b/Framework/NethereumWorkflow/BlockUtils/BlockCache.cs new file mode 100644 index 00000000..3fdf8033 --- /dev/null +++ b/Framework/NethereumWorkflow/BlockUtils/BlockCache.cs @@ -0,0 +1,39 @@ +namespace NethereumWorkflow.BlockUtils +{ + public class BlockCache + { + public delegate void CacheClearedEvent(); + + private const int MaxEntries = 1024; + private readonly Dictionary entries = new Dictionary(); + + 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; + } + } +} diff --git a/Framework/NethereumWorkflow/BlockTimeEntry.cs b/Framework/NethereumWorkflow/BlockUtils/BlockTimeEntry.cs similarity index 90% rename from Framework/NethereumWorkflow/BlockTimeEntry.cs rename to Framework/NethereumWorkflow/BlockUtils/BlockTimeEntry.cs index 921f5b59..8846b93e 100644 --- a/Framework/NethereumWorkflow/BlockTimeEntry.cs +++ b/Framework/NethereumWorkflow/BlockUtils/BlockTimeEntry.cs @@ -1,4 +1,4 @@ -namespace NethereumWorkflow +namespace NethereumWorkflow.BlockUtils { public class BlockTimeEntry { diff --git a/Framework/NethereumWorkflow/BlockUtils/BlockTimeFinder.cs b/Framework/NethereumWorkflow/BlockUtils/BlockTimeFinder.cs new file mode 100644 index 00000000..851a8bfe --- /dev/null +++ b/Framework/NethereumWorkflow/BlockUtils/BlockTimeFinder.cs @@ -0,0 +1,95 @@ +using Logging; + +namespace NethereumWorkflow.BlockUtils +{ + public class BlockTimeFinder + { + private readonly BlockCache cache; + private readonly BlockchainBounds bounds; + private readonly IWeb3Blocks web3; + private readonly ILog log; + + public BlockTimeFinder(BlockCache cache, IWeb3Blocks web3, ILog log) + { + this.web3 = web3; + this.log = log; + + this.cache = cache; + bounds = new BlockchainBounds(cache, web3); + } + + public ulong? GetHighestBlockNumberBefore(DateTime moment) + { + bounds.Initialize(); + if (moment <= bounds.Genesis.Utc) return null; + if (moment >= bounds.Current.Utc) return bounds.Current.BlockNumber; + + return Search(bounds.Genesis, bounds.Current, moment, HighestBeforeSelector); + } + + public ulong? GetLowestBlockNumberAfter(DateTime moment) + { + bounds.Initialize(); + if (moment >= bounds.Current.Utc) return null; + if (moment <= bounds.Genesis.Utc) return bounds.Genesis.BlockNumber; + + return Search(bounds.Genesis, bounds.Current, moment, LowestAfterSelector); + } + + private ulong Search(BlockTimeEntry lower, BlockTimeEntry upper, DateTime target, Func 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); + } + } +} diff --git a/Framework/NethereumWorkflow/BlockUtils/BlockchainBounds.cs b/Framework/NethereumWorkflow/BlockUtils/BlockchainBounds.cs new file mode 100644 index 00000000..92841b37 --- /dev/null +++ b/Framework/NethereumWorkflow/BlockUtils/BlockchainBounds.cs @@ -0,0 +1,106 @@ +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); + } + } +} diff --git a/Framework/NethereumWorkflow/NethereumInteraction.cs b/Framework/NethereumWorkflow/NethereumInteraction.cs index 2f588740..3240d9ba 100644 --- a/Framework/NethereumWorkflow/NethereumInteraction.cs +++ b/Framework/NethereumWorkflow/NethereumInteraction.cs @@ -3,12 +3,16 @@ using Nethereum.ABI.FunctionEncoding.Attributes; using Nethereum.Contracts; using Nethereum.RPC.Eth.DTOs; using Nethereum.Web3; +using NethereumWorkflow.BlockUtils; 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; @@ -88,12 +92,23 @@ namespace NethereumWorkflow public List> GetEvents(string address, TimeRange timeRange) where TEvent : IEventDTO, new() { var wrapper = new Web3Wrapper(web3, log); - var blockTimeFinder = new BlockTimeFinder(wrapper, log); + var blockTimeFinder = new BlockTimeFinder(blockCache, wrapper, log); var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(timeRange.From); var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(timeRange.To); - return GetEvents(address, fromBlock, toBlock); + if (!fromBlock.HasValue) + { + log.Error("Failed to find lowest block for time range: " + timeRange); + throw new Exception("Failed"); + } + if (!toBlock.HasValue) + { + log.Error("Failed to find highest block for time range: " + timeRange); + throw new Exception("Failed"); + } + + return GetEvents(address, fromBlock.Value, toBlock.Value); } public List> GetEvents(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new() diff --git a/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs b/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs index 94039811..5653b450 100644 --- a/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs +++ b/Tests/FrameworkTests/NethereumWorkflow/BlockTimeFinderTests.cs @@ -1,6 +1,7 @@ using Logging; using Moq; using NethereumWorkflow; +using NethereumWorkflow.BlockUtils; using NUnit.Framework; namespace FrameworkTests.NethereumWorkflow @@ -39,7 +40,7 @@ namespace FrameworkTests.NethereumWorkflow return null; }); - finder = new BlockTimeFinder(web3.Object, log.Object); + finder = new BlockTimeFinder(new BlockCache(), web3.Object, log.Object); } [Test]