From cedaf8474031f21f8f5f6d4c98aa8bdb83f75fb6 Mon Sep 17 00:00:00 2001 From: benbierens Date: Wed, 20 Sep 2023 08:45:55 +0200 Subject: [PATCH] Wires up marketplace access --- CodexPlugin/CodexContainerRecipe.cs | 6 - CodexPlugin/CodexNode.cs | 5 +- CodexPlugin/CodexNodeFactory.cs | 9 +- CodexPlugin/CodexPlugin.cs | 25 ++- CodexPlugin/CodexSetup.cs | 6 +- CodexPlugin/CoreInterfaceExtensions.cs | 4 +- CodexPlugin/MarketplaceAccess.cs | 208 ++++++++++++++++++++++++ CodexPlugin/MarketplaceInitialConfig.cs | 6 +- Tests/BasicTests/ExampleTests.cs | 11 +- 9 files changed, 256 insertions(+), 24 deletions(-) create mode 100644 CodexPlugin/MarketplaceAccess.cs diff --git a/CodexPlugin/CodexContainerRecipe.cs b/CodexPlugin/CodexContainerRecipe.cs index 724f2c4b..e173fc6e 100644 --- a/CodexPlugin/CodexContainerRecipe.cs +++ b/CodexPlugin/CodexContainerRecipe.cs @@ -111,12 +111,6 @@ namespace CodexPlugin return 8.GB().Multiply(1.2); } - //private int GetAccountIndex(MarketplaceInitialConfig marketplaceConfig) - //{ - // if (marketplaceConfig.AccountIndexOverride != null) return marketplaceConfig.AccountIndexOverride.Value; - // return Index; - //} - private string GetDockerImage() { var image = Environment.GetEnvironmentVariable("CODEXDOCKERIMAGE"); diff --git a/CodexPlugin/CodexNode.cs b/CodexPlugin/CodexNode.cs index 6b006c0a..eb17a496 100644 --- a/CodexPlugin/CodexNode.cs +++ b/CodexPlugin/CodexNode.cs @@ -18,6 +18,7 @@ namespace CodexPlugin TrackedFile? DownloadContent(ContentId contentId, string fileLabel = ""); void ConnectToPeer(ICodexNode node); CodexDebugVersionResponse Version { get; } + IMarketplaceAccess Marketplace { get; } void Stop(); } @@ -28,18 +29,20 @@ namespace CodexPlugin private readonly IPluginTools tools; private readonly IEthAddress? ethAddress; - public CodexNode(IPluginTools tools, CodexAccess codexAccess, CodexNodeGroup group, IEthAddress? ethAddress) + public CodexNode(IPluginTools tools, CodexAccess codexAccess, CodexNodeGroup group, IMarketplaceAccess marketplaceAccess, IEthAddress? ethAddress) { this.tools = tools; this.ethAddress = ethAddress; CodexAccess = codexAccess; Group = group; + Marketplace = marketplaceAccess; Version = new CodexDebugVersionResponse(); } public RunningContainer Container { get { return CodexAccess.Container; } } public CodexAccess CodexAccess { get; } public CodexNodeGroup Group { get; } + public IMarketplaceAccess Marketplace { get; } public CodexDebugVersionResponse Version { get; private set; } public IMetricsScrapeTarget MetricsScrapeTarget { diff --git a/CodexPlugin/CodexNodeFactory.cs b/CodexPlugin/CodexNodeFactory.cs index babae9ec..f340fdec 100644 --- a/CodexPlugin/CodexNodeFactory.cs +++ b/CodexPlugin/CodexNodeFactory.cs @@ -20,7 +20,14 @@ namespace CodexPlugin public CodexNode CreateOnlineCodexNode(CodexAccess access, CodexNodeGroup group) { var ethAddress = GetEthAddress(access); - return new CodexNode(tools, access, group, ethAddress); + var marketplaceAccess = GetMarketplaceAccess(access, ethAddress); + return new CodexNode(tools, access, group, marketplaceAccess, ethAddress); + } + + private IMarketplaceAccess GetMarketplaceAccess(CodexAccess codexAccess, IEthAddress? ethAddress) + { + if (ethAddress == null) return new MarketplaceUnavailable(); + return new MarketplaceAccess(tools.GetLog(), codexAccess); } private IEthAddress? GetEthAddress(CodexAccess access) diff --git a/CodexPlugin/CodexPlugin.cs b/CodexPlugin/CodexPlugin.cs index fe634600..b2ae282b 100644 --- a/CodexPlugin/CodexPlugin.cs +++ b/CodexPlugin/CodexPlugin.cs @@ -33,9 +33,7 @@ namespace CodexPlugin public RunningContainers[] StartCodexNodes(int numberOfNodes, Action setup) { - var codexSetup = new CodexSetup(numberOfNodes); - codexSetup.LogLevel = defaultLogLevel; - setup(codexSetup); + var codexSetup = GetSetup(numberOfNodes, setup); return codexStarter.BringOnline(codexSetup); } @@ -43,5 +41,26 @@ namespace CodexPlugin { return codexStarter.WrapCodexContainers(coreInterface, containers); } + + public void WireUpMarketplace(ICodexNodeGroup result, Action setup) + { + var codexSetup = GetSetup(1, setup); + if (codexSetup.MarketplaceConfig == null) return; + + var mconfig = codexSetup.MarketplaceConfig; + foreach (var node in result) + { + mconfig.GethNode.SendEth(node, mconfig.InitialEth); + mconfig.CodexContracts.MintTestTokens(mconfig.GethNode, node, mconfig.InitialTokens); + } + } + + private CodexSetup GetSetup(int numberOfNodes, Action setup) + { + var codexSetup = new CodexSetup(numberOfNodes); + codexSetup.LogLevel = defaultLogLevel; + setup(codexSetup); + return codexSetup; + } } } diff --git a/CodexPlugin/CodexSetup.cs b/CodexPlugin/CodexSetup.cs index 00719a00..46289a90 100644 --- a/CodexPlugin/CodexSetup.cs +++ b/CodexPlugin/CodexSetup.cs @@ -16,7 +16,7 @@ namespace CodexPlugin ICodexSetup WithBlockMaintenanceInterval(TimeSpan duration); ICodexSetup WithBlockMaintenanceNumber(int numberOfBlocks); ICodexSetup EnableMetrics(); - ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, bool isValidator = false); + ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens, bool isValidator = false); } public class CodexSetup : CodexStartupConfig, ICodexSetup @@ -82,9 +82,9 @@ namespace CodexPlugin return this; } - public ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, bool isValidator = false) + public ICodexSetup EnableMarketplace(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens, bool isValidator = false) { - MarketplaceConfig = new MarketplaceInitialConfig(gethNode, codexContracts, isValidator); + MarketplaceConfig = new MarketplaceInitialConfig(gethNode, codexContracts, initialEth, initialTokens, isValidator); return this; } diff --git a/CodexPlugin/CoreInterfaceExtensions.cs b/CodexPlugin/CoreInterfaceExtensions.cs index 946fa882..c9cd6a4d 100644 --- a/CodexPlugin/CoreInterfaceExtensions.cs +++ b/CodexPlugin/CoreInterfaceExtensions.cs @@ -28,7 +28,9 @@ namespace CodexPlugin public static ICodexNodeGroup SetupCodexNodes(this CoreInterface ci, int number, Action setup) { var rc = ci.StartCodexNodes(number, setup); - return ci.WrapCodexContainers(rc); + var result = ci.WrapCodexContainers(rc); + Plugin(ci).WireUpMarketplace(result, setup); + return result; } public static ICodexNodeGroup SetupCodexNodes(this CoreInterface ci, int number) diff --git a/CodexPlugin/MarketplaceAccess.cs b/CodexPlugin/MarketplaceAccess.cs new file mode 100644 index 00000000..2a0683a3 --- /dev/null +++ b/CodexPlugin/MarketplaceAccess.cs @@ -0,0 +1,208 @@ +using CodexContractsPlugin; +using Logging; +using Newtonsoft.Json; +using NUnit.Framework; +using Utils; +using System.Numerics; + +namespace CodexPlugin +{ + public interface IMarketplaceAccess + { + string MakeStorageAvailable(ByteSize size, TestToken minPricePerBytePerSecond, TestToken maxCollateral, TimeSpan maxDuration); + StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration); + } + + public class MarketplaceAccess : IMarketplaceAccess + { + private readonly ILog log; + private readonly CodexAccess codexAccess; + + public MarketplaceAccess(ILog log, CodexAccess codexAccess) + { + this.log = log; + this.codexAccess = codexAccess; + } + + public StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerSlotPerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration) + { + var request = new CodexSalesRequestStorageRequest + { + duration = ToDecInt(duration.TotalSeconds), + proofProbability = ToDecInt(proofProbability), + reward = ToDecInt(pricePerSlotPerSecond), + collateral = ToDecInt(requiredCollateral), + expiry = null, + nodes = minRequiredNumberOfNodes, + tolerance = null, + }; + + Log($"Requesting storage for: {contentId.Id}... (" + + $"pricePerSlotPerSecond: {pricePerSlotPerSecond}, " + + $"requiredCollateral: {requiredCollateral}, " + + $"minRequiredNumberOfNodes: {minRequiredNumberOfNodes}, " + + $"proofProbability: {proofProbability}, " + + $"duration: {Time.FormatDuration(duration)})"); + + var response = codexAccess.RequestStorage(request, contentId.Id); + + if (response == "Purchasing not available") + { + throw new InvalidOperationException(response); + } + + Log($"Storage requested successfully. PurchaseId: '{response}'."); + + return new StoragePurchaseContract(log, codexAccess, response, duration); + } + + public string MakeStorageAvailable(ByteSize totalSpace, TestToken minPriceForTotalSpace, TestToken maxCollateral, TimeSpan maxDuration) + { + var request = new CodexSalesAvailabilityRequest + { + size = ToDecInt(totalSpace.SizeInBytes), + duration = ToDecInt(maxDuration.TotalSeconds), + maxCollateral = ToDecInt(maxCollateral), + minPrice = ToDecInt(minPriceForTotalSpace) + }; + + Log($"Making storage available... (" + + $"size: {totalSpace}, " + + $"minPriceForTotalSpace: {minPriceForTotalSpace}, " + + $"maxCollateral: {maxCollateral}, " + + $"maxDuration: {Time.FormatDuration(maxDuration)})"); + + var response = codexAccess.SalesAvailability(request); + + Log($"Storage successfully made available. Id: {response.id}"); + + return response.id; + } + + private string ToDecInt(double d) + { + var i = new BigInteger(d); + return i.ToString("D"); + } + + public string ToDecInt(TestToken t) + { + var i = new BigInteger(t.Amount); + return i.ToString("D"); + } + + private void Log(string msg) + { + log.Log($"{codexAccess.Container.Name} {msg}"); + } + } + + public class MarketplaceUnavailable : IMarketplaceAccess + { + public StoragePurchaseContract RequestStorage(ContentId contentId, TestToken pricePerBytePerSecond, TestToken requiredCollateral, uint minRequiredNumberOfNodes, int proofProbability, TimeSpan duration) + { + Unavailable(); + return null!; + } + + public string MakeStorageAvailable(ByteSize size, TestToken minPricePerBytePerSecond, TestToken maxCollateral, TimeSpan duration) + { + Unavailable(); + return string.Empty; + } + + private void Unavailable() + { + Assert.Fail("Incorrect test setup: Marketplace was not enabled for this group of Codex nodes. Add 'EnableMarketplace(...)' after 'SetupCodexNodes()' to enable it."); + throw new InvalidOperationException(); + } + } + + public class StoragePurchaseContract + { + private readonly ILog log; + private readonly CodexAccess codexAccess; + private DateTime? contractStartUtc; + + public StoragePurchaseContract(ILog log, CodexAccess codexAccess, string purchaseId, TimeSpan contractDuration) + { + this.log = log; + this.codexAccess = codexAccess; + PurchaseId = purchaseId; + ContractDuration = contractDuration; + } + + public string PurchaseId { get; } + public TimeSpan ContractDuration { get; } + + public void WaitForStorageContractStarted() + { + WaitForStorageContractStarted(TimeSpan.FromSeconds(30)); + } + + public void WaitForStorageContractFinished() + { + if (!contractStartUtc.HasValue) + { + WaitForStorageContractStarted(); + } + var gracePeriod = TimeSpan.FromSeconds(10); + var currentContractTime = DateTime.UtcNow - contractStartUtc!.Value; + var timeout = (ContractDuration - currentContractTime) + gracePeriod; + 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"); + contractStartUtc = DateTime.UtcNow; + } + + private void WaitForStorageContractState(TimeSpan timeout, string desiredState) + { + var lastState = ""; + var waitStart = DateTime.UtcNow; + + log.Log($"Waiting for {Time.FormatDuration(timeout)} for contract '{PurchaseId}' to reach state '{desiredState}'."); + while (lastState != desiredState) + { + var purchaseStatus = codexAccess.GetPurchaseStatus(PurchaseId); + var statusJson = JsonConvert.SerializeObject(purchaseStatus); + if (purchaseStatus != null && purchaseStatus.state != lastState) + { + lastState = purchaseStatus.state; + log.Debug("Purchase status: " + statusJson); + } + + Thread.Sleep(1000); + + if (lastState == "errored") + { + Assert.Fail("Contract errored: " + statusJson); + } + + if (DateTime.UtcNow - waitStart > timeout) + { + Assert.Fail($"Contract did not reach '{desiredState}' within timeout. {statusJson}"); + } + } + log.Log($"Contract '{desiredState}'."); + } + + public CodexStoragePurchase GetPurchaseStatus(string purchaseId) + { + return codexAccess.GetPurchaseStatus(purchaseId); + } + } +} diff --git a/CodexPlugin/MarketplaceInitialConfig.cs b/CodexPlugin/MarketplaceInitialConfig.cs index f6a49187..7c925916 100644 --- a/CodexPlugin/MarketplaceInitialConfig.cs +++ b/CodexPlugin/MarketplaceInitialConfig.cs @@ -5,15 +5,19 @@ namespace CodexPlugin { public class MarketplaceInitialConfig { - public MarketplaceInitialConfig(IGethNode gethNode, ICodexContracts codexContracts, bool isValidator) + public MarketplaceInitialConfig(IGethNode gethNode, ICodexContracts codexContracts, Ether initialEth, TestToken initialTokens, bool isValidator) { GethNode = gethNode; CodexContracts = codexContracts; + InitialEth = initialEth; + InitialTokens = initialTokens; IsValidator = isValidator; } public IGethNode GethNode { get; } public ICodexContracts CodexContracts { get; } + public Ether InitialEth { get; } + public TestToken InitialTokens { get; } public bool IsValidator { get; } } } diff --git a/Tests/BasicTests/ExampleTests.cs b/Tests/BasicTests/ExampleTests.cs index 9a00c186..40609a8c 100644 --- a/Tests/BasicTests/ExampleTests.cs +++ b/Tests/BasicTests/ExampleTests.cs @@ -1,5 +1,4 @@ using CodexContractsPlugin; -using CodexPlugin; using DistTestCore; using GethPlugin; using MetricsPlugin; @@ -57,9 +56,7 @@ namespace Tests.BasicTests var seller = AddCodex(s => s .WithStorageQuota(11.GB()) - .EnableMarketplace(geth, contracts)); - geth.SendEth(seller, 10.Eth()); - contracts.MintTestTokens(geth, seller, sellerInitialBalance); + .EnableMarketplace(geth, contracts, initialEth: 10.Eth(), initialTokens: sellerInitialBalance)); AssertBalance(geth, contracts, seller, Is.EqualTo(sellerInitialBalance)); seller.Marketplace.MakeStorageAvailable( @@ -72,10 +69,8 @@ namespace Tests.BasicTests var buyer = AddCodex(s => s .WithBootstrapNode(seller) - .EnableMarketplace(geth, contracts)); - geth.SendEth(seller, 10.Eth()); - contracts.MintTestTokens(geth, seller, buyerInitialBalance); - + .EnableMarketplace(geth, contracts, initialEth: 10.Eth(), initialTokens: buyerInitialBalance)); + AssertBalance(geth, contracts, buyer, Is.EqualTo(buyerInitialBalance)); var contentId = buyer.UploadFile(testFile);