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