github.com/NebulousLabs/Sia@v1.3.7/modules/renter/hostdb/hostweight.go (about)

     1  package hostdb
     2  
     3  import (
     4  	"math"
     5  	"math/big"
     6  
     7  	"github.com/NebulousLabs/Sia/build"
     8  	"github.com/NebulousLabs/Sia/modules"
     9  	"github.com/NebulousLabs/Sia/types"
    10  )
    11  
    12  var (
    13  	// Because most weights would otherwise be fractional, we set the base
    14  	// weight to be very large.
    15  	baseWeight = types.NewCurrency(new(big.Int).Exp(big.NewInt(10), big.NewInt(80), nil))
    16  
    17  	// collateralExponentiation is the power to which we raise the weight
    18  	// during collateral adjustment.
    19  	collateralExponentiation = 0.75
    20  
    21  	// minCollateral is the amount of collateral we weight all hosts as having,
    22  	// even if they do not have any collateral. This is to temporarily prop up
    23  	// weak / cheap hosts on the network while the network is bootstrapping.
    24  	minCollateral = types.SiacoinPrecision.Div64(5).Div64(tbMonth)
    25  
    26  	// Set a minimum price, below which setting lower prices will no longer put
    27  	// this host at an advatnage. This price is considered the bar for
    28  	// 'essentially free', and is kept to a minimum to prevent certain Sybil
    29  	// attack related attack vectors.
    30  	//
    31  	// NOTE: This needs to be intelligently adjusted down as the practical price
    32  	// of storage changes, and as the price of the siacoin changes.
    33  	minTotalPrice = types.SiacoinPrecision.Mul64(1).Div64(tbMonth)
    34  
    35  	// priceDiveNormalization reduces the raw value of the price so that not so
    36  	// many digits are needed when operating on the weight. This also allows the
    37  	// base weight to be a lot lower.
    38  	priceDivNormalization = types.SiacoinPrecision.Div64(10e3).Div64(tbMonth)
    39  
    40  	// priceExponentiation is the number of times that the weight is divided by
    41  	// the price.
    42  	priceExponentiation = 5
    43  
    44  	// requiredStorage indicates the amount of storage that the host must be
    45  	// offering in order to be considered a valuable/worthwhile host.
    46  	requiredStorage = build.Select(build.Var{
    47  		Standard: uint64(20e9),
    48  		Dev:      uint64(1e6),
    49  		Testing:  uint64(1e3),
    50  	}).(uint64)
    51  
    52  	// tbMonth is the number of bytes in a terabyte times the number of blocks
    53  	// in a month.
    54  	tbMonth = uint64(4032) * uint64(1e12)
    55  )
    56  
    57  // collateralAdjustments improves the host's weight according to the amount of
    58  // collateral that they have provided.
    59  func (hdb *HostDB) collateralAdjustments(entry modules.HostDBEntry) float64 {
    60  	// Sanity checks - the constants values need to have certain relationships
    61  	// to eachother
    62  	if build.DEBUG {
    63  		// If the minTotalPrice is not much larger than the divNormalization,
    64  		// there will be problems with granularity after the divNormalization is
    65  		// applied.
    66  		if minCollateral.Div64(1e3).Cmp(priceDivNormalization) < 0 {
    67  			build.Critical("Maladjusted minCollateral and divNormalization constants in hostdb package")
    68  		}
    69  	}
    70  
    71  	// Set a minimum on the collateral, then normalize to a sane precision.
    72  	usedCollateral := entry.Collateral
    73  	if entry.Collateral.Cmp(minCollateral) < 0 {
    74  		usedCollateral = minCollateral
    75  	}
    76  	baseU64, err := minCollateral.Div(priceDivNormalization).Uint64()
    77  	if err != nil {
    78  		baseU64 = math.MaxUint64
    79  	}
    80  	actualU64, err := usedCollateral.Div(priceDivNormalization).Uint64()
    81  	if err != nil {
    82  		actualU64 = math.MaxUint64
    83  	}
    84  	base := float64(baseU64)
    85  	actual := float64(actualU64)
    86  
    87  	// Exponentiate the results.
    88  	weight := math.Pow(actual/base, collateralExponentiation)
    89  
    90  	// Add in penalties for low MaxCollateral. Hosts should be willing to pay
    91  	// for at least 100 GB of collateral on a contract.
    92  	gigaByte := types.NewCurrency64(1e9)
    93  	if entry.MaxCollateral.Cmp(entry.Collateral.Mul(gigaByte).Mul64(100)) < 0 {
    94  		weight = weight / 2 // 2x total penalty
    95  	}
    96  	if entry.MaxCollateral.Cmp(entry.Collateral.Mul(gigaByte).Mul64(33)) < 0 {
    97  		weight = weight / 5 // 10x total penalty
    98  	}
    99  	if entry.MaxCollateral.Cmp(entry.Collateral.Mul(gigaByte).Mul64(10)) < 0 {
   100  		weight = weight / 10 // 100x total penalty
   101  	}
   102  	if entry.MaxCollateral.Cmp(entry.Collateral.Mul(gigaByte).Mul64(3)) < 0 {
   103  		weight = weight / 10 // 1000x total penalty
   104  	}
   105  	return weight
   106  }
   107  
   108  // interactionAdjustments determine the penalty to be applied to a host for the
   109  // historic and currnet interactions with that host. This function focuses on
   110  // historic interactions and ignores recent interactions.
   111  func (hdb *HostDB) interactionAdjustments(entry modules.HostDBEntry) float64 {
   112  	// Give the host a baseline of 30 successful interactions and 1 failed
   113  	// interaction. This gives the host a baseline if we've had few
   114  	// interactions with them. The 1 failed interaction will become
   115  	// irrelevant after sufficient interactions with the host.
   116  	hsi := entry.HistoricSuccessfulInteractions + 30
   117  	hfi := entry.HistoricFailedInteractions + 1
   118  
   119  	// Determine the intraction ratio based off of the historic interactions.
   120  	ratio := float64(hsi) / float64(hsi+hfi)
   121  
   122  	// Raise the ratio to the 15th power and return that. The exponentiation is
   123  	// very high because the renter will already intentionally avoid hosts that
   124  	// do not have many successful interactions, meaning that the bad points do
   125  	// not rack up very quickly. We want to signal a bad score for the host
   126  	// nonetheless.
   127  	return math.Pow(ratio, 15)
   128  }
   129  
   130  // priceAdjustments will adjust the weight of the entry according to the prices
   131  // that it has set.
   132  func (hdb *HostDB) priceAdjustments(entry modules.HostDBEntry) float64 {
   133  	// Sanity checks - the constants values need to have certain relationships
   134  	// to eachother
   135  	if build.DEBUG {
   136  		// If the minTotalPrice is not much larger than the divNormalization,
   137  		// there will be problems with granularity after the divNormalization is
   138  		// applied.
   139  		if minTotalPrice.Div64(1e3).Cmp(priceDivNormalization) < 0 {
   140  			build.Critical("Maladjusted minDivePrice and divNormalization constants in hostdb package")
   141  		}
   142  	}
   143  
   144  	// Prices tiered as follows:
   145  	//    - the storage price is presented as 'per block per byte'
   146  	//    - the contract price is presented as a flat rate
   147  	//    - the upload bandwidth price is per byte
   148  	//    - the download bandwidth price is per byte
   149  	//
   150  	// The hostdb will naively assume the following for now:
   151  	//    - each contract covers 6 weeks of storage (default is 12 weeks, but
   152  	//      renewals occur at midpoint) - 6048 blocks - and 25GB of storage.
   153  	//    - uploads happen once per 12 weeks (average lifetime of a file is 12 weeks)
   154  	//    - downloads happen once per 12 weeks (files are on average downloaded once throughout lifetime)
   155  	//
   156  	// In the future, the renter should be able to track average user behavior
   157  	// and adjust accordingly. This flexibility will be added later.
   158  	adjustedContractPrice := entry.ContractPrice.Div64(6048).Div64(25e9)        // Adjust contract price to match 25GB for 6 weeks.
   159  	adjustedUploadPrice := entry.UploadBandwidthPrice.Div64(24192)              // Adjust upload price to match a single upload over 24 weeks.
   160  	adjustedDownloadPrice := entry.DownloadBandwidthPrice.Div64(12096).Div64(3) // Adjust download price to match one download over 12 weeks, 1 redundancy.
   161  	siafundFee := adjustedContractPrice.Add(adjustedUploadPrice).Add(adjustedDownloadPrice).Add(entry.Collateral).MulTax()
   162  	totalPrice := entry.StoragePrice.Add(adjustedContractPrice).Add(adjustedUploadPrice).Add(adjustedDownloadPrice).Add(siafundFee)
   163  
   164  	// Set a minimum on the price, then normalize to a sane precision.
   165  	if totalPrice.Cmp(minTotalPrice) < 0 {
   166  		totalPrice = minTotalPrice
   167  	}
   168  	baseU64, err := minTotalPrice.Div(priceDivNormalization).Uint64()
   169  	if err != nil {
   170  		baseU64 = math.MaxUint64
   171  	}
   172  	actualU64, err := totalPrice.Div(priceDivNormalization).Uint64()
   173  	if err != nil {
   174  		actualU64 = math.MaxUint64
   175  	}
   176  	base := float64(baseU64)
   177  	actual := float64(actualU64)
   178  
   179  	weight := float64(1)
   180  	for i := 0; i < priceExponentiation; i++ {
   181  		weight *= base / actual
   182  	}
   183  	return weight
   184  }
   185  
   186  // storageRemainingAdjustments adjusts the weight of the entry according to how
   187  // much storage it has remaining.
   188  func storageRemainingAdjustments(entry modules.HostDBEntry) float64 {
   189  	base := float64(1)
   190  	if entry.RemainingStorage < 200*requiredStorage {
   191  		base = base / 2 // 2x total penalty
   192  	}
   193  	if entry.RemainingStorage < 150*requiredStorage {
   194  		base = base / 2 // 4x total penalty
   195  	}
   196  	if entry.RemainingStorage < 100*requiredStorage {
   197  		base = base / 2 // 8x total penalty
   198  	}
   199  	if entry.RemainingStorage < 80*requiredStorage {
   200  		base = base / 2 // 16x total penalty
   201  	}
   202  	if entry.RemainingStorage < 40*requiredStorage {
   203  		base = base / 2 // 32x total penalty
   204  	}
   205  	if entry.RemainingStorage < 20*requiredStorage {
   206  		base = base / 2 // 64x total penalty
   207  	}
   208  	if entry.RemainingStorage < 15*requiredStorage {
   209  		base = base / 2 // 128x total penalty
   210  	}
   211  	if entry.RemainingStorage < 10*requiredStorage {
   212  		base = base / 2 // 256x total penalty
   213  	}
   214  	if entry.RemainingStorage < 5*requiredStorage {
   215  		base = base / 2 // 512x total penalty
   216  	}
   217  	if entry.RemainingStorage < 3*requiredStorage {
   218  		base = base / 2 // 1024x total penalty
   219  	}
   220  	if entry.RemainingStorage < 2*requiredStorage {
   221  		base = base / 2 // 2048x total penalty
   222  	}
   223  	if entry.RemainingStorage < requiredStorage {
   224  		base = base / 2 // 4096x total penalty
   225  	}
   226  	return base
   227  }
   228  
   229  // versionAdjustments will adjust the weight of the entry according to the siad
   230  // version reported by the host.
   231  func versionAdjustments(entry modules.HostDBEntry) float64 {
   232  	base := float64(1)
   233  	if build.VersionCmp(entry.Version, "1.4.0") < 0 {
   234  		base = base * 0.99999 // Safety value to make sure we update the version penalties every time we update the host.
   235  	}
   236  	if build.VersionCmp(entry.Version, "1.3.2") < 0 {
   237  		base = base * 0.9
   238  	}
   239  	// we shouldn't use pre hardfork hosts
   240  	if build.VersionCmp(entry.Version, "1.3.1") < 0 {
   241  		base = math.SmallestNonzeroFloat64
   242  	}
   243  	return base
   244  }
   245  
   246  // lifetimeAdjustments will adjust the weight of the host according to the total
   247  // amount of time that has passed since the host's original announcement.
   248  func (hdb *HostDB) lifetimeAdjustments(entry modules.HostDBEntry) float64 {
   249  	base := float64(1)
   250  	if hdb.blockHeight >= entry.FirstSeen {
   251  		age := hdb.blockHeight - entry.FirstSeen
   252  		if age < 6000 {
   253  			base = base / 2 // 2x total
   254  		}
   255  		if age < 4000 {
   256  			base = base / 2 // 4x total
   257  		}
   258  		if age < 2000 {
   259  			base = base / 2 // 8x total
   260  		}
   261  		if age < 1000 {
   262  			base = base / 2 // 16x total
   263  		}
   264  		if age < 576 {
   265  			base = base / 2 // 32x total
   266  		}
   267  		if age < 288 {
   268  			base = base / 2 // 64x total
   269  		}
   270  		if age < 144 {
   271  			base = base / 2 // 128x total
   272  		}
   273  	}
   274  	return base
   275  }
   276  
   277  // uptimeAdjustments penalizes the host for having poor uptime, and for being
   278  // offline.
   279  //
   280  // CAUTION: The function 'updateEntry' will manually fill out two scans for a
   281  // new host to give the host some initial uptime or downtime. Modification of
   282  // this function needs to be made paying attention to the structure of that
   283  // function.
   284  func (hdb *HostDB) uptimeAdjustments(entry modules.HostDBEntry) float64 {
   285  	// Special case: if we have scanned the host twice or fewer, don't perform
   286  	// uptime math.
   287  	if len(entry.ScanHistory) == 0 {
   288  		return 0.25
   289  	}
   290  	if len(entry.ScanHistory) == 1 {
   291  		if entry.ScanHistory[0].Success {
   292  			return 0.75
   293  		}
   294  		return 0.25
   295  	}
   296  	if len(entry.ScanHistory) == 2 {
   297  		if entry.ScanHistory[0].Success && entry.ScanHistory[1].Success {
   298  			return 0.85
   299  		}
   300  		if entry.ScanHistory[0].Success || entry.ScanHistory[1].Success {
   301  			return 0.50
   302  		}
   303  		return 0.05
   304  	}
   305  
   306  	// Compute the total measured uptime and total measured downtime for this
   307  	// host.
   308  	downtime := entry.HistoricDowntime
   309  	uptime := entry.HistoricUptime
   310  	recentTime := entry.ScanHistory[0].Timestamp
   311  	recentSuccess := entry.ScanHistory[0].Success
   312  	for _, scan := range entry.ScanHistory[1:] {
   313  		if recentTime.After(scan.Timestamp) {
   314  			if build.DEBUG {
   315  				hdb.log.Critical("Host entry scan history not sorted.")
   316  			} else {
   317  				hdb.log.Print("WARNING: Host entry scan history not sorted.")
   318  			}
   319  			// Ignore the unsorted scan entry.
   320  			continue
   321  		}
   322  		if recentSuccess {
   323  			uptime += scan.Timestamp.Sub(recentTime)
   324  		} else {
   325  			downtime += scan.Timestamp.Sub(recentTime)
   326  		}
   327  		recentTime = scan.Timestamp
   328  		recentSuccess = scan.Success
   329  	}
   330  	// Sanity check against 0 total time.
   331  	if uptime == 0 && downtime == 0 {
   332  		return 0.001 // Shouldn't happen.
   333  	}
   334  
   335  	// Compute the uptime ratio, but shift by 0.02 to acknowledge fully that
   336  	// 98% uptime and 100% uptime is valued the same.
   337  	uptimeRatio := float64(uptime) / float64(uptime+downtime)
   338  	if uptimeRatio > 0.98 {
   339  		uptimeRatio = 0.98
   340  	}
   341  	uptimeRatio += 0.02
   342  
   343  	// Cap the total amount of downtime allowed based on the total number of
   344  	// scans that have happened.
   345  	allowedDowntime := 0.03 * float64(len(entry.ScanHistory))
   346  	if uptimeRatio < 1-allowedDowntime {
   347  		uptimeRatio = 1 - allowedDowntime
   348  	}
   349  
   350  	// Calculate the penalty for low uptime. Penalties increase extremely
   351  	// quickly as uptime falls away from 95%.
   352  	//
   353  	// 100% uptime = 1
   354  	// 98%  uptime = 1
   355  	// 95%  uptime = 0.91
   356  	// 90%  uptime = 0.51
   357  	// 85%  uptime = 0.16
   358  	// 80%  uptime = 0.03
   359  	// 75%  uptime = 0.005
   360  	// 70%  uptime = 0.001
   361  	// 50%  uptime = 0.000002
   362  	exp := 100 * math.Min(1-uptimeRatio, 0.20)
   363  	return math.Pow(uptimeRatio, exp)
   364  }
   365  
   366  // calculateHostWeight returns the weight of a host according to the settings of
   367  // the host database entry.
   368  func (hdb *HostDB) calculateHostWeight(entry modules.HostDBEntry) types.Currency {
   369  	collateralReward := hdb.collateralAdjustments(entry)
   370  	interactionPenalty := hdb.interactionAdjustments(entry)
   371  	lifetimePenalty := hdb.lifetimeAdjustments(entry)
   372  	pricePenalty := hdb.priceAdjustments(entry)
   373  	storageRemainingPenalty := storageRemainingAdjustments(entry)
   374  	uptimePenalty := hdb.uptimeAdjustments(entry)
   375  	versionPenalty := versionAdjustments(entry)
   376  
   377  	// Combine the adjustments.
   378  	fullPenalty := collateralReward * interactionPenalty * lifetimePenalty *
   379  		pricePenalty * storageRemainingPenalty * uptimePenalty * versionPenalty
   380  
   381  	// Return a types.Currency.
   382  	weight := baseWeight.MulFloat(fullPenalty)
   383  	if weight.IsZero() {
   384  		// A weight of zero is problematic for for the host tree.
   385  		return types.NewCurrency64(1)
   386  	}
   387  	return weight
   388  }
   389  
   390  // calculateConversionRate calculates the conversion rate of the provided
   391  // host score, comparing it to the hosts in the database and returning what
   392  // percentage of contracts it is likely to participate in.
   393  func (hdb *HostDB) calculateConversionRate(score types.Currency) float64 {
   394  	var totalScore types.Currency
   395  	for _, h := range hdb.ActiveHosts() {
   396  		totalScore = totalScore.Add(hdb.calculateHostWeight(h))
   397  	}
   398  	if totalScore.IsZero() {
   399  		totalScore = types.NewCurrency64(1)
   400  	}
   401  	conversionRate, _ := big.NewRat(0, 1).SetFrac(score.Mul64(50).Big(), totalScore.Big()).Float64()
   402  	if conversionRate > 100 {
   403  		conversionRate = 100
   404  	}
   405  	return conversionRate
   406  }
   407  
   408  // EstimateHostScore takes a HostExternalSettings and returns the estimated
   409  // score of that host in the hostdb, assuming no penalties for age or uptime.
   410  func (hdb *HostDB) EstimateHostScore(entry modules.HostDBEntry) modules.HostScoreBreakdown {
   411  	// Grab the adjustments. Age, and uptime penalties are set to '1', to
   412  	// assume best behavior from the host.
   413  	collateralReward := hdb.collateralAdjustments(entry)
   414  	pricePenalty := hdb.priceAdjustments(entry)
   415  	storageRemainingPenalty := storageRemainingAdjustments(entry)
   416  	versionPenalty := versionAdjustments(entry)
   417  
   418  	// Combine into a full penalty, then determine the resulting estimated
   419  	// score.
   420  	fullPenalty := collateralReward * pricePenalty * storageRemainingPenalty * versionPenalty
   421  	estimatedScore := baseWeight.MulFloat(fullPenalty)
   422  	if estimatedScore.IsZero() {
   423  		estimatedScore = types.NewCurrency64(1)
   424  	}
   425  
   426  	// Compile the estimates into a host score breakdown.
   427  	return modules.HostScoreBreakdown{
   428  		Score:          estimatedScore,
   429  		ConversionRate: hdb.calculateConversionRate(estimatedScore),
   430  
   431  		AgeAdjustment:              1,
   432  		BurnAdjustment:             1,
   433  		CollateralAdjustment:       collateralReward,
   434  		PriceAdjustment:            pricePenalty,
   435  		StorageRemainingAdjustment: storageRemainingPenalty,
   436  		UptimeAdjustment:           1,
   437  		VersionAdjustment:          versionPenalty,
   438  	}
   439  }
   440  
   441  // ScoreBreakdown provdes a detailed set of scalars and bools indicating
   442  // elements of the host's overall score.
   443  func (hdb *HostDB) ScoreBreakdown(entry modules.HostDBEntry) modules.HostScoreBreakdown {
   444  	hdb.mu.Lock()
   445  	defer hdb.mu.Unlock()
   446  
   447  	score := hdb.calculateHostWeight(entry)
   448  	return modules.HostScoreBreakdown{
   449  		Score:          score,
   450  		ConversionRate: hdb.calculateConversionRate(score),
   451  
   452  		AgeAdjustment:              hdb.lifetimeAdjustments(entry),
   453  		BurnAdjustment:             1,
   454  		CollateralAdjustment:       hdb.collateralAdjustments(entry),
   455  		InteractionAdjustment:      hdb.interactionAdjustments(entry),
   456  		PriceAdjustment:            hdb.priceAdjustments(entry),
   457  		StorageRemainingAdjustment: storageRemainingAdjustments(entry),
   458  		UptimeAdjustment:           hdb.uptimeAdjustments(entry),
   459  		VersionAdjustment:          versionAdjustments(entry),
   460  	}
   461  }