From 4f461e4cb327a797d369b311e4ea42a9724aae21 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 28 Mar 2024 13:49:02 +0100 Subject: [PATCH 1/6] Checking onchain events to debug missing slotFilled event --- Tests/CodexTests/BasicTests/ExampleTests.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Tests/CodexTests/BasicTests/ExampleTests.cs b/Tests/CodexTests/BasicTests/ExampleTests.cs index 60670ea..d308f8f 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -107,6 +107,15 @@ namespace CodexTests.BasicTests var purchaseContract = buyer.Marketplace.RequestStorage(purchase); + Time.Retry(() => + { + var slotFilledEvents = contracts.GetSlotFilledEvents(GetTestRunTimeRange()); + + Log($"SlotFilledEvents: {slotFilledEvents.Length} - NumSlots: {purchase.MinRequiredNumberOfNodes}"); + + if (slotFilledEvents.Length != purchase.MinRequiredNumberOfNodes) throw new Exception("not yet"); + }, Convert.ToInt32(purchase.Duration.TotalSeconds / 2) + 10, TimeSpan.FromSeconds(2), "Checking SlotFilled events"); + purchaseContract.WaitForStorageContractStarted(); AssertBalance(contracts, seller, Is.LessThan(sellerInitialBalance), "Collateral was not placed."); From 4e978bd5b541e671192824b01616f891bc1a0d8f Mon Sep 17 00:00:00 2001 From: benbierens Date: Mon, 1 Apr 2024 08:29:55 +0200 Subject: [PATCH 2/6] Restores continuous tests --- .../ElasticSearchLogDownloader.cs | 18 ++++++++++-------- Tests/CodexContinuousTests/NodeRunner.cs | 2 +- Tests/CodexContinuousTests/SingleTestRun.cs | 2 +- Tests/CodexContinuousTests/StartupChecker.cs | 6 +++--- Tests/CodexContinuousTests/Tests/PeersTest.cs | 12 ++++++------ 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/Tests/CodexContinuousTests/ElasticSearchLogDownloader.cs b/Tests/CodexContinuousTests/ElasticSearchLogDownloader.cs index 9b7e608..80203af 100644 --- a/Tests/CodexContinuousTests/ElasticSearchLogDownloader.cs +++ b/Tests/CodexContinuousTests/ElasticSearchLogDownloader.cs @@ -34,11 +34,11 @@ namespace ContinuousTests $"{startUtc.ToString("o")} - {endUtc.ToString("o")}"); log.Log(openingLine); - var http = CreateElasticSearchHttp(); + var endpoint = CreateElasticSearchEndpoint(); var queryTemplate = CreateQueryTemplate(container, startUtc, endUtc); targetFile.Write($"Downloading '{container.Name}' to '{targetFile.FullFilename}'."); - var reconstructor = new LogReconstructor(targetFile, http, queryTemplate); + var reconstructor = new LogReconstructor(targetFile, endpoint, queryTemplate); reconstructor.DownloadFullLog(); log.Log("Log download finished."); @@ -64,34 +64,36 @@ namespace ContinuousTests .Replace("", namespaceName); } - private IHttp CreateElasticSearchHttp() + private IEndpoint CreateElasticSearchEndpoint() { var serviceName = "elasticsearch"; var k8sNamespace = "monitoring"; var address = new Address($"http://{serviceName}.{k8sNamespace}.svc.cluster.local", 9200); var baseUrl = ""; - return tools.CreateHttp(address, baseUrl, client => + var http = tools.CreateHttp(client => { client.DefaultRequestHeaders.Add("kbn-xsrf", "reporting"); }); + + return http.CreateEndpoint(address, baseUrl); } public class LogReconstructor { private readonly List queue = new List(); private readonly LogFile targetFile; - private readonly IHttp http; + private readonly IEndpoint endpoint; private readonly string queryTemplate; private const int sizeOfPage = 2000; private string searchAfter = ""; private int lastHits = 1; private ulong? lastLogLine; - public LogReconstructor(LogFile targetFile, IHttp http, string queryTemplate) + public LogReconstructor(LogFile targetFile, IEndpoint endpoint, string queryTemplate) { this.targetFile = targetFile; - this.http = http; + this.endpoint = endpoint; this.queryTemplate = queryTemplate; } @@ -110,7 +112,7 @@ namespace ContinuousTests .Replace("", sizeOfPage.ToString()) .Replace("", searchAfter); - var response = http.HttpPostString("_search", query); + var response = endpoint.HttpPostString("_search", query); lastHits = response.hits.hits.Length; if (lastHits > 0) diff --git a/Tests/CodexContinuousTests/NodeRunner.cs b/Tests/CodexContinuousTests/NodeRunner.cs index 488e47a..31f1f2e 100644 --- a/Tests/CodexContinuousTests/NodeRunner.cs +++ b/Tests/CodexContinuousTests/NodeRunner.cs @@ -44,7 +44,7 @@ namespace ContinuousTests try { var debugInfo = bootstrapNode.GetDebugInfo(); - Assert.That(!string.IsNullOrEmpty(debugInfo.spr)); + Assert.That(!string.IsNullOrEmpty(debugInfo.Spr)); var node = entryPoint.CreateInterface().StartCodexNode(s => { diff --git a/Tests/CodexContinuousTests/SingleTestRun.cs b/Tests/CodexContinuousTests/SingleTestRun.cs index 4552b46..7c6c5a6 100644 --- a/Tests/CodexContinuousTests/SingleTestRun.cs +++ b/Tests/CodexContinuousTests/SingleTestRun.cs @@ -128,7 +128,7 @@ namespace ContinuousTests var deploymentName = container.RunningContainers.StartResult.Deployment.Name; var namespaceName = container.RunningContainers.StartResult.Cluster.Configuration.KubernetesNamespace; var openingLine = - $"{namespaceName} - {deploymentName} = {node.Container.Name} = {node.GetDebugInfo().id}"; + $"{namespaceName} - {deploymentName} = {node.Container.Name} = {node.GetDebugInfo().Id}"; elasticSearchLogDownloader.Download(fixtureLog.CreateSubfile(), node.Container, effectiveStart, effectiveEnd, openingLine); } diff --git a/Tests/CodexContinuousTests/StartupChecker.cs b/Tests/CodexContinuousTests/StartupChecker.cs index 1527589..d3fb456 100644 --- a/Tests/CodexContinuousTests/StartupChecker.cs +++ b/Tests/CodexContinuousTests/StartupChecker.cs @@ -123,10 +123,10 @@ namespace ContinuousTests try { var info = n.GetDebugInfo(); - if (info == null || string.IsNullOrEmpty(info.id)) return false; + if (info == null || string.IsNullOrEmpty(info.Id)) return false; - log.Log($"Codex version: '{info.codex.version}' revision: '{info.codex.revision}'"); - LogReplacements.Add(new BaseLogStringReplacement(info.id, n.GetName())); + log.Log($"Codex version: '{info.Version.Version}' revision: '{info.Version.Revision}'"); + LogReplacements.Add(new BaseLogStringReplacement(info.Id, n.GetName())); } catch { diff --git a/Tests/CodexContinuousTests/Tests/PeersTest.cs b/Tests/CodexContinuousTests/Tests/PeersTest.cs index 38d2f4f..87bcb82 100644 --- a/Tests/CodexContinuousTests/Tests/PeersTest.cs +++ b/Tests/CodexContinuousTests/Tests/PeersTest.cs @@ -24,12 +24,12 @@ namespace CodexContinuousTests.Tests var allInfos = Nodes.Select(n => { var info = n.GetDebugInfo(); - Log.Log($"{n.GetName()} = {info.table.localNode.nodeId}"); - Log.AddStringReplace(info.table.localNode.nodeId, n.GetName()); + Log.Log($"{n.GetName()} = {info.Table.LocalNode.NodeId}"); + Log.AddStringReplace(info.Table.LocalNode.NodeId, n.GetName()); return info; }).ToArray(); - var allIds = allInfos.Select(i => i.table.localNode.nodeId).ToArray(); + var allIds = allInfos.Select(i => i.Table.LocalNode.NodeId).ToArray(); var errors = Nodes.Select(n => AreAllPresent(n, allIds)).Where(s => !string.IsNullOrEmpty(s)).ToArray(); if (errors.Any()) @@ -41,13 +41,13 @@ namespace CodexContinuousTests.Tests private string AreAllPresent(ICodexNode n, string[] allIds) { var info = n.GetDebugInfo(); - var known = info.table.nodes.Select(n => n.nodeId).ToArray(); - var expected = allIds.Where(i => i != info.table.localNode.nodeId).ToArray(); + var known = info.Table.Nodes.Select(n => n.NodeId).ToArray(); + var expected = allIds.Where(i => i != info.Table.LocalNode.NodeId).ToArray(); if (!expected.All(ex => known.Contains(ex))) { var nl = Environment.NewLine; - return $"{nl}At node '{info.table.localNode.nodeId}'{nl}" + + return $"{nl}At node '{info.Table.LocalNode.NodeId}'{nl}" + $"Not all of{nl}'{string.Join(",", expected)}'{nl}" + $"were present in routing table:{nl}'{string.Join(",", known)}'"; } From b40215dd36d64a132c54f82332e8fbed27a97f37 Mon Sep 17 00:00:00 2001 From: benbierens Date: Mon, 1 Apr 2024 08:58:45 +0200 Subject: [PATCH 3/6] Updates image and api --- ProjectPlugins/CodexPlugin/ApiChecker.cs | 2 +- .../CodexPlugin/CodexContainerRecipe.cs | 2 +- ProjectPlugins/CodexPlugin/openapi.yaml | 22 +++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ProjectPlugins/CodexPlugin/ApiChecker.cs b/ProjectPlugins/CodexPlugin/ApiChecker.cs index ae9984e..498aeb1 100644 --- a/ProjectPlugins/CodexPlugin/ApiChecker.cs +++ b/ProjectPlugins/CodexPlugin/ApiChecker.cs @@ -9,7 +9,7 @@ namespace CodexPlugin public class ApiChecker { // - private const string OpenApiYamlHash = "DC-90-1B-63-76-1B-92-01-05-65-33-DA-17-C2-34-83-E1-2E-6C-A9-04-4D-68-ED-96-43-F5-E5-6A-00-0F-5F"; + private const string OpenApiYamlHash = "63-7F-46-5E-2C-60-7A-BD-0C-EC-32-87-61-1B-79-FA-C2-EF-73-81-BA-FA-28-77-33-02-81-30-80-5D-00-97"; private const string OpenApiFilePath = "/codex/openapi.yaml"; private const string DisableEnvironmentVariable = "CODEXPLUGIN_DISABLE_APICHECK"; diff --git a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs index 7e7c111..84f2732 100644 --- a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs +++ b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs @@ -7,7 +7,7 @@ namespace CodexPlugin { public class CodexContainerRecipe : ContainerRecipeFactory { - private const string DefaultDockerImage = "codexstorage/nim-codex:sha-cd280d4-dist-tests"; + private const string DefaultDockerImage = "codexstorage/nim-codex:sha-644c83b-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/openapi.yaml b/ProjectPlugins/CodexPlugin/openapi.yaml index 3a1ad70..6099ed5 100644 --- a/ProjectPlugins/CodexPlugin/openapi.yaml +++ b/ProjectPlugins/CodexPlugin/openapi.yaml @@ -69,7 +69,7 @@ components: type: object properties: totalChunks: - type: number + type: integer PoRParameters: description: Parameters for Proof of Retrievability @@ -186,12 +186,12 @@ components: proofProbability: $ref: "#/components/schemas/ProofProbability" nodes: - type: number description: Minimal number of nodes the content should be stored on + type: integer default: 1 tolerance: - type: number description: Additional number of nodes on top of the `nodes` property that can be lost before pronouncing the content lost + type: integer default: 0 collateral: type: string @@ -206,8 +206,8 @@ components: - reward properties: slots: - type: number description: Number of slots (eq. hosts) that the Request want to have the content spread over + type: integer slotSize: type: string description: Amount of storage per slot (in bytes) as decimal string @@ -218,7 +218,7 @@ components: reward: $ref: "#/components/schemas/Reward" maxSlotLoss: - type: number + type: integer description: Max slots that can be lost without data considered to be lost StorageRequest: @@ -274,10 +274,10 @@ components: $ref: "#/components/schemas/Cid" description: "Root hash of the content" originalBytes: - type: number + type: integer description: "Length of original content in bytes" blockSize: - type: number + type: integer description: "Size of blocks" protected: type: boolean @@ -287,16 +287,16 @@ components: type: object properties: totalBlocks: - type: number description: "Number of blocks stored by the node" + type: integer quotaMaxBytes: - type: number + type: integer description: "Maximum storage space used by the node" quotaUsedBytes: - type: number + type: integer description: "Amount of storage space currently in use" quotaReservedBytes: - type: number + type: integer description: "Amount of storage space reserved" servers: From 0263cc4efff0202d4dedeeee313310cf36c85bce Mon Sep 17 00:00:00 2001 From: benbierens Date: Mon, 1 Apr 2024 10:04:32 +0200 Subject: [PATCH 4/6] Force rebuild of openapi client code each CodexPlugin build. --- ProjectPlugins/CodexPluginPrebuild/Program.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ProjectPlugins/CodexPluginPrebuild/Program.cs b/ProjectPlugins/CodexPluginPrebuild/Program.cs index 8f4a8ec..d6d2a5a 100644 --- a/ProjectPlugins/CodexPluginPrebuild/Program.cs +++ b/ProjectPlugins/CodexPluginPrebuild/Program.cs @@ -4,6 +4,7 @@ using System.Text; public static class Program { private const string OpenApiFile = "../CodexPlugin/openapi.yaml"; + private const string ClientFile = "../CodexPlugin/obj/openapiClient.cs"; private const string Search = ""; private const string TargetFile = "ApiChecker.cs"; @@ -11,6 +12,9 @@ public static class Program { Console.WriteLine("Injecting hash of 'openapi.yaml'..."); + // Force client rebuild by deleting previous artifact. + File.Delete(ClientFile); + var hash = CreateHash(); // This hash is used to verify that the Codex docker image being used is compatible // with the openapi.yaml being used by the Codex plugin. From 9a2e889ef9126eed767eaa074d7d7b67d5ba4509 Mon Sep 17 00:00:00 2001 From: benbierens Date: Mon, 1 Apr 2024 10:11:44 +0200 Subject: [PATCH 5/6] Fixes convert issue in mapper. Removes CodexPlugin from discord bot dependencies. --- ProjectPlugins/CodexPlugin/Mapper.cs | 4 ++-- Tools/BiblioTech/BiblioTech.csproj | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/ProjectPlugins/CodexPlugin/Mapper.cs b/ProjectPlugins/CodexPlugin/Mapper.cs index 014fbc7..9db2a84 100644 --- a/ProjectPlugins/CodexPlugin/Mapper.cs +++ b/ProjectPlugins/CodexPlugin/Mapper.cs @@ -57,8 +57,8 @@ namespace CodexPlugin Reward = ToDecInt(purchase.PricePerSlotPerSecond), Collateral = ToDecInt(purchase.RequiredCollateral), Expiry = ToDecInt(DateTimeOffset.UtcNow.ToUnixTimeSeconds() + purchase.Expiry.TotalSeconds), - Nodes = purchase.MinRequiredNumberOfNodes, - Tolerance = purchase.NodeFailureTolerance + Nodes = Convert.ToInt32(purchase.MinRequiredNumberOfNodes), + Tolerance = Convert.ToInt32(purchase.NodeFailureTolerance) }; } diff --git a/Tools/BiblioTech/BiblioTech.csproj b/Tools/BiblioTech/BiblioTech.csproj index 19422e4..427d3eb 100644 --- a/Tools/BiblioTech/BiblioTech.csproj +++ b/Tools/BiblioTech/BiblioTech.csproj @@ -12,7 +12,6 @@ - From 1344a6e3d2e3fca668aee4f184842327a88c81d9 Mon Sep 17 00:00:00 2001 From: benbierens Date: Mon, 1 Apr 2024 11:00:52 +0200 Subject: [PATCH 6/6] Cleanup --- Framework/Core/Http.cs | 1 - .../CodexPlugin/CodexContainerRecipe.cs | 2 +- Tests/CodexTests/BasicTests/ExampleTests.cs | 125 +--------------- .../CodexTests/BasicTests/MarketplaceTests.cs | 138 ++++++++++++++++++ 4 files changed, 140 insertions(+), 126 deletions(-) create mode 100644 Tests/CodexTests/BasicTests/MarketplaceTests.cs diff --git a/Framework/Core/Http.cs b/Framework/Core/Http.cs index 61d13f8..0bd3117 100644 --- a/Framework/Core/Http.cs +++ b/Framework/Core/Http.cs @@ -51,7 +51,6 @@ namespace Core private string GetDescription() { - // todo: check this: return DebugStack.GetCallerName(skipFrames: 2); } diff --git a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs index 84f2732..4e2cc8f 100644 --- a/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs +++ b/ProjectPlugins/CodexPlugin/CodexContainerRecipe.cs @@ -7,7 +7,7 @@ namespace CodexPlugin { public class CodexContainerRecipe : ContainerRecipeFactory { - private const string DefaultDockerImage = "codexstorage/nim-codex:sha-644c83b-dist-tests"; + private const string DefaultDockerImage = "codexstorage/nim-codex:sha-f2f1dd5-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 d308f8f..e7453b5 100644 --- a/Tests/CodexTests/BasicTests/ExampleTests.cs +++ b/Tests/CodexTests/BasicTests/ExampleTests.cs @@ -1,12 +1,9 @@ -using CodexContractsPlugin; -using CodexPlugin; +using CodexPlugin; using DistTestCore; using GethPlugin; using MetricsPlugin; -using Nethereum.Hex.HexConvertors.Extensions; using NUnit.Framework; using Utils; -using Request = CodexContractsPlugin.Marketplace.Request; namespace CodexTests.BasicTests { @@ -50,88 +47,6 @@ namespace CodexTests.BasicTests metrics[1].AssertThat("libp2p_peers", Is.EqualTo(1)); } - [Test] - public void MarketplaceExample() - { - var sellerInitialBalance = 234.TestTokens(); - var buyerInitialBalance = 100000.TestTokens(); - var fileSize = 10.MB(); - - var geth = Ci.StartGethNode(s => s.IsMiner().WithName("disttest-geth")); - var contracts = Ci.StartCodexContracts(geth); - - var seller = AddCodex(s => s - .WithName("Seller") - .WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn) - { - ContractClock = CodexLogLevel.Trace, - }) - .WithStorageQuota(11.GB()) - .EnableMarketplace(geth, contracts, m => m - .WithInitial(10.Eth(), sellerInitialBalance) - .AsStorageNode() - .AsValidator())); - - AssertBalance(contracts, seller, Is.EqualTo(sellerInitialBalance)); - - var availability = new StorageAvailability( - totalSpace: 10.GB(), - maxDuration: TimeSpan.FromMinutes(30), - minPriceForTotalSpace: 1.TestTokens(), - maxCollateral: 20.TestTokens() - ); - seller.Marketplace.MakeStorageAvailable(availability); - - var testFile = GenerateTestFile(fileSize); - - var buyer = AddCodex(s => s - .WithName("Buyer") - .WithBootstrapNode(seller) - .EnableMarketplace(geth, contracts, m => m - .WithInitial(10.Eth(), buyerInitialBalance))); - - AssertBalance(contracts, buyer, Is.EqualTo(buyerInitialBalance)); - - var contentId = buyer.UploadFile(testFile); - - var purchase = new StoragePurchaseRequest(contentId) - { - PricePerSlotPerSecond = 2.TestTokens(), - RequiredCollateral = 10.TestTokens(), - MinRequiredNumberOfNodes = 5, - NodeFailureTolerance = 2, - ProofProbability = 5, - Duration = TimeSpan.FromMinutes(5), - Expiry = TimeSpan.FromMinutes(4) - }; - - var purchaseContract = buyer.Marketplace.RequestStorage(purchase); - - Time.Retry(() => - { - var slotFilledEvents = contracts.GetSlotFilledEvents(GetTestRunTimeRange()); - - Log($"SlotFilledEvents: {slotFilledEvents.Length} - NumSlots: {purchase.MinRequiredNumberOfNodes}"); - - if (slotFilledEvents.Length != purchase.MinRequiredNumberOfNodes) throw new Exception("not yet"); - }, Convert.ToInt32(purchase.Duration.TotalSeconds / 2) + 10, TimeSpan.FromSeconds(2), "Checking SlotFilled events"); - - purchaseContract.WaitForStorageContractStarted(); - - AssertBalance(contracts, seller, Is.LessThan(sellerInitialBalance), "Collateral was not placed."); - - var request = GetOnChainStorageRequest(contracts); - AssertStorageRequest(request, purchase, contracts, buyer); - AssertSlotFilledEvents(contracts, purchase, request, seller); - AssertContractSlot(contracts, request, 0, seller); - - purchaseContract.WaitForStorageContractFinished(); - - AssertBalance(contracts, seller, Is.GreaterThan(sellerInitialBalance), "Seller was not paid for storage."); - AssertBalance(contracts, buyer, Is.LessThan(buyerInitialBalance), "Buyer was not charged for storage."); - Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Finished)); - } - [Test] public void GethBootstrapTest() { @@ -148,43 +63,5 @@ namespace CodexTests.BasicTests Assert.That(bootN, Is.EqualTo(followN)); Assert.That(discN, Is.LessThan(bootN)); } - - private void AssertSlotFilledEvents(ICodexContracts contracts, StoragePurchaseRequest 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(purchase.MinRequiredNumberOfNodes)); - for (var i = 0; i < purchase.MinRequiredNumberOfNodes; i++) - { - var filledSlotEvent = filledSlotEvents.Single(e => e.SlotIndex == i); - Assert.That(filledSlotEvent.RequestId.ToHex(), Is.EqualTo(request.RequestId.ToHex())); - Assert.That(filledSlotEvent.Host, Is.EqualTo(seller.EthAddress)); - } - } - - private void AssertStorageRequest(Request request, StoragePurchaseRequest purchase, ICodexContracts contracts, ICodexNode buyer) - { - Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Started)); - Assert.That(request.ClientAddress, Is.EqualTo(buyer.EthAddress)); - Assert.That(request.Ask.Slots, Is.EqualTo(purchase.MinRequiredNumberOfNodes)); - } - - private Request GetOnChainStorageRequest(ICodexContracts contracts) - { - var requests = contracts.GetStorageRequests(GetTestRunTimeRange()); - Assert.That(requests.Length, Is.EqualTo(1)); - return requests.Single(); - } - - private void AssertContractSlot(ICodexContracts contracts, Request request, int contractSlotIndex, ICodexNode expectedSeller) - { - var slotHost = contracts.GetSlotHost(request, contractSlotIndex); - Assert.That(slotHost, Is.EqualTo(expectedSeller.EthAddress)); - } } } diff --git a/Tests/CodexTests/BasicTests/MarketplaceTests.cs b/Tests/CodexTests/BasicTests/MarketplaceTests.cs new file mode 100644 index 0000000..cb80008 --- /dev/null +++ b/Tests/CodexTests/BasicTests/MarketplaceTests.cs @@ -0,0 +1,138 @@ +using CodexContractsPlugin; +using CodexContractsPlugin.Marketplace; +using CodexPlugin; +using GethPlugin; +using Nethereum.Hex.HexConvertors.Extensions; +using NUnit.Framework; +using Utils; + +namespace CodexTests.BasicTests +{ + [TestFixture] + public class MarketplaceTests : AutoBootstrapDistTest + { + [Test] + public void MarketplaceExample() + { + var hostInitialBalance = 234.TestTokens(); + var clientInitialBalance = 100000.TestTokens(); + var fileSize = 10.MB(); + + var geth = Ci.StartGethNode(s => s.IsMiner().WithName("disttest-geth")); + var contracts = Ci.StartCodexContracts(geth); + + var host = AddCodex(s => s + .WithName("Host") + .WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn) + { + ContractClock = CodexLogLevel.Trace, + }) + .WithStorageQuota(11.GB()) + .EnableMarketplace(geth, contracts, m => m + .WithInitial(10.Eth(), hostInitialBalance) + .AsStorageNode() + .AsValidator())); + + AssertBalance(contracts, host, Is.EqualTo(hostInitialBalance)); + + var availability = new StorageAvailability( + totalSpace: 10.GB(), + maxDuration: TimeSpan.FromMinutes(30), + minPriceForTotalSpace: 1.TestTokens(), + maxCollateral: 20.TestTokens() + ); + host.Marketplace.MakeStorageAvailable(availability); + + var testFile = GenerateTestFile(fileSize); + + var client = AddCodex(s => s + .WithName("Client") + .EnableMarketplace(geth, contracts, m => m + .WithInitial(10.Eth(), clientInitialBalance))); + + AssertBalance(contracts, client, Is.EqualTo(clientInitialBalance)); + + var contentId = client.UploadFile(testFile); + + var purchase = new StoragePurchaseRequest(contentId) + { + PricePerSlotPerSecond = 2.TestTokens(), + RequiredCollateral = 10.TestTokens(), + MinRequiredNumberOfNodes = 5, + NodeFailureTolerance = 2, + ProofProbability = 5, + Duration = TimeSpan.FromMinutes(5), + Expiry = TimeSpan.FromMinutes(4) + }; + + var purchaseContract = client.Marketplace.RequestStorage(purchase); + + WaitForAllSlotFilledEvents(contracts, purchase); + + purchaseContract.WaitForStorageContractStarted(); + + AssertBalance(contracts, host, Is.LessThan(hostInitialBalance), "Collateral was not placed."); + + var request = GetOnChainStorageRequest(contracts); + AssertStorageRequest(request, purchase, contracts, client); + AssertSlotFilledEvents(contracts, purchase, request, host); + AssertContractSlot(contracts, request, 0, host); + + purchaseContract.WaitForStorageContractFinished(); + + AssertBalance(contracts, host, Is.GreaterThan(hostInitialBalance), "Seller was not paid for storage."); + AssertBalance(contracts, client, Is.LessThan(clientInitialBalance), "Buyer was not charged for storage."); + Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Finished)); + } + + private void WaitForAllSlotFilledEvents(ICodexContracts contracts, StoragePurchaseRequest purchase) + { + Time.Retry(() => + { + var slotFilledEvents = contracts.GetSlotFilledEvents(GetTestRunTimeRange()); + + Log($"SlotFilledEvents: {slotFilledEvents.Length} - NumSlots: {purchase.MinRequiredNumberOfNodes}"); + + if (slotFilledEvents.Length != purchase.MinRequiredNumberOfNodes) throw new Exception(); + }, Convert.ToInt32(purchase.Duration.TotalSeconds / 5) + 10, TimeSpan.FromSeconds(5), "Checking SlotFilled events"); + } + + private void AssertSlotFilledEvents(ICodexContracts contracts, StoragePurchaseRequest 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(purchase.MinRequiredNumberOfNodes)); + for (var i = 0; i < purchase.MinRequiredNumberOfNodes; i++) + { + var filledSlotEvent = filledSlotEvents.Single(e => e.SlotIndex == i); + Assert.That(filledSlotEvent.RequestId.ToHex(), Is.EqualTo(request.RequestId.ToHex())); + Assert.That(filledSlotEvent.Host, Is.EqualTo(seller.EthAddress)); + } + } + + private void AssertStorageRequest(Request request, StoragePurchaseRequest purchase, ICodexContracts contracts, ICodexNode buyer) + { + Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Started)); + Assert.That(request.ClientAddress, Is.EqualTo(buyer.EthAddress)); + Assert.That(request.Ask.Slots, Is.EqualTo(purchase.MinRequiredNumberOfNodes)); + } + + private Request GetOnChainStorageRequest(ICodexContracts contracts) + { + var requests = contracts.GetStorageRequests(GetTestRunTimeRange()); + Assert.That(requests.Length, Is.EqualTo(1)); + return requests.Single(); + } + + private void AssertContractSlot(ICodexContracts contracts, Request request, int contractSlotIndex, ICodexNode expectedSeller) + { + var slotHost = contracts.GetSlotHost(request, contractSlotIndex); + Assert.That(slotHost, Is.EqualTo(expectedSeller.EthAddress)); + } + } +}