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 }