gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/hostdb/hostweight.go (about) 1 package hostdb 2 3 import ( 4 "fmt" 5 "math" 6 "strings" 7 "time" 8 9 "gitlab.com/NebulousLabs/errors" 10 11 "gitlab.com/SkynetLabs/skyd/build" 12 "gitlab.com/SkynetLabs/skyd/skymodules" 13 "gitlab.com/SkynetLabs/skyd/skymodules/renter/hostdb/hosttree" 14 "go.sia.tech/siad/types" 15 ) 16 17 const ( 18 // collateralExponentiation is the power to which we raise the weight 19 // during collateral adjustment when the collateral is large. This sublinear 20 // number ensures that there is not an overpreference on collateral when 21 // collateral is large relative to the size of the allowance. 22 collateralExponentiationLarge = 0.5 23 24 // collateralExponentiationSmall is the power to which we raise the weight 25 // during collateral adjustment when the collateral is small. A large number 26 // ensures a heavy focus on collateral when distinguishing between hosts 27 // that have a very small amount of collateral provided compared to the size 28 // of the allowance. 29 // 30 // For safety, this number needs to be larger than priceExponentiationSmall. 31 collateralExponentiationSmall = 4 32 33 // collateralFloor is a part of the equation for determining the collateral 34 // cutoff between large and small collateral. The equation figures out how 35 // much collateral is expected given the allowance, and then divided by 36 // 'collateralFloor' so that the cutoff for how much collateral counts as 37 // 'not much' is reasonably below what we are actually expecting from the 38 // host. 39 40 // collateralFloor determines how much lower than the expected collateral 41 // the host can provide before switching to a different scoring strategy. A 42 // collateral floor of 0.5 means that once the host is offering a collateral 43 // that is more than 50% of what the renter would expect given the amount of 44 // storage being used, the host switching to a scoring strategy which less 45 // intensly favors adding more collateral. As long as the host has provided 46 // sufficient skin-in-the-game, enormous amounts of extra collateral are 47 // less important. 48 // 49 // The collateralFloor is set relative to the price floor because generally 50 // we look for the collateral to be about 2x the price. 51 collateralFloor = priceFloor * 2 52 53 // interactionExponentiation determines how heavily we penalize hosts for 54 // having poor interactions - disconnecting, RPCs with errors, etc. The 55 // exponentiation is very high because the renter will already intentionally 56 // avoid hosts that do not have many successful interactions, meaning that 57 // the bad points do not rack up very quickly. 58 interactionExponentiation = 10 59 60 // priceExponentiationLarge is the number of times that the weight is 61 // divided by the price when the price is large relative to the allowance. 62 // The exponentiation is a lot higher because we care greatly about high 63 // priced hosts. 64 priceExponentiationLarge = 5 65 66 // priceExponentiationSmall is the number of times that the weight is 67 // divided by the price when the price is small relative to the allowance. 68 // The exponentiation is lower because we do not care about saving 69 // substantial amounts of money when the price is low. 70 priceExponentiationSmall = 0.75 71 72 // priceFloor determines how much cheaper than the expected allowance the 73 // host can be before switching to a different scoring strategy for the 74 // score. A price floor of 0.2 means that once the host is less than 20% of 75 // the expected price for that amount of resources (using the allowance as a 76 // guide), instead of using priceExponentiationLarge to reward decreasing 77 // prices, we use priceExponentiationSmall to reward decreasing prices. This 78 // reduced steepness reflects the reality that getting 99.9% off is not all 79 // that different from getting 80% off - both feel like an amazing deal. 80 // 81 // This is necessary to prevent exploits where a host gets an unreasonable 82 // score by putting it's price way too low. 83 priceFloor = 0.1 84 ) 85 86 // basePriceAdjustments will adjust the weight of the entry according to the prices 87 // that it has set for BaseRPCPrice and SectorAccessPrice 88 func (hdb *HostDB) basePriceAdjustments(entry skymodules.HostDBEntry) float64 { 89 // Check for BaseRPCPrice violations 90 maxBaseRPCPrice := entry.MaxBaseRPCPrice() 91 baseRPCPrice := entry.HostExternalSettings.BaseRPCPrice 92 if baseRPCPrice.Cmp(maxBaseRPCPrice) > 0 { 93 hdb.staticLog.Debugf("Host getting 0 score for BaseRPCPrice: Host %v, BaseRPCPrice %v, MaxBaseRPCPrice %v", entry.PublicKey.String(), baseRPCPrice.HumanString(), maxBaseRPCPrice.HumanString()) 94 return math.SmallestNonzeroFloat64 95 } 96 97 // Check for SectorAccessPrice violations 98 maxSectorAccessPrice := entry.MaxSectorAccessPrice() 99 sectorAccessPrice := entry.HostExternalSettings.SectorAccessPrice 100 if sectorAccessPrice.Cmp(maxSectorAccessPrice) > 0 { 101 hdb.staticLog.Debugf("Host getting 0 score for SectorAccessPrice: Host %v, SectorAccessPrice %v, MaxSectorAccessPrice %v", entry.PublicKey.String(), sectorAccessPrice.HumanString(), maxSectorAccessPrice.HumanString()) 102 return math.SmallestNonzeroFloat64 103 } 104 105 return 1 106 } 107 108 // collateralAdjustments improves the host's weight according to the amount of 109 // collateral that they have provided. 110 func (hdb *HostDB) collateralAdjustments(entry skymodules.HostDBEntry, allowance skymodules.Allowance) float64 { 111 // Ensure that all values will avoid divide by zero errors. 112 if allowance.Hosts == 0 { 113 allowance.Hosts = 1 114 } 115 if allowance.Period == 0 { 116 allowance.Period = 1 117 } 118 if allowance.ExpectedStorage == 0 { 119 allowance.ExpectedStorage = 1 120 } 121 if allowance.ExpectedUpload == 0 { 122 allowance.ExpectedUpload = 1 123 } 124 if allowance.ExpectedDownload == 0 { 125 allowance.ExpectedDownload = 1 126 } 127 if allowance.ExpectedRedundancy == 0 { 128 allowance.ExpectedRedundancy = 1 129 } 130 131 // Convert each element of the allowance into a number of resources that we 132 // expect to use in this contract. 133 contractExpectedFunds := allowance.Funds.Div64(allowance.Hosts) 134 contractExpectedStorage := uint64(float64(allowance.ExpectedStorage) * allowance.ExpectedRedundancy / float64(allowance.Hosts)) 135 contractExpectedStorageTime := types.NewCurrency64(contractExpectedStorage).Mul64(uint64(allowance.Period)) 136 137 // Ensure that the allowance and expected storage will not brush up against 138 // the max collateral. If the allowance comes within half of the max 139 // collateral, cap the collateral that we use during adjustments based on 140 // the max collateral instead of the per-byte collateral. 141 // 142 // The purpose of this code is to make sure that the host actually has a 143 // high enough MaxCollateral to cover all of the data that we intend to 144 // store with the host at the collateral price that the host is advertising. 145 // We add a 2x buffer to account for the fact that the renter may end up 146 // storing extra data on this host. 147 hostCollateral := entry.Collateral.Mul(contractExpectedStorageTime) 148 possibleCollateral := entry.MaxCollateral.Div64(2) 149 if possibleCollateral.Cmp(hostCollateral) < 0 { 150 hostCollateral = possibleCollateral 151 } 152 153 // Determine the cutoff for the difference between small collateral and 154 // large collateral. The cutoff is used to create a step function in the 155 // collateral scoring where decreasing collateral results in much higher 156 // penalties below a certain threshold. 157 // 158 // This threshold is attempting to be the threshold where the amount of 159 // money becomes insignificant. A collateral that is 10x higher than the 160 // price is not interesting, compelling, nor a sign of reliability if the 161 // price and collateral are both effectively zero. 162 // 163 // TODO: This method has no way to account for bandwidth heavy vs. storage 164 // heavy hosts, nor did we give the user any way to configure a situation 165 // where hosts aren't needed to be nearly as reliable. 166 cutoff := contractExpectedFunds.MulFloat(collateralFloor) 167 168 // Get the ratio between the cutoff and the actual collateral so we can 169 // award the bonus for having a large collateral. 170 collateral64, _ := hostCollateral.Float64() 171 cutoff64, _ := cutoff.Float64() 172 // If the hostCollateral is less than the cutoff, set the cutoff equal to 173 // the collateral so that the ratio has a minimum of 1, and also so that 174 // the smallWeight is computed based on the actual collateral instead of 175 // just the cutoff. 176 if collateral64 < cutoff64 { 177 cutoff64 = collateral64 178 } 179 // One last check for safety before grabbing the ratio. This ensures that 180 // the ratio is never less than one, which is critical to getting a coherent 181 // large weight - large weight should never be below one. 182 if collateral64 < 1 { 183 collateral64 = 1 184 } 185 if cutoff64 < 1 { 186 cutoff64 = 1 187 } 188 ratio := collateral64 / cutoff64 189 190 // Use the cutoff to determine the score based on the small exponentiation 191 // factor (which has a high exponentiation), and then use the ratio between 192 // the two to determine the bonus gained from having a high collateral. 193 smallWeight := math.Pow(cutoff64, collateralExponentiationSmall) 194 largeWeight := math.Pow(ratio, collateralExponentiationLarge) 195 return smallWeight * largeWeight 196 } 197 198 // acceptContractAdjustments checks that a host which doesn't accept contracts 199 // will receive the worst score possible until it enables accepting contracts 200 // again. 201 func (hdb *HostDB) acceptContractAdjustments(entry skymodules.HostDBEntry) float64 { 202 if !entry.AcceptingContracts { 203 return math.SmallestNonzeroFloat64 204 } 205 return 1 206 } 207 208 // durationAdjustments checks that the host has a maxduration which is larger 209 // than the period of the allowance. The host's score is heavily minimized if 210 // not. 211 func (hdb *HostDB) durationAdjustments(entry skymodules.HostDBEntry, allowance skymodules.Allowance) float64 { 212 if entry.MaxDuration < allowance.Period+allowance.RenewWindow { 213 return math.SmallestNonzeroFloat64 214 } 215 return 1 216 } 217 218 // interactionAdjustments determine the penalty to be applied to a host for the 219 // historic and current interactions with that host. This function focuses on 220 // historic interactions and ignores recent interactions. 221 func (hdb *HostDB) interactionAdjustments(entry skymodules.HostDBEntry) float64 { 222 // Give the host a baseline of 30 successful interactions and 1 failed 223 // interaction. This gives the host a baseline if we've had few 224 // interactions with them. The 1 failed interaction will become 225 // irrelevant after sufficient interactions with the host. 226 hsi := entry.HistoricSuccessfulInteractions + 30 227 hfi := entry.HistoricFailedInteractions + 1 228 229 // Determine the intraction ratio based off of the historic interactions. 230 ratio := float64(hsi) / float64(hsi+hfi) 231 return math.Pow(ratio, interactionExponentiation) 232 } 233 234 // priceAdjustments will adjust the weight of the entry according to the prices 235 // that it has set. 236 // 237 // REMINDER: The allowance contains an absolute number of bytes for expected 238 // storage on a per-renter basis that doesn't account for redundancy. This value 239 // needs to be adjusted to a per-contract basis that accounts for redundancy. 240 // The upload and download values also do not account for redundancy, and they 241 // are on a per-block basis, meaning you need to multiply be the allowance 242 // period when working with these values. 243 func (hdb *HostDB) priceAdjustments(entry skymodules.HostDBEntry, allowance skymodules.Allowance, txnFees types.Currency) float64 { 244 // Divide by zero mitigation. 245 if allowance.Hosts == 0 { 246 allowance.Hosts = 1 247 } 248 if allowance.Period == 0 { 249 allowance.Period = 1 250 } 251 if allowance.ExpectedStorage == 0 { 252 allowance.ExpectedStorage = 1 253 } 254 if allowance.ExpectedUpload == 0 { 255 allowance.ExpectedUpload = 1 256 } 257 if allowance.ExpectedDownload == 0 { 258 allowance.ExpectedDownload = 1 259 } 260 if allowance.ExpectedRedundancy == 0 { 261 allowance.ExpectedRedundancy = 1 262 } 263 264 // Check max prices first. 265 if !allowance.MaxRPCPrice.IsZero() && entry.BaseRPCPrice.Cmp(allowance.MaxRPCPrice) > 0 { 266 return math.SmallestNonzeroFloat64 267 } 268 if !allowance.MaxContractPrice.IsZero() && entry.ContractPrice.Cmp(allowance.MaxContractPrice) > 0 { 269 return math.SmallestNonzeroFloat64 270 } 271 if !allowance.MaxSectorAccessPrice.IsZero() && entry.SectorAccessPrice.Cmp(allowance.MaxSectorAccessPrice) > 0 { 272 return math.SmallestNonzeroFloat64 273 } 274 if !allowance.MaxDownloadBandwidthPrice.IsZero() && entry.DownloadBandwidthPrice.Cmp(allowance.MaxDownloadBandwidthPrice) > 0 { 275 return math.SmallestNonzeroFloat64 276 } 277 if !allowance.MaxUploadBandwidthPrice.IsZero() && entry.UploadBandwidthPrice.Cmp(allowance.MaxUploadBandwidthPrice) > 0 { 278 return math.SmallestNonzeroFloat64 279 } 280 281 // Convert each element of the allowance into a number of resources that we 282 // expect to use in this contract. 283 contractExpectedDownload := types.NewCurrency64(allowance.ExpectedDownload).Mul64(uint64(allowance.Period)).Div64(allowance.Hosts) 284 contractExpectedFunds := allowance.Funds.Div64(allowance.Hosts) 285 contractExpectedStorage := uint64(float64(allowance.ExpectedStorage) * allowance.ExpectedRedundancy / float64(allowance.Hosts)) 286 contractExpectedStorageTime := types.NewCurrency64(contractExpectedStorage).Mul64(uint64(allowance.Period)) 287 contractExpectedUpload := types.NewCurrency64(allowance.ExpectedUpload).Mul64(uint64(allowance.Period)).MulFloat(allowance.ExpectedRedundancy).Div64(allowance.Hosts) 288 289 // Get the extra costs expected for downloads and uploads from the sector access 290 // price and base price. 291 extraCostsPerRPC := entry.BaseRPCPrice.Add(entry.SectorAccessPrice) 292 293 contractExpectedDownloadRPCs := contractExpectedDownload.Div64(skymodules.StreamDownloadSize) 294 extraDownloadRPCCost := contractExpectedDownloadRPCs.Mul(extraCostsPerRPC) 295 296 contractExpectedUploadRPCs := contractExpectedUpload.Div64(skymodules.StreamUploadSize) 297 extraUploadRPCCost := contractExpectedUploadRPCs.Mul(extraCostsPerRPC) 298 299 // Calculate the hostCollateral the renter would expect the host to put 300 // into a contract. 301 // 302 contractTxnFees := txnFees.Mul64(skymodules.EstimatedFileContractTransactionSetSize) 303 _, _, hostCollateral, err := skymodules.RenterPayoutsPreTax(entry, contractExpectedFunds, contractTxnFees, types.ZeroCurrency, types.ZeroCurrency, allowance.Period, contractExpectedStorage) 304 if err != nil { 305 // Errors containing 'exceeds funding' are not logged. All it means is 306 // that the contract price (or some other price) of the host is too high 307 // for us to be able to form a contract with it, so this host is 308 // strictly not valuable given our allowance and it's pricing. This is 309 // common enough and expected enough that we don't need to log when it 310 // happens. 311 if !strings.Contains(err.Error(), "exceeds funding") { 312 info := fmt.Sprintf("Error while estimating collateral for host: Host %v, ContractPrice %v, TxnFees %v, Funds %v", entry.PublicKey.String(), entry.ContractPrice.HumanString(), txnFees.HumanString(), allowance.Funds.HumanString()) 313 hdb.staticLog.Debugln(errors.AddContext(err, info)) 314 } 315 return math.SmallestNonzeroFloat64 316 } 317 318 // Determine the pricing for each type of resource in the contract. We have 319 // already converted the resources into absolute terms for this contract. 320 // 321 // The contract price and transaction fees get doubled because we expect 322 // that there will be on average one early renewal per contract, due to 323 // spending all of the contract's money. 324 contractPrice := entry.ContractPrice.Add(txnFees).Mul64(2) 325 downloadPrice := entry.DownloadBandwidthPrice.Mul(contractExpectedDownload).Add(extraDownloadRPCCost) 326 storagePrice := entry.StoragePrice.Mul(contractExpectedStorageTime) 327 uploadPrice := entry.UploadBandwidthPrice.Mul(contractExpectedUpload).Add(extraUploadRPCCost) 328 siafundFee := contractPrice.Add(hostCollateral).Add(downloadPrice).Add(storagePrice).Add(uploadPrice).MulTax() 329 totalPrice := contractPrice.Add(downloadPrice).Add(storagePrice).Add(uploadPrice).Add(siafundFee) 330 331 // Determine a cutoff for whether the total price is considered a high price 332 // or a low price. This cutoff attempts to determine where the price becomes 333 // insignificant. 334 cutoff := contractExpectedFunds.MulFloat(priceFloor) 335 336 // Convert the price and cutoff to floats. 337 price64, _ := totalPrice.Float64() 338 cutoff64, _ := cutoff.Float64() 339 // If the total price is less than the cutoff, set the cutoff equal to the 340 // price. This ensures that the ratio (totalPrice / cutoff) can never be 341 // less than 1. 342 if price64 < cutoff64 { 343 cutoff64 = price64 344 } 345 346 // Check for less-than-one. 347 if price64 < 1 { 348 price64 = 1 349 } 350 if cutoff64 < 1 { 351 cutoff64 = 1 352 } 353 // Perform this check one more time after all of the conversions, just in 354 // case there was some sort of rounding error. 355 if price64 < cutoff64 { 356 cutoff64 = price64 357 } 358 ratio := price64 / cutoff64 359 360 smallWeight := math.Pow(cutoff64, priceExponentiationSmall) 361 largeWeight := math.Pow(ratio, priceExponentiationLarge) 362 363 return 1 / (smallWeight * largeWeight) 364 } 365 366 // storageRemainingAdjustments adjusts the weight of the entry according to how 367 // much storage it has remaining. 368 func (hdb *HostDB) storageRemainingAdjustments(entry skymodules.HostDBEntry, allowance skymodules.Allowance) float64 { 369 // Determine how much data the renter is storing on this host. 370 var storedData float64 371 if ci, exists := hdb.knownContracts[entry.PublicKey.String()]; exists { 372 storedData = float64(ci.StoredData) 373 } 374 375 // Divide by zero mitigation. 376 if allowance.Hosts == 0 { 377 allowance.Hosts = 1 378 } 379 380 // idealDataPerHost is the amount of data that we would have to put on each 381 // host assuming that our storage requirements were spread evenly across 382 // every single host. 383 idealDataPerHost := float64(allowance.ExpectedStorage) * allowance.ExpectedRedundancy / float64(allowance.Hosts) 384 // allocationPerHost is the amount of data that we would like to be able to 385 // put on each host, because data is not always spread evenly across the 386 // hosts during upload. Slower hosts may get very little data, more 387 // expensive hosts may get very little data, and other factors can skew the 388 // distribution. allocationPerHost takes into account the skew and tries to 389 // ensure that there's enough allocation per host to accommodate for a skew. 390 allocationPerHost := idealDataPerHost * storageSkewMultiplier 391 // hostExpectedStorage is the amount of storage that we expect to be able to 392 // store on this host overall, which should include the stored data that is 393 // already on the host. 394 hostExpectedStorage := (float64(entry.RemainingStorage) * storageCompetitionFactor) + storedData 395 // The score for the host is the square of the amount of storage we 396 // expected divided by the amount of storage we want. If we expect to be 397 // able to store more data on the host than we need to allocate, the host 398 // gets full score for storage. 399 if hostExpectedStorage >= allocationPerHost { 400 return 1 401 } 402 // Otherwise, the score of the host is the fraction of the data we expect 403 // raised to the storage penalty exponentiation. 404 storageRatio := hostExpectedStorage / allocationPerHost 405 result := math.Pow(storageRatio, storagePenaltyExponentitaion) 406 if result == 0 { 407 return 1 408 } 409 return result 410 } 411 412 // versionAdjustments will adjust the weight of the entry according to the siad 413 // version reported by the host. 414 func versionAdjustments(entry skymodules.HostDBEntry) float64 { 415 base := float64(1) 416 417 // This needs to give a very tiny penalty to the current version. The reason 418 // we give the current version a very tiny penalty is so that the test suite 419 // complains if we forget to update this file when we bump the version next 420 // time. The value compared against must be higher than the current version. 421 if build.VersionCmp(entry.Version, "1.5.9") < 0 { 422 base = base * 0.99999 // Safety value to make sure we update the version penalties every time we update the host. 423 } 424 425 // This needs to be "less than the current version" - anything less than the current version should get a penalty. 426 if build.VersionCmp(entry.Version, "1.5.8") < 0 { 427 base = base * 0.90 // Small penalty 428 } 429 if build.VersionCmp(entry.Version, "1.5.7") < 0 { 430 base = base * 0.90 // Small penalty 431 } 432 433 // Heavy penalty for hosts before the maxRPCLen encoding fix. 434 if build.VersionCmp(entry.Version, "1.5.6") < 0 { 435 base = math.SmallestNonzeroFloat64 436 } 437 return base 438 } 439 440 // lifetimeAdjustments will adjust the weight of the host according to the total 441 // amount of time that has passed since the host's original announcement. 442 func (hdb *HostDB) lifetimeAdjustments(entry skymodules.HostDBEntry) float64 { 443 base := float64(1) 444 if hdb.blockHeight >= entry.FirstSeen { 445 age := hdb.blockHeight - entry.FirstSeen 446 if age < 12000 { 447 base = base * 2 / 3 // 1.5x total 448 } 449 if age < 6000 { 450 base = base / 2 // 3x total 451 } 452 if age < 4000 { 453 base = base / 2 // 6x total 454 } 455 if age < 2000 { 456 base = base / 2 // 12x total 457 } 458 if age < 1000 { 459 base = base / 3 // 36x total 460 } 461 if age < 576 { 462 base = base / 3 // 108x total 463 } 464 if age < 288 { 465 base = base / 3 // 324x total 466 } 467 if age < 144 { 468 base = base / 3 // 972x total 469 } 470 } 471 return base 472 } 473 474 // uptimeAdjustments penalizes the host for having poor uptime, and for being 475 // offline. 476 // 477 // CAUTION: The function 'updateEntry' will manually fill out two scans for a 478 // new host to give the host some initial uptime or downtime. Modification of 479 // this function needs to be made paying attention to the structure of that 480 // function. 481 // 482 // TODO: This function doesn't correctly handle situations where the user's 483 // clock goes back in time. If the user adjusts their system clock to be in the 484 // past, we'll get timestamping that's out of order, and this will cause erratic 485 // / improper / untested behavior. 486 func (hdb *HostDB) uptimeAdjustments(entry skymodules.HostDBEntry) float64 { 487 // Special case: if we have scanned the host twice or fewer, don't perform 488 // uptime math. 489 if len(entry.ScanHistory) == 0 { 490 return 0.25 491 } 492 if len(entry.ScanHistory) == 1 { 493 if entry.ScanHistory[0].Success { 494 return 0.75 495 } 496 return 0.25 497 } 498 if len(entry.ScanHistory) == 2 { 499 if entry.ScanHistory[0].Success && entry.ScanHistory[1].Success { 500 return 0.85 501 } 502 if entry.ScanHistory[0].Success || entry.ScanHistory[1].Success { 503 return 0.50 504 } 505 return 0.05 506 } 507 508 // Compute the total measured uptime and total measured downtime for this 509 // host. 510 downtime := entry.HistoricDowntime 511 uptime := entry.HistoricUptime 512 recentTime := entry.ScanHistory[0].Timestamp 513 recentSuccess := entry.ScanHistory[0].Success 514 for _, scan := range entry.ScanHistory[1:] { 515 if recentTime.After(scan.Timestamp) { 516 if build.DEBUG && !hdb.staticDeps.Disrupt("DisableCriticalOnUnsortedHistory") { 517 hdb.staticLog.Critical("Host entry scan history not sorted.") 518 } else { 519 hdb.staticLog.Print("WARN: Host entry scan history not sorted.") 520 } 521 // Ignore the unsorted scan entry. 522 continue 523 } 524 if recentSuccess { 525 uptime += scan.Timestamp.Sub(recentTime) 526 } else { 527 downtime += scan.Timestamp.Sub(recentTime) 528 } 529 recentTime = scan.Timestamp 530 recentSuccess = scan.Success 531 } 532 533 // One more check to incorporate the uptime or downtime of the most recent 534 // scan, we assume that if we scanned them right now, their uptime / 535 // downtime status would be equal to what it currently is. 536 if recentSuccess { 537 uptime += time.Now().Sub(recentTime) 538 } else { 539 downtime += time.Now().Sub(recentTime) 540 } 541 542 // Sanity check against 0 total time. 543 if uptime == 0 && downtime == 0 { 544 build.Critical("uptime and downtime are zero for this host, should have been caught in earlier logic") 545 return math.SmallestNonzeroFloat64 546 } 547 548 // Compute the uptime ratio, but shift by 0.02 to acknowledge fully that 549 // 98% uptime and 100% uptime is valued the same. 550 uptimeRatio := float64(uptime) / float64(uptime+downtime) 551 if uptimeRatio > 0.98 { 552 uptimeRatio = 0.98 553 } 554 uptimeRatio += 0.02 555 556 // Cap the total amount of downtime allowed based on the total number of 557 // scans that have happened. 558 allowedDowntime := 0.03 * float64(len(entry.ScanHistory)) 559 if uptimeRatio < 1-allowedDowntime { 560 uptimeRatio = 1 - allowedDowntime 561 } 562 563 // Calculate the penalty for low uptime. Penalties increase extremely 564 // quickly as uptime falls away from 95%. 565 // 566 // 100% uptime = 1 567 // 98% uptime = 1 568 // 95% uptime = 0.83 569 // 90% uptime = 0.26 570 // 85% uptime = 0.03 571 // 80% uptime = 0.001 572 // 75% uptime = 0.00001 573 // 70% uptime = 0.0000001 574 exp := 200 * math.Min(1-uptimeRatio, 0.30) 575 return math.Pow(uptimeRatio, exp) 576 } 577 578 // managedCalculateHostWeightFn creates a hosttree.WeightFunc given an 579 // Allowance. 580 // 581 // NOTE: the hosttree.WeightFunc that is returned accesses fields of the hostdb. 582 // The hostdb lock must be held while utilizing the WeightFunc 583 func (hdb *HostDB) managedCalculateHostWeightFn(allowance skymodules.Allowance) hosttree.WeightFunc { 584 // Get the txnFees. 585 hdb.mu.RLock() 586 txnFees := hdb.txnFees 587 hdb.mu.RUnlock() 588 // Create the weight function. 589 return func(entry skymodules.HostDBEntry) hosttree.ScoreBreakdown { 590 return hosttree.HostAdjustments{ 591 AcceptContractAdjustment: hdb.acceptContractAdjustments(entry), 592 AgeAdjustment: hdb.lifetimeAdjustments(entry), 593 BasePriceAdjustment: hdb.basePriceAdjustments(entry), 594 BurnAdjustment: 1, 595 CollateralAdjustment: hdb.collateralAdjustments(entry, allowance), 596 DurationAdjustment: hdb.durationAdjustments(entry, allowance), 597 InteractionAdjustment: hdb.interactionAdjustments(entry), 598 PriceAdjustment: hdb.priceAdjustments(entry, allowance, txnFees), 599 StorageRemainingAdjustment: hdb.storageRemainingAdjustments(entry, allowance), 600 UptimeAdjustment: hdb.uptimeAdjustments(entry), 601 VersionAdjustment: versionAdjustments(entry), 602 } 603 } 604 } 605 606 // EstimateHostScore takes a HostExternalSettings and returns the estimated 607 // score of that host in the hostdb, assuming no penalties for age or uptime. 608 func (hdb *HostDB) EstimateHostScore(entry skymodules.HostDBEntry, allowance skymodules.Allowance) (skymodules.HostScoreBreakdown, error) { 609 if err := hdb.tg.Add(); err != nil { 610 return skymodules.HostScoreBreakdown{}, err 611 } 612 defer hdb.tg.Done() 613 return hdb.managedEstimatedScoreBreakdown(entry, allowance, true, true, true) 614 } 615 616 // ScoreBreakdown provdes a detailed set of scalars and bools indicating 617 // elements of the host's overall score. 618 func (hdb *HostDB) ScoreBreakdown(entry skymodules.HostDBEntry) (skymodules.HostScoreBreakdown, error) { 619 if err := hdb.tg.Add(); err != nil { 620 return skymodules.HostScoreBreakdown{}, err 621 } 622 defer hdb.tg.Done() 623 return hdb.managedScoreBreakdown(entry, false, false, false) 624 } 625 626 // managedEstimatedScoreBreakdown computes the score breakdown of a host. 627 // Certain adjustments can be ignored. 628 func (hdb *HostDB) managedEstimatedScoreBreakdown(entry skymodules.HostDBEntry, allowance skymodules.Allowance, ignoreAge, ignoreDuration, ignoreUptime bool) (skymodules.HostScoreBreakdown, error) { 629 hosts, err := hdb.ActiveHosts() 630 if err != nil { 631 return skymodules.HostScoreBreakdown{}, errors.AddContext(err, "error getting Active hosts:") 632 } 633 weightFunc := hdb.managedCalculateHostWeightFn(allowance) 634 635 // Compute the totalScore. 636 hdb.mu.Lock() 637 defer hdb.mu.Unlock() 638 totalScore := types.Currency{} 639 for _, host := range hosts { 640 totalScore = totalScore.Add(hdb.weightFunc(host).Score()) 641 } 642 // Compute the breakdown. 643 644 return weightFunc(entry).HostScoreBreakdown(totalScore, ignoreAge, ignoreDuration, ignoreUptime), nil 645 } 646 647 // managedScoreBreakdown computes the score breakdown of a host. Certain 648 // adjustments can be ignored. 649 func (hdb *HostDB) managedScoreBreakdown(entry skymodules.HostDBEntry, ignoreAge, ignoreDuration, ignoreUptime bool) (skymodules.HostScoreBreakdown, error) { 650 hosts, err := hdb.ActiveHosts() 651 if err != nil { 652 return skymodules.HostScoreBreakdown{}, errors.AddContext(err, "error getting Active hosts:") 653 } 654 655 // Compute the totalScore. 656 hdb.mu.Lock() 657 defer hdb.mu.Unlock() 658 totalScore := types.Currency{} 659 for _, host := range hosts { 660 totalScore = totalScore.Add(hdb.weightFunc(host).Score()) 661 } 662 // Compute the breakdown. 663 return hdb.weightFunc(entry).HostScoreBreakdown(totalScore, ignoreAge, ignoreDuration, ignoreUptime), nil 664 }