github.com/fozzysec/SiaPrime@v0.0.0-20190612043147-66c8e8d11fe3/modules/renter/hostdb/hostweight.go (about) 1 package hostdb 2 3 import ( 4 "math" 5 "math/big" 6 7 "SiaPrime/build" 8 "SiaPrime/modules" 9 "SiaPrime/modules/renter/hostdb/hosttree" 10 "SiaPrime/types" 11 ) 12 13 const ( 14 // collateralExponentiation is the power to which we raise the weight 15 // during collateral adjustment when the collateral is large. This sublinear 16 // number ensures that there is not an overpreference on collateral when 17 // collateral is large relative to the size of the allowance. 18 collateralExponentiationLarge = 0.5 19 20 // collateralExponentiationSmall is the power to which we raise the weight 21 // during collateral adjustment when the collateral is small. This large 22 // number ensures a heavy focus on collateral when distinguishing between 23 // hosts that have a very small amount of collateral provided compared to 24 // the size of the allowance. 25 // 26 // The number is set relative to the price exponentiation, because the goal 27 // is to ensure that the collateral has more weight than the price when the 28 // collateral is small. 29 collateralExponentiationSmall = priceExponentiationLarge + 1 30 31 // collateralFloor is a part of the equation for determining the collateral 32 // cutoff between large and small collateral. The equation figures out how 33 // much collateral is expected given the allowance, and then divided by 34 // 'collateralFloor' so that the cutoff for how much collateral counts as 35 // 'not much' is reasonably below what we are actually expecting from the 36 // host. 37 collateralFloor = 2 38 39 // interactionExponentiation determines how heavily we penalize hosts for 40 // having poor interactions - disconnecting, RPCs with errors, etc. The 41 // exponentiation is very high because the renter will already intentionally 42 // avoid hosts that do not have many successful interactions, meaning that 43 // the bad points do not rack up very quickly. 44 interactionExponentiation = 10 45 46 // priceExponentiationLarge is the number of times that the weight is 47 // divided by the price when the price is large relative to the allowance. 48 // The exponentiation is a lot higher because we care greatly about high 49 // priced hosts. 50 priceExponentiationLarge = 5 51 52 // priceExponentiationSmall is the number of times that the weight is 53 // divided by the price when the price is small relative to the allowance. 54 // The exponentiation is lower because we do not care about saving 55 // substantial amounts of money when the price is low. 56 priceExponentiationSmall = 0.75 57 58 // priceFloor is used in the final step of the equation that determines the 59 // cutoff for where a low price no longer counts as interesting to the 60 // renter. A priceFloor of '5' means that if the host can provide us with 61 // the amount of storage we require for less than 20% of the total 62 // allowance, then we switch to a new equation where further decreases in 63 // price are valued much less aggressively (though they are still valued). 64 priceFloor = 5 65 66 // tbMonth is the number of bytes in a terabyte times the number of blocks 67 // in a month. 68 tbMonth = 4032 * 1e12 69 ) 70 71 var ( 72 // Because most weights would otherwise be fractional, we set the base 73 // weight to be very large. 74 baseWeight = types.NewCurrency(new(big.Int).Exp(big.NewInt(10), big.NewInt(80), nil)) 75 76 // priceDiveNormalization reduces the raw value of the price so that not so 77 // many digits are needed when operating on the weight. This also allows the 78 // base weight to be a lot lower. 79 priceDivNormalization = types.SiacoinPrecision.Div64(1e3).Div64(tbMonth) 80 81 // requiredStorage indicates the amount of storage that the host must be 82 // offering in order to be considered a valuable/worthwhile host. 83 requiredStorage = build.Select(build.Var{ 84 Standard: uint64(20e9), 85 Dev: uint64(1e6), 86 Testing: uint64(1e3), 87 }).(uint64) 88 ) 89 90 // collateralAdjustments improves the host's weight according to the amount of 91 // collateral that they have provided. 92 func (hdb *HostDB) collateralAdjustments(entry modules.HostDBEntry, allowance modules.Allowance, ug modules.UsageGuidelines) float64 { 93 // Ensure that all values will avoid divide by zero errors. 94 if allowance.Hosts == 0 { 95 allowance.Hosts = 1 96 } 97 if allowance.Period == 0 { 98 allowance.Period = 1 99 } 100 if ug.ExpectedStorage == 0 { 101 ug.ExpectedStorage = 1 102 } 103 if ug.ExpectedUploadFrequency == 0 { 104 ug.ExpectedUploadFrequency = 1 105 } 106 if ug.ExpectedDownloadFrequency == 0 { 107 ug.ExpectedDownloadFrequency = 1 108 } 109 if ug.ExpectedRedundancy == 0 { 110 ug.ExpectedRedundancy = 1 111 } 112 113 // Ensure that the allowance and expected storage will not brush up against 114 // the max collateral. If the allowance comes within half of the max 115 // collateral, cap the collateral that we use during adjustments based on 116 // the max collateral instead of the per-byte collateral. 117 // 118 // The purpose of this code is to make sure that the host actually has a 119 // high enough MaxCollateral to cover all of the data that we intend to 120 // store with the host at the collateral price that the host is advertising. 121 // We add a 2x buffer to account for the fact that the renter may end up 122 // storing extra data on this host. 123 hostCollateral := entry.Collateral 124 possibleCollateral := entry.MaxCollateral.Div64(uint64(allowance.Period)).Div64(ug.ExpectedStorage).Div64(2) 125 if possibleCollateral.Cmp(hostCollateral) < 0 { 126 hostCollateral = possibleCollateral 127 } 128 129 // Determine the cutoff for the difference between small collateral and 130 // large collateral. The cutoff is used to create a step function in the 131 // collateral scoring where decreasing collateral results in much higher 132 // penalties below a certain threshold. 133 // 134 // This threshold is attempting to be the threshold where the amount of 135 // money becomes insignificant. A collateral that is 10x higher than the 136 // price is not interesting, compelling, nor a sign of reliability if the 137 // price and collateral are both effectively zero. 138 // 139 // The strategy is to take our total allowance and divide it by the number 140 // of hosts, to get an expected allowance per host. We then adjust based on 141 // the period, and then adjust by adding in the expected storage, upload and 142 // download. We add the three together so that storage heavy allowances will 143 // have higher expectations for collateral than bandwidth heavy allowances. 144 // Finally, we divide the whole thing by collateralFloor to give some wiggle room to 145 // hosts. The large multiplier provided for low collaterals is only intended 146 // to discredit hosts that have a meaningless amount of collateral. 147 expectedUploadBandwidth := ug.ExpectedStorage * uint64(allowance.Period) / ug.ExpectedUploadFrequency 148 expectedDownloadBandwidthRedundant := ug.ExpectedStorage * uint64(allowance.Period) / ug.ExpectedDownloadFrequency 149 expectedDownloadBandwidth := uint64(float64(expectedDownloadBandwidthRedundant) / ug.ExpectedRedundancy) 150 expectedBandwidth := expectedUploadBandwidth + expectedDownloadBandwidth 151 cutoff := allowance.Funds.Div64(allowance.Hosts).Div64(uint64(allowance.Period)).Div64(ug.ExpectedStorage + expectedBandwidth).Div64(collateralFloor) 152 if hostCollateral.Cmp(cutoff) < 0 { 153 // Set the cutoff equal to the collateral so that the ratio has a 154 // minimum of 1, and also so that the smallWeight is computed based on 155 // the actual collateral instead of just the cutoff. 156 cutoff = hostCollateral 157 } 158 // Get the ratio between the cutoff and the actual collateral so we can 159 // award the bonus for having a large collateral. Perform normalization 160 // before converting to uint64. 161 collateral64, _ := hostCollateral.Div(priceDivNormalization).Uint64() 162 cutoff64, _ := cutoff.Div(priceDivNormalization).Uint64() 163 if cutoff64 == 0 { 164 cutoff64 = 1 165 } 166 ratio := float64(collateral64) / float64(cutoff64) 167 168 // Use the cutoff to determine the score based on the small exponentiation 169 // factor (which has a high exponentiation), and then use the ratio between 170 // the two to determine the bonus gained from having a high collateral. 171 smallWeight := math.Pow(float64(cutoff64), collateralExponentiationSmall) 172 largeWeight := math.Pow(ratio, collateralExponentiationLarge) 173 return smallWeight * largeWeight 174 } 175 176 // interactionAdjustments determine the penalty to be applied to a host for the 177 // historic and currnet interactions with that host. This function focuses on 178 // historic interactions and ignores recent interactions. 179 func (hdb *HostDB) interactionAdjustments(entry modules.HostDBEntry) float64 { 180 // Give the host a baseline of 30 successful interactions and 1 failed 181 // interaction. This gives the host a baseline if we've had few 182 // interactions with them. The 1 failed interaction will become 183 // irrelevant after sufficient interactions with the host. 184 hsi := entry.HistoricSuccessfulInteractions + 30 185 hfi := entry.HistoricFailedInteractions + 1 186 187 // Determine the intraction ratio based off of the historic interactions. 188 ratio := float64(hsi) / float64(hsi+hfi) 189 return math.Pow(ratio, interactionExponentiation) 190 } 191 192 // priceAdjustments will adjust the weight of the entry according to the prices 193 // that it has set. 194 func (hdb *HostDB) priceAdjustments(entry modules.HostDBEntry, allowance modules.Allowance, ug modules.UsageGuidelines) float64 { 195 // Divide by zero mitigation. 196 if allowance.Hosts == 0 { 197 allowance.Hosts = 1 198 } 199 if allowance.Period == 0 { 200 allowance.Period = 1 201 } 202 if ug.ExpectedStorage == 0 { 203 ug.ExpectedStorage = 1 204 } 205 if ug.ExpectedUploadFrequency == 0 { 206 ug.ExpectedUploadFrequency = 1 207 } 208 if ug.ExpectedDownloadFrequency == 0 { 209 ug.ExpectedDownloadFrequency = 1 210 } 211 if ug.ExpectedRedundancy == 0 { 212 ug.ExpectedRedundancy = 1 213 } 214 215 // Calculate the hostCollateral the renter would expect the host to put 216 // into a contract. 217 // TODO: Use actual transaction fee estimation instead of hardcoded 1SC. 218 _, _, hostCollateral, err := modules.RenterPayoutsPreTax(entry, allowance.Funds.Div64(allowance.Hosts), types.SiacoinPrecision, types.ZeroCurrency, allowance.Period, ug.ExpectedStorage) 219 if err != nil { 220 hdb.log.Println(err) 221 return 0 222 } 223 224 // Prices tiered as follows: 225 // - the collateral price is presented as 'per block per byte' 226 // - the storage price is presented as 'per block per byte' 227 // - the contract price is presented as a flat rate 228 // - the upload bandwidth price is per byte 229 // - the download bandwidth price is per byte 230 // 231 // The adjusted prices take the pricing for other parts of the contract 232 // (like bandwidth and fees) and convert them into terms that are relative 233 // to the storage price. 234 adjustedCollateralPrice := hostCollateral.Div64(uint64(allowance.Period)).Div64(ug.ExpectedStorage) 235 adjustedContractPrice := entry.ContractPrice.Div64(uint64(allowance.Period)).Div64(ug.ExpectedStorage) 236 adjustedUploadPrice := entry.UploadBandwidthPrice.Div64(ug.ExpectedUploadFrequency) 237 adjustedDownloadPrice := entry.DownloadBandwidthPrice.Div64(ug.ExpectedDownloadFrequency).MulFloat(ug.ExpectedRedundancy) 238 siafundFee := adjustedContractPrice.Add(adjustedUploadPrice).Add(adjustedDownloadPrice).Add(adjustedCollateralPrice).MulTax() 239 totalPrice := entry.StoragePrice.Add(adjustedContractPrice).Add(adjustedUploadPrice).Add(adjustedDownloadPrice).Add(siafundFee) 240 241 // Determine a cutoff for whether the total price is considered a high price 242 // or a low price. This cutoff attempts to determine where the price becomes 243 // insignificant. 244 expectedUploadBandwidth := ug.ExpectedStorage * uint64(allowance.Period) / ug.ExpectedUploadFrequency 245 expectedDownloadBandwidthRedundant := ug.ExpectedStorage * uint64(allowance.Period) / ug.ExpectedDownloadFrequency 246 expectedDownloadBandwidth := uint64(float64(expectedDownloadBandwidthRedundant) / ug.ExpectedRedundancy) 247 expectedBandwidth := expectedUploadBandwidth + expectedDownloadBandwidth 248 cutoff := allowance.Funds.Div64(allowance.Hosts).Div64(uint64(allowance.Period)).Div64(ug.ExpectedStorage + expectedBandwidth).Div64(priceFloor) 249 if totalPrice.Cmp(cutoff) < 0 { 250 cutoff = totalPrice 251 } 252 price64, _ := totalPrice.Div(priceDivNormalization).Uint64() 253 cutoff64, _ := cutoff.Div(priceDivNormalization).Uint64() 254 if cutoff64 == 0 { 255 cutoff64 = 1 256 } 257 if price64 == 0 { 258 price64 = 1 259 } 260 ratio := float64(price64) / float64(cutoff64) 261 262 smallWeight := math.Pow(float64(cutoff64), priceExponentiationSmall) 263 largeWeight := math.Pow(ratio, priceExponentiationLarge) 264 return 1 / (smallWeight * largeWeight) 265 } 266 267 // storageRemainingAdjustments adjusts the weight of the entry according to how 268 // much storage it has remaining. 269 func storageRemainingAdjustments(entry modules.HostDBEntry) float64 { 270 base := float64(1) 271 if entry.RemainingStorage < 100*requiredStorage { 272 base = base / 2 // 2x total penalty 273 } 274 if entry.RemainingStorage < 80*requiredStorage { 275 base = base / 2 // 4x total penalty 276 } 277 if entry.RemainingStorage < 40*requiredStorage { 278 base = base / 2 // 8x total penalty 279 } 280 if entry.RemainingStorage < 20*requiredStorage { 281 base = base / 2 // 16x total penalty 282 } 283 if entry.RemainingStorage < 15*requiredStorage { 284 base = base / 2 // 32x total penalty 285 } 286 if entry.RemainingStorage < 10*requiredStorage { 287 base = base / 2 // 64x total penalty 288 } 289 if entry.RemainingStorage < 5*requiredStorage { 290 base = base / 2 // 128x total penalty 291 } 292 if entry.RemainingStorage < 3*requiredStorage { 293 base = base / 2 // 256x total penalty 294 } 295 if entry.RemainingStorage < 2*requiredStorage { 296 base = base / 2 // 512x total penalty 297 } 298 if entry.RemainingStorage < requiredStorage { 299 base = base / 2 // 1024x total penalty 300 } 301 return base 302 } 303 304 // versionAdjustments will adjust the weight of the entry according to the siad 305 // version reported by the host. 306 func versionAdjustments(entry modules.HostDBEntry) float64 { 307 base := float64(1) 308 if build.VersionCmp(entry.Version, "1.4.0") < 0 { 309 base = base * 0.99999 // Safety value to make sure we update the version penalties every time we update the host. 310 } 311 // -10% for being below 1.3.8. 312 if build.VersionCmp(entry.Version, "1.3.8") < 0 { 313 base = base * 0.9 314 } 315 // -10% for being below 1.3.5. 316 if build.VersionCmp(entry.Version, "1.3.5") < 0 { 317 base = base * 0.9 318 } 319 // -10% for being below 1.3.4. 320 if build.VersionCmp(entry.Version, "1.3.4") < 0 { 321 base = base * 0.9 322 } 323 // -10% for being below 1.3.3. 324 if build.VersionCmp(entry.Version, "1.3.3") < 0 { 325 base = base * 0.9 326 } 327 // we shouldn't use pre hardfork hosts 328 if build.VersionCmp(entry.Version, "1.3.1") < 0 { 329 base = math.SmallestNonzeroFloat64 330 } 331 return base 332 } 333 334 // lifetimeAdjustments will adjust the weight of the host according to the total 335 // amount of time that has passed since the host's original announcement. 336 func (hdb *HostDB) lifetimeAdjustments(entry modules.HostDBEntry) float64 { 337 base := float64(1) 338 if hdb.blockHeight >= entry.FirstSeen { 339 age := hdb.blockHeight - entry.FirstSeen 340 if age < 12000 { 341 base = base * 2 / 3 // 1.5x total 342 } 343 if age < 6000 { 344 base = base / 2 // 3x total 345 } 346 if age < 4000 { 347 base = base / 2 // 6x total 348 } 349 if age < 2000 { 350 base = base / 2 // 12x total 351 } 352 if age < 1000 { 353 base = base / 3 // 36x total 354 } 355 if age < 576 { 356 base = base / 3 // 108x total 357 } 358 if age < 288 { 359 base = base / 3 // 324x total 360 } 361 if age < 144 { 362 base = base / 3 // 972x total 363 } 364 } 365 return base 366 } 367 368 // uptimeAdjustments penalizes the host for having poor uptime, and for being 369 // offline. 370 // 371 // CAUTION: The function 'updateEntry' will manually fill out two scans for a 372 // new host to give the host some initial uptime or downtime. Modification of 373 // this function needs to be made paying attention to the structure of that 374 // function. 375 func (hdb *HostDB) uptimeAdjustments(entry modules.HostDBEntry) float64 { 376 // Special case: if we have scanned the host twice or fewer, don't perform 377 // uptime math. 378 if len(entry.ScanHistory) == 0 { 379 return 0.25 380 } 381 if len(entry.ScanHistory) == 1 { 382 if entry.ScanHistory[0].Success { 383 return 0.75 384 } 385 return 0.25 386 } 387 if len(entry.ScanHistory) == 2 { 388 if entry.ScanHistory[0].Success && entry.ScanHistory[1].Success { 389 return 0.85 390 } 391 if entry.ScanHistory[0].Success || entry.ScanHistory[1].Success { 392 return 0.50 393 } 394 return 0.05 395 } 396 397 // Compute the total measured uptime and total measured downtime for this 398 // host. 399 downtime := entry.HistoricDowntime 400 uptime := entry.HistoricUptime 401 recentTime := entry.ScanHistory[0].Timestamp 402 recentSuccess := entry.ScanHistory[0].Success 403 for _, scan := range entry.ScanHistory[1:] { 404 if recentTime.After(scan.Timestamp) { 405 if build.DEBUG { 406 hdb.log.Critical("Host entry scan history not sorted.") 407 } else { 408 hdb.log.Print("WARNING: Host entry scan history not sorted.") 409 } 410 // Ignore the unsorted scan entry. 411 continue 412 } 413 if recentSuccess { 414 uptime += scan.Timestamp.Sub(recentTime) 415 } else { 416 downtime += scan.Timestamp.Sub(recentTime) 417 } 418 recentTime = scan.Timestamp 419 recentSuccess = scan.Success 420 } 421 // Sanity check against 0 total time. 422 if uptime == 0 && downtime == 0 { 423 return 0.001 // Shouldn't happen. 424 } 425 426 // Compute the uptime ratio, but shift by 0.02 to acknowledge fully that 427 // 98% uptime and 100% uptime is valued the same. 428 uptimeRatio := float64(uptime) / float64(uptime+downtime) 429 if uptimeRatio > 0.98 { 430 uptimeRatio = 0.98 431 } 432 uptimeRatio += 0.02 433 434 // Cap the total amount of downtime allowed based on the total number of 435 // scans that have happened. 436 allowedDowntime := 0.03 * float64(len(entry.ScanHistory)) 437 if uptimeRatio < 1-allowedDowntime { 438 uptimeRatio = 1 - allowedDowntime 439 } 440 441 // Calculate the penalty for low uptime. Penalties increase extremely 442 // quickly as uptime falls away from 95%. 443 // 444 // 100% uptime = 1 445 // 98% uptime = 1 446 // 95% uptime = 0.83 447 // 90% uptime = 0.26 448 // 85% uptime = 0.03 449 // 80% uptime = 0.001 450 // 75% uptime = 0.00001 451 // 70% uptime = 0.0000001 452 exp := 200 * math.Min(1-uptimeRatio, 0.30) 453 return math.Pow(uptimeRatio, exp) 454 } 455 456 // calculateHostWeightFn creates a hosttree.WeightFunc given an Allowance. 457 func (hdb *HostDB) calculateHostWeightFn(allowance modules.Allowance) hosttree.WeightFunc { 458 // TODO: Pass these in as input instead of using the defaults. 459 ug := modules.DefaultUsageGuideLines 460 461 return func(entry modules.HostDBEntry) types.Currency { 462 collateralReward := hdb.collateralAdjustments(entry, allowance, ug) 463 interactionPenalty := hdb.interactionAdjustments(entry) 464 lifetimePenalty := hdb.lifetimeAdjustments(entry) 465 pricePenalty := hdb.priceAdjustments(entry, allowance, ug) 466 storageRemainingPenalty := storageRemainingAdjustments(entry) 467 uptimePenalty := hdb.uptimeAdjustments(entry) 468 versionPenalty := versionAdjustments(entry) 469 470 // Combine the adjustments. 471 fullPenalty := collateralReward * interactionPenalty * lifetimePenalty * 472 pricePenalty * storageRemainingPenalty * uptimePenalty * versionPenalty 473 474 // Return a types.Currency. 475 weight := baseWeight.MulFloat(fullPenalty) 476 if weight.IsZero() { 477 // A weight of zero is problematic for for the host tree. 478 return types.NewCurrency64(1) 479 } 480 return weight 481 } 482 } 483 484 // calculateConversionRate calculates the conversion rate of the provided 485 // host score, comparing it to the hosts in the database and returning what 486 // percentage of contracts it is likely to participate in. 487 func (hdb *HostDB) calculateConversionRate(score types.Currency) float64 { 488 var totalScore types.Currency 489 for _, h := range hdb.ActiveHosts() { 490 totalScore = totalScore.Add(hdb.weightFunc(h)) 491 } 492 if totalScore.IsZero() { 493 totalScore = types.NewCurrency64(1) 494 } 495 conversionRate, _ := big.NewRat(0, 1).SetFrac(score.Mul64(50).Big(), totalScore.Big()).Float64() 496 if conversionRate > 100 { 497 conversionRate = 100 498 } 499 return conversionRate 500 } 501 502 // EstimateHostScore takes a HostExternalSettings and returns the estimated 503 // score of that host in the hostdb, assuming no penalties for age or uptime. 504 func (hdb *HostDB) EstimateHostScore(entry modules.HostDBEntry, allowance modules.Allowance) modules.HostScoreBreakdown { 505 // TODO: Pass these in as input instead of using the defaults. 506 ug := modules.DefaultUsageGuideLines 507 508 // Grab the adjustments. Age, and uptime penalties are set to '1', to 509 // assume best behavior from the host. 510 collateralReward := hdb.collateralAdjustments(entry, allowance, ug) 511 pricePenalty := hdb.priceAdjustments(entry, allowance, ug) 512 storageRemainingPenalty := storageRemainingAdjustments(entry) 513 versionPenalty := versionAdjustments(entry) 514 515 // Combine into a full penalty, then determine the resulting estimated 516 // score. 517 fullPenalty := collateralReward * pricePenalty * storageRemainingPenalty * versionPenalty 518 estimatedScore := baseWeight.MulFloat(fullPenalty) 519 if estimatedScore.IsZero() { 520 estimatedScore = types.NewCurrency64(1) 521 } 522 523 // Compile the estimates into a host score breakdown. 524 return modules.HostScoreBreakdown{ 525 Score: estimatedScore, 526 ConversionRate: hdb.calculateConversionRate(estimatedScore), 527 528 AgeAdjustment: 1, 529 BurnAdjustment: 1, 530 CollateralAdjustment: collateralReward, 531 PriceAdjustment: pricePenalty, 532 StorageRemainingAdjustment: storageRemainingPenalty, 533 UptimeAdjustment: 1, 534 VersionAdjustment: versionPenalty, 535 } 536 } 537 538 // ScoreBreakdown provdes a detailed set of scalars and bools indicating 539 // elements of the host's overall score. 540 func (hdb *HostDB) ScoreBreakdown(entry modules.HostDBEntry) modules.HostScoreBreakdown { 541 // TODO: Pass these in as input instead of using the defaults. 542 ug := modules.DefaultUsageGuideLines 543 544 hdb.mu.Lock() 545 defer hdb.mu.Unlock() 546 547 score := hdb.weightFunc(entry) 548 return modules.HostScoreBreakdown{ 549 Score: score, 550 ConversionRate: hdb.calculateConversionRate(score), 551 552 AgeAdjustment: hdb.lifetimeAdjustments(entry), 553 BurnAdjustment: 1, 554 CollateralAdjustment: hdb.collateralAdjustments(entry, hdb.allowance, ug), 555 InteractionAdjustment: hdb.interactionAdjustments(entry), 556 PriceAdjustment: hdb.priceAdjustments(entry, hdb.allowance, ug), 557 StorageRemainingAdjustment: storageRemainingAdjustments(entry), 558 UptimeAdjustment: hdb.uptimeAdjustments(entry), 559 VersionAdjustment: versionAdjustments(entry), 560 } 561 }