gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/contractor/maintenancechecks.go (about) 1 package contractor 2 3 import ( 4 "math" 5 "math/big" 6 7 "gitlab.com/NebulousLabs/errors" 8 "gitlab.com/SkynetLabs/skyd/skymodules" 9 "gitlab.com/SkynetLabs/skyd/skymodules/gouging" 10 "go.sia.tech/siad/modules" 11 "go.sia.tech/siad/persist" 12 "go.sia.tech/siad/types" 13 ) 14 15 type utilityUpdateStatus int 16 17 const ( 18 _ = iota 19 noUpdate 20 suggestedUtilityUpdate 21 necessaryUtilityUpdate 22 ) 23 24 // Merge merges two update statuses into one. Whichever has the higher priority 25 // wins. 26 func (us utilityUpdateStatus) Merge(us2 utilityUpdateStatus) utilityUpdateStatus { 27 if us > us2 { 28 return us 29 } 30 return us2 31 } 32 33 // managedCheckHostScore checks host scorebreakdown against minimum accepted 34 // scores. forceUpdate is true if the utility change must be taken. 35 func (c *Contractor) managedCheckHostScore(contract skymodules.RenterContract, sb skymodules.HostScoreBreakdown, minScoreGFR, minScoreGFU types.Currency) (skymodules.ContractUtility, utilityUpdateStatus) { 36 c.mu.Lock() 37 defer c.mu.Unlock() 38 39 u := contract.Utility 40 41 // Check whether the contract is a payment contract. Payment contracts 42 // cannot be marked !GFR for poor score. 43 var size uint64 44 if len(contract.Transaction.FileContractRevisions) > 0 { 45 size = contract.Transaction.FileContractRevisions[0].NewFileSize 46 } 47 paymentContract := !c.allowance.PaymentContractInitialFunding.IsZero() && size == 0 48 49 // Contract has no utility if the score is poor. Cannot be marked as bad if 50 // the contract is a payment contract. 51 badScore := !minScoreGFR.IsZero() && sb.Score.Cmp(minScoreGFR) < 0 52 if badScore && !paymentContract { 53 // Log if the utility has changed. 54 if u.GoodForUpload || u.GoodForRenew || u.GoodForRefresh { 55 c.staticLog.Printf("[CONTRACTUTILITY][%v] managedCheckHostScore failed, GFU: %v -> false, GFR: %v -> false, GFRef: %v -> false\n", contract.ID, u.GoodForUpload, u.GoodForRenew, u.GoodForRefresh) 56 c.staticLog.Println("Min Score:", minScoreGFR) 57 c.staticLog.Println("Score: ", sb.Score) 58 c.staticLog.Println("Age Adjustment: ", sb.AgeAdjustment) 59 c.staticLog.Println("Base Price Adjustment: ", sb.BasePriceAdjustment) 60 c.staticLog.Println("Burn Adjustment: ", sb.BurnAdjustment) 61 c.staticLog.Println("Collateral Adjustment: ", sb.CollateralAdjustment) 62 c.staticLog.Println("Duration Adjustment: ", sb.DurationAdjustment) 63 c.staticLog.Println("Interaction Adjustment:", sb.InteractionAdjustment) 64 c.staticLog.Println("Price Adjustment: ", sb.PriceAdjustment) 65 c.staticLog.Println("Storage Adjustment: ", sb.StorageRemainingAdjustment) 66 c.staticLog.Println("Uptime Adjustment: ", sb.UptimeAdjustment) 67 c.staticLog.Println("Version Adjustment: ", sb.VersionAdjustment) 68 } 69 u.GoodForUpload = false 70 u.GoodForRefresh = false 71 u.GoodForRenew = false 72 73 c.staticLog.Println("Adding contract utility update to churnLimiter queue") 74 return u, suggestedUtilityUpdate 75 } 76 77 // Contract should not be used for uploading if the score is poor. 78 if !minScoreGFU.IsZero() && sb.Score.Cmp(minScoreGFU) < 0 { 79 if u.GoodForUpload { 80 c.staticLog.Printf("[CONTRACTUTILITY][%v] managedCheckHostScore failed, poor score, GFU: true -> false\n", contract.ID) 81 c.staticLog.Println("Min Score:", minScoreGFU) 82 c.staticLog.Println("Score: ", sb.Score) 83 c.staticLog.Println("Age Adjustment: ", sb.AgeAdjustment) 84 c.staticLog.Println("Base Price Adjustment: ", sb.BasePriceAdjustment) 85 c.staticLog.Println("Burn Adjustment: ", sb.BurnAdjustment) 86 c.staticLog.Println("Collateral Adjustment: ", sb.CollateralAdjustment) 87 c.staticLog.Println("Duration Adjustment: ", sb.DurationAdjustment) 88 c.staticLog.Println("Interaction Adjustment:", sb.InteractionAdjustment) 89 c.staticLog.Println("Price Adjustment: ", sb.PriceAdjustment) 90 c.staticLog.Println("Storage Adjustment: ", sb.StorageRemainingAdjustment) 91 c.staticLog.Println("Uptime Adjustment: ", sb.UptimeAdjustment) 92 c.staticLog.Println("Version Adjustment: ", sb.VersionAdjustment) 93 } 94 u.GoodForUpload = false 95 return u, necessaryUtilityUpdate 96 } 97 return u, noUpdate 98 } 99 100 // managedUtilityChecks performs checks on a contract that 101 // might require marking the contract as !GFR and/or !GFU. 102 // Returns the new utility and corresponding update status. 103 func (c *Contractor) managedUtilityChecks(contract skymodules.RenterContract, host skymodules.DecoratedHostDBEntry, sb skymodules.HostScoreBreakdown, minScoreGFU, minScoreGFR types.Currency, endHeight types.BlockHeight) (_ skymodules.ContractUtility, uus utilityUpdateStatus) { 104 revision := contract.Transaction.FileContractRevisions[0] 105 c.mu.RLock() 106 allowance := c.allowance 107 blockHeight := c.blockHeight 108 renewWindow := c.allowance.RenewWindow 109 period := c.allowance.Period 110 _, renewed := c.renewedTo[contract.ID] 111 c.mu.RUnlock() 112 113 // Init uus to no update and the utility with the contract's utility. 114 // We assume that the contract is good when we start. 115 uus = noUpdate 116 oldUtility := contract.Utility 117 contract.Utility.GoodForRefresh = true 118 contract.Utility.GoodForRenew = true 119 contract.Utility.GoodForUpload = true 120 121 // A contract with a dead score should not be used for anything. 122 u, needsUpdate := deadScoreCheck(contract, sb.Score, c.staticLog) 123 uus = uus.Merge(needsUpdate) 124 contract.Utility = contract.Utility.Merge(u) 125 126 // A contract that has been renewed should be set to !GFU and !GFR. 127 u, needsUpdate = renewedCheck(contract, renewed, c.staticLog) 128 uus = uus.Merge(needsUpdate) 129 contract.Utility = contract.Utility.Merge(u) 130 131 u, needsUpdate = maxRevisionCheck(contract, revision.NewRevisionNumber, c.staticLog) 132 uus = uus.Merge(needsUpdate) 133 contract.Utility = contract.Utility.Merge(u) 134 135 u, needsUpdate = badContractCheck(contract, c.staticLog) 136 uus = uus.Merge(needsUpdate) 137 contract.Utility = contract.Utility.Merge(u) 138 139 u, needsUpdate = offlineCheck(contract, host.HostDBEntry, c.staticLog) 140 uus = uus.Merge(needsUpdate) 141 contract.Utility = contract.Utility.Merge(u) 142 143 u, needsUpdate = upForRenewalCheck(contract, renewWindow, blockHeight, c.staticLog) 144 uus = uus.Merge(needsUpdate) 145 contract.Utility = contract.Utility.Merge(u) 146 147 u, needsUpdate = sufficientFundsCheck(contract, host.HostDBEntry, period, c.staticLog) 148 uus = uus.Merge(needsUpdate) 149 contract.Utility = contract.Utility.Merge(u) 150 151 if !c.staticDeps.Disrupt("DisableUDSGougingCheck") { 152 u, needsUpdate = outOfStorageCheck(contract, blockHeight, c.staticLog) 153 uus = uus.Merge(needsUpdate) 154 contract.Utility = contract.Utility.Merge(u) 155 } 156 157 u, needsUpdate = udsGougingCheck(contract, allowance, host, revision.NewFileSize, endHeight, c.staticLog) 158 uus = uus.Merge(needsUpdate) 159 contract.Utility = contract.Utility.Merge(u) 160 161 u, needsUpdate = c.managedCheckHostScore(contract, sb, minScoreGFR, minScoreGFU) 162 uus = uus.Merge(needsUpdate) 163 contract.Utility = contract.Utility.Merge(u) 164 165 if c.staticDeps.Disrupt("ContractForceNotGoodForUpload") { 166 contract.Utility.GoodForUpload = false 167 uus = necessaryUtilityUpdate 168 } 169 170 // Log some info about changed utilities. 171 if oldUtility.BadContract != contract.Utility.BadContract { 172 c.staticLog.Printf("[CONTRACTUTILITY][%v] badContract %v -> %v", contract.ID, oldUtility.BadContract, contract.Utility.BadContract) 173 } 174 if oldUtility.GoodForRefresh != contract.Utility.GoodForRefresh { 175 c.staticLog.Printf("managedUtilityChecks: [%v] goodForRefresh %v -> %v", contract.ID, oldUtility.GoodForRefresh, contract.Utility.GoodForRefresh) 176 } 177 if oldUtility.GoodForRenew != contract.Utility.GoodForRenew { 178 c.staticLog.Printf("[CONTRACTUTILITY][%v] goodForRenew %v -> %v", contract.ID, oldUtility.GoodForRenew, contract.Utility.GoodForRenew) 179 } 180 if oldUtility.GoodForUpload != contract.Utility.GoodForUpload { 181 c.staticLog.Printf("[CONTRACTUTILITY][%v] goodForUpload %v -> %v", contract.ID, oldUtility.GoodForUpload, contract.Utility.GoodForUpload) 182 } 183 184 // If uus is no update but we still changed the utility that means the 185 // state of the contract improved. We then set uus to "suggestedUpdate". 186 if uus == noUpdate { 187 uus = suggestedUtilityUpdate 188 } 189 return contract.Utility, uus 190 } 191 192 // managedHostInHostDBCheck checks if the host is in the hostdb and not 193 // filtered. Returns true if a check fails and the utility returned must be 194 // used to update the contract state. 195 func (c *Contractor) managedHostInHostDBCheck(contract skymodules.RenterContract) (skymodules.DecoratedHostDBEntry, skymodules.ContractUtility, bool) { 196 u := contract.Utility 197 hostEntry, exists, err := c.staticHDB.Host(contract.HostPublicKey) 198 host := skymodules.DecoratedHostDBEntry{HostDBEntry: hostEntry, PriceTable: c.staticHDB.PriceTable(contract.HostPublicKey)} 199 200 // Contract has no utility if the host is not in the database. Or is 201 // filtered by the blacklist or whitelist. Or if there was an error 202 if !exists || host.Filtered || err != nil { 203 // Log if the utility has changed. 204 if u.GoodForUpload || u.GoodForRenew || u.GoodForRefresh { 205 c.staticLog.Printf("[CONTRACTUTILITY][%v] managedHostInHostDBCheck failed because found in hostDB: %v, or host is Filtered: %v, GFU: %v -> false, GFR: %v -> false, GFRef: %v -> false\n", contract.ID, exists, host.Filtered, u.GoodForUpload, u.GoodForRenew, u.GoodForRefresh) 206 } 207 u.GoodForUpload = false 208 u.GoodForRefresh = false 209 u.GoodForRenew = false 210 return host, u, true 211 } 212 213 // TODO: If the host is not in the hostdb, we need to do some sort of rescan 214 // to recover the host. The hostdb is not supposed to be dropping hosts that 215 // we have formed contracts with. We should do what we can to get the host 216 // back. 217 return host, u, false 218 } 219 220 // badContractCheck checks whether the contract has been marked as bad. If the 221 // contract has been marked as bad, GoodForUpload and GoodForRenew need to be 222 // set to false to prevent the renter from using this contract. 223 func badContractCheck(contract skymodules.RenterContract, logger *persist.Logger) (skymodules.ContractUtility, utilityUpdateStatus) { 224 u := contract.Utility 225 if u.BadContract { 226 // Log if the utility has changed. 227 if u.GoodForUpload || u.GoodForRenew || u.GoodForRefresh { 228 logger.Printf("[CONTRACTUTILITY][%v] badContractCheck failed, GFU: %v -> false, GFR: %v -> false, GFRef: %v -> false\n", contract.ID, u.GoodForUpload, u.GoodForRenew, u.GoodForRefresh) 229 } 230 u.GoodForUpload = false 231 u.GoodForRefresh = false 232 u.GoodForRenew = false 233 return u, necessaryUtilityUpdate 234 } 235 return u, noUpdate 236 } 237 238 // maxRevisionCheck will return a locked utility if the contract has reached its 239 // max revision. 240 func maxRevisionCheck(contract skymodules.RenterContract, revisionNumber uint64, logger *persist.Logger) (skymodules.ContractUtility, utilityUpdateStatus) { 241 u := contract.Utility 242 if revisionNumber == math.MaxUint64 { 243 // Log if the utility has changed. 244 if u.GoodForUpload || u.GoodForRenew || u.GoodForRefresh { 245 logger.Printf("[CONTRACTUTILITY][%v] maxRevisionCheck failed, GFU: %v -> false, GFR: %v -> false, GFRef: %v -> false\n", contract.ID, u.GoodForUpload, u.GoodForRenew, u.GoodForRefresh) 246 } 247 u.GoodForUpload = false 248 u.GoodForRefresh = false 249 u.GoodForRenew = false 250 u.Locked = true 251 return u, necessaryUtilityUpdate 252 } 253 return u, noUpdate 254 } 255 256 // offLineCheck checks if the host for this contract is offline. 257 // Returns true if a check fails and the utility returned must be used to update 258 // the contract state. 259 func offlineCheck(contract skymodules.RenterContract, host skymodules.HostDBEntry, log *persist.Logger) (skymodules.ContractUtility, utilityUpdateStatus) { 260 u := contract.Utility 261 // Contract has no utility if the host is offline. 262 if isOffline(host) { 263 // Log if the utility has changed. 264 if u.GoodForUpload || u.GoodForRenew || u.GoodForRefresh { 265 log.Printf("[CONTRACTUTILITY][%v] offlineCheck failed, GFU: %v -> false, GFR: %v -> false, GFRef: %v -> false\n", contract.ID, u.GoodForUpload, u.GoodForRenew, u.GoodForRefresh) 266 } 267 u.GoodForUpload = false 268 u.GoodForRefresh = false 269 u.GoodForRenew = false 270 return u, necessaryUtilityUpdate 271 } 272 return u, noUpdate 273 } 274 275 // outOfStorageCheck checks if the host is running out of storage. 276 // Returns true if a check fails and the utility returned must be used to update 277 // the contract state. 278 func outOfStorageCheck(contract skymodules.RenterContract, blockHeight types.BlockHeight, log *persist.Logger) (skymodules.ContractUtility, utilityUpdateStatus) { 279 u := contract.Utility 280 // If LastOOSErr has never been set, return false. 281 if u.LastOOSErr == 0 { 282 return u, noUpdate 283 } 284 // Contract should not be used for uploading if the host is out of storage. 285 if blockHeight-u.LastOOSErr <= oosRetryInterval { 286 if u.GoodForUpload { 287 log.Printf("[CONTRACTUTILITY][%v] outOfStorageCheck failed, GFU: true -> false\n", contract.ID) 288 } 289 u.GoodForUpload = false 290 return u, necessaryUtilityUpdate 291 } 292 return u, noUpdate 293 } 294 295 // deadScoreCheck will return a contract with no utility and a required update 296 // if the contract has a score <= 1. 297 func deadScoreCheck(contract skymodules.RenterContract, score types.Currency, logger *persist.Logger) (skymodules.ContractUtility, utilityUpdateStatus) { 298 u := contract.Utility 299 if score.Cmp(types.NewCurrency64(1)) <= 0 { 300 // Log if the utility has changed. 301 if u.GoodForUpload || u.GoodForRenew || u.GoodForRefresh { 302 logger.Printf("[CONTRACTUTILITY][%v] deadScoreCheck failed, GFU: %v -> false, GFR: %v -> false, GFRef: %v -> false\n", contract.ID, u.GoodForUpload, u.GoodForRenew, u.GoodForRefresh) 303 } 304 u.GoodForRefresh = false 305 u.GoodForUpload = false 306 u.GoodForRenew = false 307 return u, necessaryUtilityUpdate 308 } 309 return u, noUpdate 310 } 311 312 // renewedCheck will return a contract with no utility and a required update if 313 // the contract has been renewed, no changes otherwise. 314 func renewedCheck(contract skymodules.RenterContract, renewed bool, logger *persist.Logger) (skymodules.ContractUtility, utilityUpdateStatus) { 315 u := contract.Utility 316 if renewed { 317 // Log if the utility has changed. 318 if u.GoodForUpload || u.GoodForRenew || u.GoodForRefresh { 319 logger.Printf("[CONTRACTUTILITY][%v] renewedCheck failed, GFU: %v -> false, GFR: %v -> false, GFRef: %v -> false\n", contract.ID, u.GoodForUpload, u.GoodForRenew, u.GoodForRefresh) 320 } 321 u.GoodForUpload = false 322 u.GoodForRefresh = false 323 u.GoodForRenew = false 324 return u, necessaryUtilityUpdate 325 } 326 return u, noUpdate 327 } 328 329 // udsGougingCheck makes sure the host is not price gouging. UDS stands for 330 // upload, download and sector access. 331 // NOTE: The contractEndHeight argument is supposed to be the endHeight as 332 // perceived by the contractor when forming new contracts or renewing them. Not 333 // the actual endheight of the contract itself. 334 func udsGougingCheck(contract skymodules.RenterContract, allowance skymodules.Allowance, host skymodules.DecoratedHostDBEntry, contractSize uint64, contractEndHeight types.BlockHeight, logger *persist.Logger) (skymodules.ContractUtility, utilityUpdateStatus) { 335 u := contract.Utility 336 pt := host.PriceTable 337 338 // use both gouging checks, download gouging is implicitly checked by both 339 // upload gouging checks 340 var err1 error 341 if pt != nil { 342 err1 = gouging.CheckUpload(allowance, *pt) 343 } 344 err2 := gouging.CheckUploadHES(allowance, pt, host.HostExternalSettings, true) 345 if err := errors.Compose(err1, err2); err != nil { 346 wasGFU := u.GoodForUpload 347 wasGFR := u.GoodForRenew 348 wasGFRef := u.GoodForRefresh 349 350 markBadForRenew := contractSize > 0 351 markBadForRefresh := markBadForRenew && contract.EndHeight < contractEndHeight 352 353 // If the contract contains data don't renew to make sure we 354 // don't pay for its storage. 355 u.GoodForUpload = false 356 if markBadForRenew { 357 u.GoodForRenew = false 358 } 359 360 // If the contract contains data and a renew of the contract 361 // would lead to its endHeight being extended, don't refresh it 362 // anymore. 363 if markBadForRefresh { 364 u.GoodForRefresh = false 365 } 366 367 // Log if the utility has changed 368 if wasGFU || (wasGFR && markBadForRenew) || (wasGFRef && markBadForRefresh) { 369 logger.Printf("[CONTRACTUTILITY][%v] gouging check failed, err: %v, GFU: %v -> %v, GFR: %v -> %v, GFRef: %v -> %v\n", contract.ID, err, wasGFU, u.GoodForUpload, wasGFR, u.GoodForRenew, wasGFRef, u.GoodForRefresh) 370 } 371 return u, necessaryUtilityUpdate 372 } 373 return u, noUpdate 374 } 375 376 // sufficientFundsCheck checks if there are enough funds left in the contract 377 // for uploads. 378 // Returns true if a check fails and the utility returned must be used to update 379 // the contract state. 380 func sufficientFundsCheck(contract skymodules.RenterContract, host skymodules.HostDBEntry, period types.BlockHeight, log *persist.Logger) (skymodules.ContractUtility, utilityUpdateStatus) { 381 u := contract.Utility 382 383 // Contract should not be used for uploading if the contract does 384 // not have enough money remaining to perform the upload. 385 blockBytes := types.NewCurrency64(modules.SectorSize * uint64(period)) 386 sectorStoragePrice := host.StoragePrice.Mul(blockBytes) 387 sectorUploadBandwidthPrice := host.UploadBandwidthPrice.Mul64(modules.SectorSize) 388 sectorDownloadBandwidthPrice := host.DownloadBandwidthPrice.Mul64(modules.SectorSize) 389 sectorBandwidthPrice := sectorUploadBandwidthPrice.Add(sectorDownloadBandwidthPrice) 390 sectorPrice := sectorStoragePrice.Add(sectorBandwidthPrice) 391 percentRemaining, _ := big.NewRat(0, 1).SetFrac(contract.RenterFunds.Big(), contract.TotalCost.Big()).Float64() 392 if contract.RenterFunds.Cmp(sectorPrice.Mul64(3)) < 0 || percentRemaining < MinContractFundUploadThreshold { 393 if u.GoodForUpload { 394 log.Printf("[CONTRACTUTILITY][%v] sufficientFundsCheck failed, insufficient funds, %v | %v, GFU: true -> false\n", contract.ID, contract.RenterFunds.Cmp(sectorPrice.Mul64(3)) < 0, percentRemaining) 395 } 396 u.GoodForUpload = false 397 return u, necessaryUtilityUpdate 398 } 399 return u, noUpdate 400 } 401 402 // upForRenewalCheck checks if this contract is up for renewal. 403 // Returns true if a check fails and the utility returned must be used to update 404 // the contract state. 405 func upForRenewalCheck(contract skymodules.RenterContract, renewWindow, blockHeight types.BlockHeight, log *persist.Logger) (skymodules.ContractUtility, utilityUpdateStatus) { 406 u := contract.Utility 407 // Contract should not be used for uploading if it's halfway through the 408 // renew window. That way we don't lose all upload contracts as soon as 409 // we hit the renew window and give them some time to be renewed while 410 // still uploading. If uploading blocks renews for half a window, 411 // uploading will be prevented and the contract will have the remaining 412 // window to update. 413 if blockHeight+renewWindow/2 >= contract.EndHeight { 414 if u.GoodForUpload { 415 log.Printf("[CONTRACTUTILITY][%v] upForRenewalCheck failed, it is time to renew the contract, GFU: true -> false\n", contract.ID) 416 } 417 u.GoodForUpload = false 418 return u, necessaryUtilityUpdate 419 } 420 return u, noUpdate 421 }