gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/contractor/allowance.go (about) 1 package contractor 2 3 import ( 4 "errors" 5 "fmt" 6 "reflect" 7 8 "gitlab.com/SkynetLabs/skyd/skymodules" 9 "go.sia.tech/siad/types" 10 ) 11 12 var ( 13 // ErrAllowanceZeroFunds is returned if the allowance funds are being set to 14 // zero when not cancelling the allowance 15 ErrAllowanceZeroFunds = errors.New("funds must be non-zero") 16 // ErrAllowanceZeroPeriod is returned if the allowance period is being set 17 // to zero when not cancelling the allowance 18 ErrAllowanceZeroPeriod = errors.New("period must be non-zero") 19 // ErrAllowanceZeroWindow is returned if the allowance renew window is being 20 // set to zero when not cancelling the allowance 21 ErrAllowanceZeroWindow = errors.New("renew window must be non-zero") 22 // ErrAllowanceNoHosts is returned if the allowance hosts are being set to 23 // zero when not cancelling the allowance 24 ErrAllowanceNoHosts = errors.New("hosts must be non-zero") 25 // ErrAllowanceZeroExpectedStorage is returned if the allowance expected 26 // storage is being set to zero when not cancelling the allowance 27 ErrAllowanceZeroExpectedStorage = errors.New("expected storage must be non-zero") 28 // ErrAllowanceZeroExpectedUpload is returned if the allowance expected 29 // upload is being set to zero when not cancelling the allowance 30 ErrAllowanceZeroExpectedUpload = errors.New("expected upload must be non-zero") 31 // ErrAllowanceZeroExpectedDownload is returned if the allowance expected 32 // download is being set to zero when not cancelling the allowance 33 ErrAllowanceZeroExpectedDownload = errors.New("expected download must be non-zero") 34 // ErrAllowanceZeroExpectedRedundancy is returned if the allowance expected 35 // redundancy is being set to zero when not cancelling the allowance 36 ErrAllowanceZeroExpectedRedundancy = errors.New("expected redundancy must be non-zero") 37 // ErrAllowanceZeroMaxPeriodChurn is returned if the allowance max period 38 // churn is being set to zero when not cancelling the allowance 39 ErrAllowanceZeroMaxPeriodChurn = errors.New("max period churn must be non-zero") 40 ) 41 42 // SetAllowance sets the amount of money the Contractor is allowed to spend on 43 // contracts over a given time period, divided among the number of hosts 44 // specified. Note that Contractor can start forming contracts as soon as 45 // SetAllowance is called; that is, it may block. 46 // 47 // In most cases, SetAllowance will renew existing contracts instead of 48 // forming new ones. This preserves the data on those hosts. When this occurs, 49 // the renewed contracts will atomically replace their previous versions. If 50 // SetAllowance is interrupted, renewed contracts may be lost, though the 51 // allocated funds will eventually be returned. 52 // 53 // If a is the empty allowance, SetAllowance will archive the current contract 54 // set. The contracts cannot be used to create Editors or Downloads, and will 55 // not be renewed. 56 // 57 // NOTE: At this time, transaction fees are not counted towards the allowance. 58 // This means the contractor may spend more than allowance.Funds. 59 func (c *Contractor) SetAllowance(a skymodules.Allowance) error { 60 if reflect.DeepEqual(a, skymodules.Allowance{}) { 61 return c.managedCancelAllowance() 62 } 63 64 c.mu.Lock() 65 noChange := reflect.DeepEqual(a, c.allowance) 66 c.mu.Unlock() 67 if noChange { 68 return nil 69 } 70 71 // sanity checks 72 if a.Funds.Cmp(types.ZeroCurrency) <= 0 { 73 return ErrAllowanceZeroFunds 74 } else if a.Hosts == 0 { 75 return ErrAllowanceNoHosts 76 } else if a.Period == 0 { 77 return ErrAllowanceZeroPeriod 78 } else if a.RenewWindow == 0 { 79 return ErrAllowanceZeroWindow 80 } else if a.ExpectedStorage == 0 { 81 return ErrAllowanceZeroExpectedStorage 82 } else if a.ExpectedUpload == 0 { 83 return ErrAllowanceZeroExpectedUpload 84 } else if a.ExpectedDownload == 0 { 85 return ErrAllowanceZeroExpectedDownload 86 } else if a.ExpectedRedundancy == 0 { 87 return ErrAllowanceZeroExpectedRedundancy 88 } else if a.MaxPeriodChurn == 0 { 89 return ErrAllowanceZeroMaxPeriodChurn 90 } 91 c.staticLog.Println("INFO: setting allowance to", a) 92 93 // Set the current period if the existing allowance is empty. 94 // 95 // When setting the current period we want to ensure that it aligns with the 96 // start and endheights of the contracts as we would expect. To do this we 97 // have to consider the following. First, that the current period value is 98 // incremented by the allowance period, and second, that the total length of 99 // a contract is the period + renew window. This means the that contracts are 100 // always overlapping periods, and we want that overlap to be the renew 101 // window. In order to create this overlap we set the current period as such. 102 // 103 // If the renew window is less than the period the current period is set in 104 // the past by the renew window. 105 // 106 // If the renew window is greater than or equal to the period we set the 107 // current period to the current block height. 108 // 109 // Also remember that we might have to unlock our contracts if the allowance 110 // was set to the empty allowance before. 111 c.mu.Lock() 112 unlockContracts := false 113 if reflect.DeepEqual(c.allowance, skymodules.Allowance{}) { 114 c.currentPeriod = c.blockHeight 115 if a.Period > a.RenewWindow { 116 c.currentPeriod -= a.RenewWindow 117 } 118 unlockContracts = true 119 } 120 c.allowance = a 121 err := c.save() 122 c.mu.Unlock() 123 if err != nil { 124 c.staticLog.Println("Unable to save contractor after setting allowance:", err) 125 } 126 127 // Cycle through all contracts and unlock them again since they might have 128 // been locked by managedCancelAllowance previously. 129 if unlockContracts { 130 ids := c.staticContracts.IDs() 131 for _, id := range ids { 132 contract, exists := c.staticContracts.Acquire(id) 133 if !exists { 134 continue 135 } 136 utility := contract.Utility() 137 utility.Locked = false 138 err := c.callUpdateUtility(contract, utility, false) 139 c.staticContracts.Return(contract) 140 if err != nil { 141 return err 142 } 143 } 144 } 145 146 // Inform the watchdog about the allowance change. 147 c.staticWatchdog.callAllowanceUpdated(a) 148 149 // We changed the allowance successfully. Update the hostdb. 150 err = c.staticHDB.SetAllowance(a) 151 if err != nil { 152 return err 153 } 154 155 // Interrupt any existing maintenance and launch a new round of 156 // maintenance. 157 if err := c.staticTG.Add(); err != nil { 158 return err 159 } 160 go func() { 161 defer c.staticTG.Done() 162 c.callInterruptContractMaintenance() 163 c.threadedContractMaintenance() 164 }() 165 return nil 166 } 167 168 // managedCancelAllowance handles the special case where the allowance is empty. 169 func (c *Contractor) managedCancelAllowance() error { 170 c.staticLog.Println("INFO: canceling allowance") 171 // first need to invalidate any active editors 172 // NOTE: this code is the same as in managedRenewContracts 173 ids := c.staticContracts.IDs() 174 c.mu.Lock() 175 for _, id := range ids { 176 // we aren't renewing, but we don't want new editors or downloaders to 177 // be created 178 c.renewing[id] = true 179 } 180 c.mu.Unlock() 181 defer func() { 182 c.mu.Lock() 183 for _, id := range ids { 184 delete(c.renewing, id) 185 } 186 c.mu.Unlock() 187 }() 188 for _, id := range ids { 189 c.mu.RLock() 190 e, eok := c.editors[id] 191 s, sok := c.sessions[id] 192 c.mu.RUnlock() 193 if eok { 194 e.callInvalidate() 195 } 196 if sok { 197 s.callInvalidate() 198 } 199 } 200 201 // Clear out the allowance and save. 202 c.mu.Lock() 203 c.allowance = skymodules.Allowance{} 204 c.currentPeriod = 0 205 err := c.save() 206 c.mu.Unlock() 207 if err != nil { 208 return err 209 } 210 211 // Issue an interrupt to any in-progress contract maintenance thread. 212 c.callInterruptContractMaintenance() 213 214 // Cycle through all contracts and mark them as !goodForRenew and !goodForUpload 215 ids = c.staticContracts.IDs() 216 for _, id := range ids { 217 contract, exists := c.staticContracts.Acquire(id) 218 if !exists { 219 continue 220 } 221 utility := contract.Utility() 222 msg := fmt.Sprintf("[CONTRACTUTILITY][%v] cancelling allowance, updating contract utility, locked: %v -> true, GFU: %v -> false, GFR: %v -> false, GFRef: %v -> false", id, utility.Locked, utility.GoodForUpload, utility.GoodForRenew, utility.GoodForRefresh) 223 utility.GoodForRefresh = false 224 utility.GoodForRenew = false 225 utility.GoodForUpload = false 226 utility.Locked = true 227 // We ignore churn since this was triggered by a user on purpose. 228 err := c.callUpdateUtility(contract, utility, true) 229 c.staticContracts.Return(contract) 230 if err != nil { 231 return err 232 } 233 c.staticLog.Println(msg) 234 } 235 return nil 236 }