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 }