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 }