/ ProjectPlugins / CodexClient / StoragePurchaseContract.cs
StoragePurchaseContract.cs
  1  using CodexClient.Hooks;
  2  using Logging;
  3  using Newtonsoft.Json;
  4  using Utils;
  5  
  6  namespace CodexClient
  7  {
  8      public interface IStoragePurchaseContract
  9      {
 10          string PurchaseId { get; }
 11          StoragePurchaseRequest Purchase { get; }
 12          ContentId ContentId { get; }
 13          StoragePurchase? GetStatus();
 14          void WaitForStorageContractSubmitted();
 15          void WaitForStorageContractExpired();
 16          void WaitForStorageContractStarted();
 17          void WaitForStorageContractFinished();
 18          void WaitForContractFailed(IMarketplaceConfigInput config);
 19      }
 20  
 21      public interface IMarketplaceConfigInput
 22      {
 23          int MaxNumberOfSlashes { get; }
 24          TimeSpan PeriodDuration { get; }
 25      }
 26  
 27      public class StoragePurchaseContract : IStoragePurchaseContract
 28      {
 29          private readonly ILog log;
 30          private readonly CodexAccess codexAccess;
 31          private readonly ICodexNodeHooks hooks;
 32          private readonly TimeSpan gracePeriod = TimeSpan.FromSeconds(60);
 33          private readonly DateTime contractPendingUtc = DateTime.UtcNow;
 34          private DateTime? contractSubmittedUtc = DateTime.UtcNow;
 35          private DateTime? contractStartedUtc;
 36          private DateTime? contractFinishedUtc;
 37          private StoragePurchaseState lastState = StoragePurchaseState.Unknown;
 38          private ContentId encodedContentId = new ContentId();
 39  
 40          public StoragePurchaseContract(ILog log, CodexAccess codexAccess, string purchaseId, StoragePurchaseRequest purchase, ICodexNodeHooks hooks)
 41          {
 42              this.log = log;
 43              this.codexAccess = codexAccess;
 44              PurchaseId = purchaseId;
 45              Purchase = purchase;
 46              this.hooks = hooks;
 47          }
 48  
 49          public string PurchaseId { get; }
 50          public StoragePurchaseRequest Purchase { get; }
 51          public ContentId ContentId
 52          {
 53              get
 54              {
 55                  if (string.IsNullOrEmpty(encodedContentId.Id)) GetStatus();
 56                  return encodedContentId;
 57              }
 58          }
 59  
 60          public TimeSpan? PendingToSubmitted => contractSubmittedUtc - contractPendingUtc;
 61          public TimeSpan? SubmittedToStarted => contractStartedUtc - contractSubmittedUtc;
 62          public TimeSpan? SubmittedToFinished => contractFinishedUtc - contractSubmittedUtc;
 63  
 64          public StoragePurchase? GetStatus()
 65          {
 66              var status = codexAccess.GetPurchaseStatus(PurchaseId);
 67              if (status != null)
 68              {
 69                  encodedContentId = new ContentId(status.Request.Content.Cid);
 70              }
 71              return status;
 72          }
 73  
 74          public void WaitForStorageContractSubmitted()
 75          {
 76              var timeout = Purchase.Expiry + gracePeriod;
 77              var raiseHook = lastState != StoragePurchaseState.Submitted;
 78              WaitForStorageContractState(timeout, StoragePurchaseState.Submitted, sleep: 200);
 79              contractSubmittedUtc = DateTime.UtcNow;
 80              if (raiseHook) hooks.OnStorageContractSubmitted(this);
 81              LogSubmittedDuration();
 82              AssertDuration(PendingToSubmitted, timeout, nameof(PendingToSubmitted));
 83          }
 84  
 85          public void WaitForStorageContractExpired()
 86          {
 87              var timeout = Purchase.Expiry + gracePeriod + gracePeriod;
 88              WaitForStorageContractState(timeout, StoragePurchaseState.Cancelled);
 89          }
 90  
 91          public void WaitForStorageContractStarted()
 92          {
 93              var timeout = Purchase.Expiry + gracePeriod;
 94  
 95              WaitForStorageContractState(timeout, StoragePurchaseState.Started);
 96              contractStartedUtc = DateTime.UtcNow;
 97              LogStartedDuration();
 98              AssertDuration(SubmittedToStarted, timeout, nameof(SubmittedToStarted));
 99          }
100  
101          public void WaitForStorageContractFinished()
102          {
103              if (!contractStartedUtc.HasValue)
104              {
105                  WaitForStorageContractStarted();
106              }
107              var currentContractTime = DateTime.UtcNow - contractSubmittedUtc!.Value;
108              var timeout = (Purchase.Duration - currentContractTime) + gracePeriod;
109              WaitForStorageContractState(timeout, StoragePurchaseState.Finished);
110              contractFinishedUtc = DateTime.UtcNow;
111              LogFinishedDuration();
112              AssertDuration(SubmittedToFinished, timeout, nameof(SubmittedToFinished));
113          }
114  
115          public void WaitForContractFailed(IMarketplaceConfigInput config)
116          {
117              if (!contractStartedUtc.HasValue)
118              {
119                  WaitForStorageContractStarted();
120              }
121              var currentContractTime = DateTime.UtcNow - contractSubmittedUtc!.Value;
122              var timeout = (Purchase.Duration - currentContractTime) + gracePeriod;
123              var minTimeout = TimeNeededToFailEnoughProofsToFreeASlot(config);
124  
125              if (timeout < minTimeout)
126              {
127                  throw new ArgumentOutOfRangeException(
128                      $"Test is misconfigured. Assuming a proof is required every period, it will take {Time.FormatDuration(minTimeout)} " +
129                      $"to fail enough proofs for a slot to be freed. But, the storage contract will complete in {Time.FormatDuration(timeout)}. " +
130                      $"Increase the duration."
131                  );
132              }
133  
134              WaitForStorageContractState(timeout, StoragePurchaseState.Errored);
135          }
136  
137          private TimeSpan TimeNeededToFailEnoughProofsToFreeASlot(IMarketplaceConfigInput config)
138          {
139              var numMissedProofsRequiredForFree = config.MaxNumberOfSlashes;
140              var timePerProof = config.PeriodDuration;
141              var result = timePerProof * (numMissedProofsRequiredForFree + 1);
142  
143              // Times 2!
144              // Because of pointer-downtime it's possible that some periods even though there's a probability of 100%
145              // will not require any proof. To be safe we take twice the required time.
146              return result * 2;
147          }
148  
149          private void WaitForStorageContractState(TimeSpan timeout, StoragePurchaseState desiredState, int sleep = 1000)
150          {
151              var waitStart = DateTime.UtcNow;
152  
153              Log($"Waiting for {Time.FormatDuration(timeout)} to reach state '{desiredState}'.");
154              while (lastState != desiredState)
155              {
156                  Thread.Sleep(sleep);
157  
158                  var purchaseStatus = codexAccess.GetPurchaseStatus(PurchaseId);
159                  var statusJson = JsonConvert.SerializeObject(purchaseStatus);
160                  if (purchaseStatus != null && purchaseStatus.State != lastState)
161                  {
162                      lastState = purchaseStatus.State;
163                      log.Debug("Purchase status: " + statusJson);
164                      hooks.OnStorageContractUpdated(purchaseStatus);
165                  }
166  
167                  if (desiredState != StoragePurchaseState.Errored && lastState == StoragePurchaseState.Errored)
168                  {
169                      FrameworkAssert.Fail("Contract errored: " + statusJson);
170                  }
171  
172                  if (DateTime.UtcNow - waitStart > timeout)
173                  {
174                      FrameworkAssert.Fail($"Contract did not reach '{desiredState}' within {Time.FormatDuration(timeout)} timeout. {statusJson}");
175                  }
176              }
177          }
178  
179          private void LogSubmittedDuration()
180          {
181              Log($"Pending to Submitted in {Time.FormatDuration(PendingToSubmitted)} " +
182                  $"( < {Time.FormatDuration(Purchase.Expiry + gracePeriod)})");
183          }
184  
185          private void LogStartedDuration()
186          {
187              Log($"Submitted to Started in {Time.FormatDuration(SubmittedToStarted)} " +
188                  $"( < {Time.FormatDuration(Purchase.Expiry + gracePeriod)})");
189          }
190  
191          private void LogFinishedDuration()
192          {
193              Log($"Submitted to Finished in {Time.FormatDuration(SubmittedToFinished)} " +
194                  $"( < {Time.FormatDuration(Purchase.Duration + gracePeriod)})");
195          }
196  
197          private void AssertDuration(TimeSpan? span, TimeSpan max, string message)
198          {
199              if (span == null) throw new ArgumentNullException(nameof(MarketplaceAccess) + ": " + message + " (IsNull)");
200              if (span.Value.TotalDays >= max.TotalSeconds)
201              {
202                  throw new Exception(nameof(MarketplaceAccess) +
203                      $": Duration out of range. Max: {Time.FormatDuration(max)} but was: {Time.FormatDuration(span.Value)} " +
204                      message);
205              }
206          }
207  
208          private void Log(string msg)
209          {
210              log.Log($"[{PurchaseId}] {msg}");
211          }
212      }
213  }