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  }