gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/contractor/maintenancechecks.go (about)

     1  package contractor
     2  
     3  import (
     4  	"math"
     5  	"math/big"
     6  
     7  	"gitlab.com/NebulousLabs/errors"
     8  	"gitlab.com/SkynetLabs/skyd/skymodules"
     9  	"gitlab.com/SkynetLabs/skyd/skymodules/gouging"
    10  	"go.sia.tech/siad/modules"
    11  	"go.sia.tech/siad/persist"
    12  	"go.sia.tech/siad/types"
    13  )
    14  
    15  type utilityUpdateStatus int
    16  
    17  const (
    18  	_ = iota
    19  	noUpdate
    20  	suggestedUtilityUpdate
    21  	necessaryUtilityUpdate
    22  )
    23  
    24  // Merge merges two update statuses into one. Whichever has the higher priority
    25  // wins.
    26  func (us utilityUpdateStatus) Merge(us2 utilityUpdateStatus) utilityUpdateStatus {
    27  	if us > us2 {
    28  		return us
    29  	}
    30  	return us2
    31  }
    32  
    33  // managedCheckHostScore checks host scorebreakdown against minimum accepted
    34  // scores.  forceUpdate is true if the utility change must be taken.
    35  func (c *Contractor) managedCheckHostScore(contract skymodules.RenterContract, sb skymodules.HostScoreBreakdown, minScoreGFR, minScoreGFU types.Currency) (skymodules.ContractUtility, utilityUpdateStatus) {
    36  	c.mu.Lock()
    37  	defer c.mu.Unlock()
    38  
    39  	u := contract.Utility
    40  
    41  	// Check whether the contract is a payment contract. Payment contracts
    42  	// cannot be marked !GFR for poor score.
    43  	var size uint64
    44  	if len(contract.Transaction.FileContractRevisions) > 0 {
    45  		size = contract.Transaction.FileContractRevisions[0].NewFileSize
    46  	}
    47  	paymentContract := !c.allowance.PaymentContractInitialFunding.IsZero() && size == 0
    48  
    49  	// Contract has no utility if the score is poor. Cannot be marked as bad if
    50  	// the contract is a payment contract.
    51  	badScore := !minScoreGFR.IsZero() && sb.Score.Cmp(minScoreGFR) < 0
    52  	if badScore && !paymentContract {
    53  		// Log if the utility has changed.
    54  		if u.GoodForUpload || u.GoodForRenew || u.GoodForRefresh {
    55  			c.staticLog.Printf("[CONTRACTUTILITY][%v] managedCheckHostScore failed, GFU: %v -> false, GFR: %v -> false, GFRef: %v -> false\n", contract.ID, u.GoodForUpload, u.GoodForRenew, u.GoodForRefresh)
    56  			c.staticLog.Println("Min Score:", minScoreGFR)
    57  			c.staticLog.Println("Score:    ", sb.Score)
    58  			c.staticLog.Println("Age Adjustment:        ", sb.AgeAdjustment)
    59  			c.staticLog.Println("Base Price Adjustment: ", sb.BasePriceAdjustment)
    60  			c.staticLog.Println("Burn Adjustment:       ", sb.BurnAdjustment)
    61  			c.staticLog.Println("Collateral Adjustment: ", sb.CollateralAdjustment)
    62  			c.staticLog.Println("Duration Adjustment:   ", sb.DurationAdjustment)
    63  			c.staticLog.Println("Interaction Adjustment:", sb.InteractionAdjustment)
    64  			c.staticLog.Println("Price Adjustment:      ", sb.PriceAdjustment)
    65  			c.staticLog.Println("Storage Adjustment:    ", sb.StorageRemainingAdjustment)
    66  			c.staticLog.Println("Uptime Adjustment:     ", sb.UptimeAdjustment)
    67  			c.staticLog.Println("Version Adjustment:    ", sb.VersionAdjustment)
    68  		}
    69  		u.GoodForUpload = false
    70  		u.GoodForRefresh = false
    71  		u.GoodForRenew = false
    72  
    73  		c.staticLog.Println("Adding contract utility update to churnLimiter queue")
    74  		return u, suggestedUtilityUpdate
    75  	}
    76  
    77  	// Contract should not be used for uploading if the score is poor.
    78  	if !minScoreGFU.IsZero() && sb.Score.Cmp(minScoreGFU) < 0 {
    79  		if u.GoodForUpload {
    80  			c.staticLog.Printf("[CONTRACTUTILITY][%v] managedCheckHostScore failed, poor score, GFU: true -> false\n", contract.ID)
    81  			c.staticLog.Println("Min Score:", minScoreGFU)
    82  			c.staticLog.Println("Score:    ", sb.Score)
    83  			c.staticLog.Println("Age Adjustment:        ", sb.AgeAdjustment)
    84  			c.staticLog.Println("Base Price Adjustment: ", sb.BasePriceAdjustment)
    85  			c.staticLog.Println("Burn Adjustment:       ", sb.BurnAdjustment)
    86  			c.staticLog.Println("Collateral Adjustment: ", sb.CollateralAdjustment)
    87  			c.staticLog.Println("Duration Adjustment:   ", sb.DurationAdjustment)
    88  			c.staticLog.Println("Interaction Adjustment:", sb.InteractionAdjustment)
    89  			c.staticLog.Println("Price Adjustment:      ", sb.PriceAdjustment)
    90  			c.staticLog.Println("Storage Adjustment:    ", sb.StorageRemainingAdjustment)
    91  			c.staticLog.Println("Uptime Adjustment:     ", sb.UptimeAdjustment)
    92  			c.staticLog.Println("Version Adjustment:    ", sb.VersionAdjustment)
    93  		}
    94  		u.GoodForUpload = false
    95  		return u, necessaryUtilityUpdate
    96  	}
    97  	return u, noUpdate
    98  }
    99  
   100  // managedUtilityChecks performs checks on a contract that
   101  // might require marking the contract as !GFR and/or !GFU.
   102  // Returns the new utility and corresponding update status.
   103  func (c *Contractor) managedUtilityChecks(contract skymodules.RenterContract, host skymodules.DecoratedHostDBEntry, sb skymodules.HostScoreBreakdown, minScoreGFU, minScoreGFR types.Currency, endHeight types.BlockHeight) (_ skymodules.ContractUtility, uus utilityUpdateStatus) {
   104  	revision := contract.Transaction.FileContractRevisions[0]
   105  	c.mu.RLock()
   106  	allowance := c.allowance
   107  	blockHeight := c.blockHeight
   108  	renewWindow := c.allowance.RenewWindow
   109  	period := c.allowance.Period
   110  	_, renewed := c.renewedTo[contract.ID]
   111  	c.mu.RUnlock()
   112  
   113  	// Init uus to no update and the utility with the contract's utility.
   114  	// We assume that the contract is good when we start.
   115  	uus = noUpdate
   116  	oldUtility := contract.Utility
   117  	contract.Utility.GoodForRefresh = true
   118  	contract.Utility.GoodForRenew = true
   119  	contract.Utility.GoodForUpload = true
   120  
   121  	// A contract with a dead score should not be used for anything.
   122  	u, needsUpdate := deadScoreCheck(contract, sb.Score, c.staticLog)
   123  	uus = uus.Merge(needsUpdate)
   124  	contract.Utility = contract.Utility.Merge(u)
   125  
   126  	// A contract that has been renewed should be set to !GFU and !GFR.
   127  	u, needsUpdate = renewedCheck(contract, renewed, c.staticLog)
   128  	uus = uus.Merge(needsUpdate)
   129  	contract.Utility = contract.Utility.Merge(u)
   130  
   131  	u, needsUpdate = maxRevisionCheck(contract, revision.NewRevisionNumber, c.staticLog)
   132  	uus = uus.Merge(needsUpdate)
   133  	contract.Utility = contract.Utility.Merge(u)
   134  
   135  	u, needsUpdate = badContractCheck(contract, c.staticLog)
   136  	uus = uus.Merge(needsUpdate)
   137  	contract.Utility = contract.Utility.Merge(u)
   138  
   139  	u, needsUpdate = offlineCheck(contract, host.HostDBEntry, c.staticLog)
   140  	uus = uus.Merge(needsUpdate)
   141  	contract.Utility = contract.Utility.Merge(u)
   142  
   143  	u, needsUpdate = upForRenewalCheck(contract, renewWindow, blockHeight, c.staticLog)
   144  	uus = uus.Merge(needsUpdate)
   145  	contract.Utility = contract.Utility.Merge(u)
   146  
   147  	u, needsUpdate = sufficientFundsCheck(contract, host.HostDBEntry, period, c.staticLog)
   148  	uus = uus.Merge(needsUpdate)
   149  	contract.Utility = contract.Utility.Merge(u)
   150  
   151  	if !c.staticDeps.Disrupt("DisableUDSGougingCheck") {
   152  		u, needsUpdate = outOfStorageCheck(contract, blockHeight, c.staticLog)
   153  		uus = uus.Merge(needsUpdate)
   154  		contract.Utility = contract.Utility.Merge(u)
   155  	}
   156  
   157  	u, needsUpdate = udsGougingCheck(contract, allowance, host, revision.NewFileSize, endHeight, c.staticLog)
   158  	uus = uus.Merge(needsUpdate)
   159  	contract.Utility = contract.Utility.Merge(u)
   160  
   161  	u, needsUpdate = c.managedCheckHostScore(contract, sb, minScoreGFR, minScoreGFU)
   162  	uus = uus.Merge(needsUpdate)
   163  	contract.Utility = contract.Utility.Merge(u)
   164  
   165  	if c.staticDeps.Disrupt("ContractForceNotGoodForUpload") {
   166  		contract.Utility.GoodForUpload = false
   167  		uus = necessaryUtilityUpdate
   168  	}
   169  
   170  	// Log some info about changed utilities.
   171  	if oldUtility.BadContract != contract.Utility.BadContract {
   172  		c.staticLog.Printf("[CONTRACTUTILITY][%v] badContract %v -> %v", contract.ID, oldUtility.BadContract, contract.Utility.BadContract)
   173  	}
   174  	if oldUtility.GoodForRefresh != contract.Utility.GoodForRefresh {
   175  		c.staticLog.Printf("managedUtilityChecks: [%v] goodForRefresh %v -> %v", contract.ID, oldUtility.GoodForRefresh, contract.Utility.GoodForRefresh)
   176  	}
   177  	if oldUtility.GoodForRenew != contract.Utility.GoodForRenew {
   178  		c.staticLog.Printf("[CONTRACTUTILITY][%v] goodForRenew %v -> %v", contract.ID, oldUtility.GoodForRenew, contract.Utility.GoodForRenew)
   179  	}
   180  	if oldUtility.GoodForUpload != contract.Utility.GoodForUpload {
   181  		c.staticLog.Printf("[CONTRACTUTILITY][%v] goodForUpload %v -> %v", contract.ID, oldUtility.GoodForUpload, contract.Utility.GoodForUpload)
   182  	}
   183  
   184  	// If uus is no update but we still changed the utility that means the
   185  	// state of the contract improved. We then set uus to "suggestedUpdate".
   186  	if uus == noUpdate {
   187  		uus = suggestedUtilityUpdate
   188  	}
   189  	return contract.Utility, uus
   190  }
   191  
   192  // managedHostInHostDBCheck checks if the host is in the hostdb and not
   193  // filtered.  Returns true if a check fails and the utility returned must be
   194  // used to update the contract state.
   195  func (c *Contractor) managedHostInHostDBCheck(contract skymodules.RenterContract) (skymodules.DecoratedHostDBEntry, skymodules.ContractUtility, bool) {
   196  	u := contract.Utility
   197  	hostEntry, exists, err := c.staticHDB.Host(contract.HostPublicKey)
   198  	host := skymodules.DecoratedHostDBEntry{HostDBEntry: hostEntry, PriceTable: c.staticHDB.PriceTable(contract.HostPublicKey)}
   199  
   200  	// Contract has no utility if the host is not in the database. Or is
   201  	// filtered by the blacklist or whitelist. Or if there was an error
   202  	if !exists || host.Filtered || err != nil {
   203  		// Log if the utility has changed.
   204  		if u.GoodForUpload || u.GoodForRenew || u.GoodForRefresh {
   205  			c.staticLog.Printf("[CONTRACTUTILITY][%v] managedHostInHostDBCheck failed because found in hostDB: %v, or host is Filtered: %v, GFU: %v -> false, GFR: %v -> false, GFRef: %v -> false\n", contract.ID, exists, host.Filtered, u.GoodForUpload, u.GoodForRenew, u.GoodForRefresh)
   206  		}
   207  		u.GoodForUpload = false
   208  		u.GoodForRefresh = false
   209  		u.GoodForRenew = false
   210  		return host, u, true
   211  	}
   212  
   213  	// TODO: If the host is not in the hostdb, we need to do some sort of rescan
   214  	// to recover the host. The hostdb is not supposed to be dropping hosts that
   215  	// we have formed contracts with. We should do what we can to get the host
   216  	// back.
   217  	return host, u, false
   218  }
   219  
   220  // badContractCheck checks whether the contract has been marked as bad. If the
   221  // contract has been marked as bad, GoodForUpload and GoodForRenew need to be
   222  // set to false to prevent the renter from using this contract.
   223  func badContractCheck(contract skymodules.RenterContract, logger *persist.Logger) (skymodules.ContractUtility, utilityUpdateStatus) {
   224  	u := contract.Utility
   225  	if u.BadContract {
   226  		// Log if the utility has changed.
   227  		if u.GoodForUpload || u.GoodForRenew || u.GoodForRefresh {
   228  			logger.Printf("[CONTRACTUTILITY][%v] badContractCheck failed, GFU: %v -> false, GFR: %v -> false, GFRef: %v -> false\n", contract.ID, u.GoodForUpload, u.GoodForRenew, u.GoodForRefresh)
   229  		}
   230  		u.GoodForUpload = false
   231  		u.GoodForRefresh = false
   232  		u.GoodForRenew = false
   233  		return u, necessaryUtilityUpdate
   234  	}
   235  	return u, noUpdate
   236  }
   237  
   238  // maxRevisionCheck will return a locked utility if the contract has reached its
   239  // max revision.
   240  func maxRevisionCheck(contract skymodules.RenterContract, revisionNumber uint64, logger *persist.Logger) (skymodules.ContractUtility, utilityUpdateStatus) {
   241  	u := contract.Utility
   242  	if revisionNumber == math.MaxUint64 {
   243  		// Log if the utility has changed.
   244  		if u.GoodForUpload || u.GoodForRenew || u.GoodForRefresh {
   245  			logger.Printf("[CONTRACTUTILITY][%v] maxRevisionCheck failed, GFU: %v -> false, GFR: %v -> false, GFRef: %v -> false\n", contract.ID, u.GoodForUpload, u.GoodForRenew, u.GoodForRefresh)
   246  		}
   247  		u.GoodForUpload = false
   248  		u.GoodForRefresh = false
   249  		u.GoodForRenew = false
   250  		u.Locked = true
   251  		return u, necessaryUtilityUpdate
   252  	}
   253  	return u, noUpdate
   254  }
   255  
   256  // offLineCheck checks if the host for this contract is offline.
   257  // Returns true if a check fails and the utility returned must be used to update
   258  // the contract state.
   259  func offlineCheck(contract skymodules.RenterContract, host skymodules.HostDBEntry, log *persist.Logger) (skymodules.ContractUtility, utilityUpdateStatus) {
   260  	u := contract.Utility
   261  	// Contract has no utility if the host is offline.
   262  	if isOffline(host) {
   263  		// Log if the utility has changed.
   264  		if u.GoodForUpload || u.GoodForRenew || u.GoodForRefresh {
   265  			log.Printf("[CONTRACTUTILITY][%v] offlineCheck failed, GFU: %v -> false, GFR: %v -> false, GFRef: %v -> false\n", contract.ID, u.GoodForUpload, u.GoodForRenew, u.GoodForRefresh)
   266  		}
   267  		u.GoodForUpload = false
   268  		u.GoodForRefresh = false
   269  		u.GoodForRenew = false
   270  		return u, necessaryUtilityUpdate
   271  	}
   272  	return u, noUpdate
   273  }
   274  
   275  // outOfStorageCheck checks if the host is running out of storage.
   276  // Returns true if a check fails and the utility returned must be used to update
   277  // the contract state.
   278  func outOfStorageCheck(contract skymodules.RenterContract, blockHeight types.BlockHeight, log *persist.Logger) (skymodules.ContractUtility, utilityUpdateStatus) {
   279  	u := contract.Utility
   280  	// If LastOOSErr has never been set, return false.
   281  	if u.LastOOSErr == 0 {
   282  		return u, noUpdate
   283  	}
   284  	// Contract should not be used for uploading if the host is out of storage.
   285  	if blockHeight-u.LastOOSErr <= oosRetryInterval {
   286  		if u.GoodForUpload {
   287  			log.Printf("[CONTRACTUTILITY][%v] outOfStorageCheck failed, GFU: true -> false\n", contract.ID)
   288  		}
   289  		u.GoodForUpload = false
   290  		return u, necessaryUtilityUpdate
   291  	}
   292  	return u, noUpdate
   293  }
   294  
   295  // deadScoreCheck will return a contract with no utility and a required update
   296  // if the contract has a score <= 1.
   297  func deadScoreCheck(contract skymodules.RenterContract, score types.Currency, logger *persist.Logger) (skymodules.ContractUtility, utilityUpdateStatus) {
   298  	u := contract.Utility
   299  	if score.Cmp(types.NewCurrency64(1)) <= 0 {
   300  		// Log if the utility has changed.
   301  		if u.GoodForUpload || u.GoodForRenew || u.GoodForRefresh {
   302  			logger.Printf("[CONTRACTUTILITY][%v] deadScoreCheck failed, GFU: %v -> false, GFR: %v -> false, GFRef: %v -> false\n", contract.ID, u.GoodForUpload, u.GoodForRenew, u.GoodForRefresh)
   303  		}
   304  		u.GoodForRefresh = false
   305  		u.GoodForUpload = false
   306  		u.GoodForRenew = false
   307  		return u, necessaryUtilityUpdate
   308  	}
   309  	return u, noUpdate
   310  }
   311  
   312  // renewedCheck will return a contract with no utility and a required update if
   313  // the contract has been renewed, no changes otherwise.
   314  func renewedCheck(contract skymodules.RenterContract, renewed bool, logger *persist.Logger) (skymodules.ContractUtility, utilityUpdateStatus) {
   315  	u := contract.Utility
   316  	if renewed {
   317  		// Log if the utility has changed.
   318  		if u.GoodForUpload || u.GoodForRenew || u.GoodForRefresh {
   319  			logger.Printf("[CONTRACTUTILITY][%v] renewedCheck failed, GFU: %v -> false, GFR: %v -> false, GFRef: %v -> false\n", contract.ID, u.GoodForUpload, u.GoodForRenew, u.GoodForRefresh)
   320  		}
   321  		u.GoodForUpload = false
   322  		u.GoodForRefresh = false
   323  		u.GoodForRenew = false
   324  		return u, necessaryUtilityUpdate
   325  	}
   326  	return u, noUpdate
   327  }
   328  
   329  // udsGougingCheck makes sure the host is not price gouging. UDS stands for
   330  // upload, download and sector access.
   331  // NOTE: The contractEndHeight argument is supposed to be the endHeight as
   332  // perceived by the contractor when forming new contracts or renewing them. Not
   333  // the actual endheight of the contract itself.
   334  func udsGougingCheck(contract skymodules.RenterContract, allowance skymodules.Allowance, host skymodules.DecoratedHostDBEntry, contractSize uint64, contractEndHeight types.BlockHeight, logger *persist.Logger) (skymodules.ContractUtility, utilityUpdateStatus) {
   335  	u := contract.Utility
   336  	pt := host.PriceTable
   337  
   338  	// use both gouging checks, download gouging is implicitly checked by both
   339  	// upload gouging checks
   340  	var err1 error
   341  	if pt != nil {
   342  		err1 = gouging.CheckUpload(allowance, *pt)
   343  	}
   344  	err2 := gouging.CheckUploadHES(allowance, pt, host.HostExternalSettings, true)
   345  	if err := errors.Compose(err1, err2); err != nil {
   346  		wasGFU := u.GoodForUpload
   347  		wasGFR := u.GoodForRenew
   348  		wasGFRef := u.GoodForRefresh
   349  
   350  		markBadForRenew := contractSize > 0
   351  		markBadForRefresh := markBadForRenew && contract.EndHeight < contractEndHeight
   352  
   353  		// If the contract contains data don't renew to make sure we
   354  		// don't pay for its storage.
   355  		u.GoodForUpload = false
   356  		if markBadForRenew {
   357  			u.GoodForRenew = false
   358  		}
   359  
   360  		// If the contract contains data and a renew of the contract
   361  		// would lead to its endHeight being extended, don't refresh it
   362  		// anymore.
   363  		if markBadForRefresh {
   364  			u.GoodForRefresh = false
   365  		}
   366  
   367  		// Log if the utility has changed
   368  		if wasGFU || (wasGFR && markBadForRenew) || (wasGFRef && markBadForRefresh) {
   369  			logger.Printf("[CONTRACTUTILITY][%v] gouging check failed, err: %v, GFU: %v -> %v, GFR: %v -> %v, GFRef: %v -> %v\n", contract.ID, err, wasGFU, u.GoodForUpload, wasGFR, u.GoodForRenew, wasGFRef, u.GoodForRefresh)
   370  		}
   371  		return u, necessaryUtilityUpdate
   372  	}
   373  	return u, noUpdate
   374  }
   375  
   376  // sufficientFundsCheck checks if there are enough funds left in the contract
   377  // for uploads.
   378  // Returns true if a check fails and the utility returned must be used to update
   379  // the contract state.
   380  func sufficientFundsCheck(contract skymodules.RenterContract, host skymodules.HostDBEntry, period types.BlockHeight, log *persist.Logger) (skymodules.ContractUtility, utilityUpdateStatus) {
   381  	u := contract.Utility
   382  
   383  	// Contract should not be used for uploading if the contract does
   384  	// not have enough money remaining to perform the upload.
   385  	blockBytes := types.NewCurrency64(modules.SectorSize * uint64(period))
   386  	sectorStoragePrice := host.StoragePrice.Mul(blockBytes)
   387  	sectorUploadBandwidthPrice := host.UploadBandwidthPrice.Mul64(modules.SectorSize)
   388  	sectorDownloadBandwidthPrice := host.DownloadBandwidthPrice.Mul64(modules.SectorSize)
   389  	sectorBandwidthPrice := sectorUploadBandwidthPrice.Add(sectorDownloadBandwidthPrice)
   390  	sectorPrice := sectorStoragePrice.Add(sectorBandwidthPrice)
   391  	percentRemaining, _ := big.NewRat(0, 1).SetFrac(contract.RenterFunds.Big(), contract.TotalCost.Big()).Float64()
   392  	if contract.RenterFunds.Cmp(sectorPrice.Mul64(3)) < 0 || percentRemaining < MinContractFundUploadThreshold {
   393  		if u.GoodForUpload {
   394  			log.Printf("[CONTRACTUTILITY][%v] sufficientFundsCheck failed, insufficient funds, %v | %v, GFU: true -> false\n", contract.ID, contract.RenterFunds.Cmp(sectorPrice.Mul64(3)) < 0, percentRemaining)
   395  		}
   396  		u.GoodForUpload = false
   397  		return u, necessaryUtilityUpdate
   398  	}
   399  	return u, noUpdate
   400  }
   401  
   402  // upForRenewalCheck checks if this contract is up for renewal.
   403  // Returns true if a check fails and the utility returned must be used to update
   404  // the contract state.
   405  func upForRenewalCheck(contract skymodules.RenterContract, renewWindow, blockHeight types.BlockHeight, log *persist.Logger) (skymodules.ContractUtility, utilityUpdateStatus) {
   406  	u := contract.Utility
   407  	// Contract should not be used for uploading if it's halfway through the
   408  	// renew window. That way we don't lose all upload contracts as soon as
   409  	// we hit the renew window and give them some time to be renewed while
   410  	// still uploading. If uploading blocks renews for half a window,
   411  	// uploading will be prevented and the contract will have the remaining
   412  	// window to update.
   413  	if blockHeight+renewWindow/2 >= contract.EndHeight {
   414  		if u.GoodForUpload {
   415  			log.Printf("[CONTRACTUTILITY][%v] upForRenewalCheck failed, it is time to renew the contract, GFU: true -> false\n", contract.ID)
   416  		}
   417  		u.GoodForUpload = false
   418  		return u, necessaryUtilityUpdate
   419  	}
   420  	return u, noUpdate
   421  }