gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/renter/contractor/contractor.go (about) 1 package contractor 2 3 import ( 4 "fmt" 5 "os" 6 "path/filepath" 7 "sync" 8 "sync/atomic" 9 10 "gitlab.com/NebulousLabs/errors" 11 12 "gitlab.com/SiaPrime/SiaPrime/build" 13 "gitlab.com/SiaPrime/SiaPrime/modules" 14 "gitlab.com/SiaPrime/SiaPrime/modules/renter/proto" 15 "gitlab.com/SiaPrime/SiaPrime/persist" 16 siasync "gitlab.com/SiaPrime/SiaPrime/sync" 17 "gitlab.com/SiaPrime/SiaPrime/types" 18 ) 19 20 var ( 21 errNilCS = errors.New("cannot create contractor with nil consensus set") 22 errNilTpool = errors.New("cannot create contractor with nil transaction pool") 23 errNilWallet = errors.New("cannot create contractor with nil wallet") 24 25 // COMPATv1.0.4-lts 26 // metricsContractID identifies a special contract that contains aggregate 27 // financial metrics from older contractors 28 metricsContractID = types.FileContractID{'m', 'e', 't', 'r', 'i', 'c', 's'} 29 ) 30 31 // A Contractor negotiates, revises, renews, and provides access to file 32 // contracts. 33 type Contractor struct { 34 // dependencies 35 cs consensusSet 36 hdb hostDB 37 log *persist.Logger 38 mu sync.RWMutex 39 persist persister 40 staticDeps modules.Dependencies 41 tg siasync.ThreadGroup 42 tpool transactionPool 43 wallet wallet 44 45 // Only one thread should be performing contract maintenance at a time. 46 interruptMaintenance chan struct{} 47 maintenanceLock siasync.TryMutex 48 49 // Only one thread should be scanning the blockchain for recoverable 50 // contracts at a time. 51 atomicScanInProgress uint32 52 atomicRecoveryScanHeight int64 53 54 allowance modules.Allowance 55 blockHeight types.BlockHeight 56 currentPeriod types.BlockHeight 57 lastChange modules.ConsensusChangeID 58 59 // recentRecoveryChange is the first ConsensusChange that was missed while 60 // trying to find recoverable contracts. This is where we need to start 61 // rescanning the blockchain for recoverable contracts the next time the wallet 62 // is unlocked. 63 recentRecoveryChange modules.ConsensusChangeID 64 65 downloaders map[types.FileContractID]*hostDownloader 66 editors map[types.FileContractID]*hostEditor 67 sessions map[types.FileContractID]*hostSession 68 numFailedRenews map[types.FileContractID]types.BlockHeight 69 pubKeysToContractID map[string]types.FileContractID 70 renewing map[types.FileContractID]bool // prevent revising during renewal 71 72 // renewedFrom links the new contract's ID to the old contract's ID 73 // renewedTo links the old contract's ID to the new contract's ID 74 staticContracts *proto.ContractSet 75 oldContracts map[types.FileContractID]modules.RenterContract 76 recoverableContracts map[types.FileContractID]modules.RecoverableContract 77 renewedFrom map[types.FileContractID]types.FileContractID 78 renewedTo map[types.FileContractID]types.FileContractID 79 } 80 81 // Allowance returns the current allowance. 82 func (c *Contractor) Allowance() modules.Allowance { 83 c.mu.RLock() 84 defer c.mu.RUnlock() 85 return c.allowance 86 } 87 88 // InitRecoveryScan starts scanning the whole blockchain for recoverable 89 // contracts within a separate thread. 90 func (c *Contractor) InitRecoveryScan() (err error) { 91 if err := c.tg.Add(); err != nil { 92 return err 93 } 94 defer c.tg.Done() 95 return c.managedInitRecoveryScan(modules.ConsensusChangeBeginning) 96 } 97 98 // PeriodSpending returns the amount spent on contracts during the current 99 // billing period. 100 func (c *Contractor) PeriodSpending() modules.ContractorSpending { 101 allContracts := c.staticContracts.ViewAll() 102 c.mu.RLock() 103 defer c.mu.RUnlock() 104 105 var spending modules.ContractorSpending 106 for _, contract := range allContracts { 107 // Calculate ContractFees 108 spending.ContractFees = spending.ContractFees.Add(contract.ContractFee) 109 spending.ContractFees = spending.ContractFees.Add(contract.TxnFee) 110 spending.ContractFees = spending.ContractFees.Add(contract.SiafundFee) 111 // Calculate TotalAllocated 112 spending.TotalAllocated = spending.TotalAllocated.Add(contract.TotalCost) 113 spending.ContractSpendingDeprecated = spending.TotalAllocated 114 // Calculate Spending 115 spending.DownloadSpending = spending.DownloadSpending.Add(contract.DownloadSpending) 116 spending.UploadSpending = spending.UploadSpending.Add(contract.UploadSpending) 117 spending.StorageSpending = spending.StorageSpending.Add(contract.StorageSpending) 118 } 119 120 // Calculate needed spending to be reported from old contracts 121 for _, contract := range c.oldContracts { 122 host, exist := c.hdb.Host(contract.HostPublicKey) 123 if contract.StartHeight >= c.currentPeriod { 124 // Calculate spending from contracts that were renewed during the current period 125 // Calculate ContractFees 126 spending.ContractFees = spending.ContractFees.Add(contract.ContractFee) 127 spending.ContractFees = spending.ContractFees.Add(contract.TxnFee) 128 spending.ContractFees = spending.ContractFees.Add(contract.SiafundFee) 129 // Calculate TotalAllocated 130 spending.TotalAllocated = spending.TotalAllocated.Add(contract.TotalCost) 131 // Calculate Spending 132 spending.DownloadSpending = spending.DownloadSpending.Add(contract.DownloadSpending) 133 spending.UploadSpending = spending.UploadSpending.Add(contract.UploadSpending) 134 spending.StorageSpending = spending.StorageSpending.Add(contract.StorageSpending) 135 } else if exist && contract.EndHeight+host.WindowSize+types.MaturityDelay > c.blockHeight { 136 // Calculate funds that are being withheld in contracts 137 spending.WithheldFunds = spending.WithheldFunds.Add(contract.RenterFunds) 138 // Record the largest window size for worst case when reporting the spending 139 if contract.EndHeight+host.WindowSize+types.MaturityDelay >= spending.ReleaseBlock { 140 spending.ReleaseBlock = contract.EndHeight + host.WindowSize + types.MaturityDelay 141 } 142 // Calculate Previous spending 143 spending.PreviousSpending = spending.PreviousSpending.Add(contract.ContractFee).Add(contract.TxnFee). 144 Add(contract.SiafundFee).Add(contract.DownloadSpending).Add(contract.UploadSpending).Add(contract.StorageSpending) 145 } else { 146 // Calculate Previous spending 147 spending.PreviousSpending = spending.PreviousSpending.Add(contract.ContractFee).Add(contract.TxnFee). 148 Add(contract.SiafundFee).Add(contract.DownloadSpending).Add(contract.UploadSpending).Add(contract.StorageSpending) 149 } 150 } 151 152 // Calculate amount of spent money to get unspent money. 153 allSpending := spending.ContractFees 154 allSpending = allSpending.Add(spending.DownloadSpending) 155 allSpending = allSpending.Add(spending.UploadSpending) 156 allSpending = allSpending.Add(spending.StorageSpending) 157 if c.allowance.Funds.Cmp(allSpending) >= 0 { 158 spending.Unspent = c.allowance.Funds.Sub(allSpending) 159 } 160 161 return spending 162 } 163 164 // CurrentPeriod returns the height at which the current allowance period 165 // began. 166 func (c *Contractor) CurrentPeriod() types.BlockHeight { 167 c.mu.RLock() 168 defer c.mu.RUnlock() 169 return c.currentPeriod 170 } 171 172 // RateLimits sets the bandwidth limits for connections created by the 173 // contractSet. 174 func (c *Contractor) RateLimits() (readBPW int64, writeBPS int64, packetSize uint64) { 175 return c.staticContracts.RateLimits() 176 } 177 178 // RecoveryScanStatus returns a bool indicating if a scan for recoverable 179 // contracts is in progress and if it is, the current progress of the scan. 180 func (c *Contractor) RecoveryScanStatus() (bool, types.BlockHeight) { 181 bh := types.BlockHeight(atomic.LoadInt64(&c.atomicRecoveryScanHeight)) 182 sip := atomic.LoadUint32(&c.atomicScanInProgress) 183 return sip == 1, bh 184 } 185 186 // RefreshedContract returns a bool indicating if the contract was a refreshed 187 // contract. A refreshed contract refers to a contract that ran out of funds 188 // prior to the end height and so was renewed with the host in the same period. 189 // Both the old and the new contract have the same end height 190 func (c *Contractor) RefreshedContract(fcid types.FileContractID) bool { 191 // Add thread and acquire lock 192 if err := c.tg.Add(); err != nil { 193 return false 194 } 195 defer c.tg.Done() 196 c.mu.RLock() 197 defer c.mu.RUnlock() 198 199 // Check if contract ID is found in the renewedTo map indicating that the 200 // contract was renewed 201 newFCID, renewed := c.renewedTo[fcid] 202 if !renewed { 203 return renewed 204 } 205 206 // Grab the contract to check its end height 207 contract, ok := c.oldContracts[fcid] 208 if !ok { 209 build.Critical("contract not found in oldContracts, this should never happen") 210 return renewed 211 } 212 213 // Grab the contract it was renewed to to check its end height 214 newContract, ok := c.staticContracts.View(newFCID) 215 if !ok { 216 newContract, ok = c.oldContracts[newFCID] 217 if !ok { 218 build.Critical("contract not tracked in staticContracts of old contracts, this should never happen") 219 return renewed 220 } 221 } 222 223 // The contract was refreshed if the endheights are the same 224 return newContract.EndHeight == contract.EndHeight 225 } 226 227 // SetRateLimits sets the bandwidth limits for connections created by the 228 // contractSet. 229 func (c *Contractor) SetRateLimits(readBPS int64, writeBPS int64, packetSize uint64) { 230 c.staticContracts.SetRateLimits(readBPS, writeBPS, packetSize) 231 } 232 233 // Close closes the Contractor. 234 func (c *Contractor) Close() error { 235 return c.tg.Stop() 236 } 237 238 // New returns a new Contractor. 239 func New(cs consensusSet, wallet walletShim, tpool transactionPool, hdb hostDB, persistDir string) (*Contractor, error) { 240 // Check for nil inputs. 241 if cs == nil { 242 return nil, errNilCS 243 } 244 if wallet == nil { 245 return nil, errNilWallet 246 } 247 if tpool == nil { 248 return nil, errNilTpool 249 } 250 251 // Create the persist directory if it does not yet exist. 252 if err := os.MkdirAll(persistDir, 0700); err != nil { 253 return nil, err 254 } 255 256 // Convert the old persist file(s), if necessary. This must occur before 257 // loading the contract set. 258 if err := convertPersist(persistDir); err != nil { 259 return nil, err 260 } 261 262 // Create the contract set. 263 contractSet, err := proto.NewContractSet(filepath.Join(persistDir, "contracts"), modules.ProdDependencies) 264 if err != nil { 265 return nil, err 266 } 267 // Create the logger. 268 logger, err := persist.NewFileLogger(filepath.Join(persistDir, "contractor.log")) 269 if err != nil { 270 return nil, err 271 } 272 273 // Create Contractor using production dependencies. 274 return NewCustomContractor(cs, &WalletBridge{W: wallet}, tpool, hdb, contractSet, NewPersist(persistDir), logger, modules.ProdDependencies) 275 } 276 277 // NewCustomContractor creates a Contractor using the provided dependencies. 278 func NewCustomContractor(cs consensusSet, w wallet, tp transactionPool, hdb hostDB, contractSet *proto.ContractSet, p persister, l *persist.Logger, deps modules.Dependencies) (*Contractor, error) { 279 // Create the Contractor object. 280 c := &Contractor{ 281 cs: cs, 282 staticDeps: deps, 283 hdb: hdb, 284 log: l, 285 persist: p, 286 tpool: tp, 287 wallet: w, 288 289 interruptMaintenance: make(chan struct{}), 290 291 staticContracts: contractSet, 292 downloaders: make(map[types.FileContractID]*hostDownloader), 293 editors: make(map[types.FileContractID]*hostEditor), 294 sessions: make(map[types.FileContractID]*hostSession), 295 oldContracts: make(map[types.FileContractID]modules.RenterContract), 296 recoverableContracts: make(map[types.FileContractID]modules.RecoverableContract), 297 pubKeysToContractID: make(map[string]types.FileContractID), 298 renewing: make(map[types.FileContractID]bool), 299 renewedFrom: make(map[types.FileContractID]types.FileContractID), 300 renewedTo: make(map[types.FileContractID]types.FileContractID), 301 } 302 303 // Close the contract set and logger upon shutdown. 304 c.tg.AfterStop(func() { 305 if err := c.staticContracts.Close(); err != nil { 306 c.log.Println("Failed to close contract set:", err) 307 } 308 if err := c.log.Close(); err != nil { 309 fmt.Println("Failed to close the contractor logger:", err) 310 } 311 }) 312 313 // Load the prior persistence structures. 314 err := c.load() 315 if err != nil && !os.IsNotExist(err) { 316 return nil, err 317 } 318 319 // Initialize the contractIDToPubKey map 320 for _, contract := range c.oldContracts { 321 c.pubKeysToContractID[contract.HostPublicKey.String()] = contract.ID 322 } 323 for _, contract := range c.staticContracts.ViewAll() { 324 c.pubKeysToContractID[contract.HostPublicKey.String()] = contract.ID 325 } 326 327 // Subscribe to the consensus set. 328 err = cs.ConsensusSetSubscribe(c, c.lastChange, c.tg.StopChan()) 329 if err == modules.ErrInvalidConsensusChangeID { 330 // Reset the contractor consensus variables and try rescanning. 331 c.blockHeight = 0 332 c.lastChange = modules.ConsensusChangeBeginning 333 c.recentRecoveryChange = modules.ConsensusChangeBeginning 334 err = cs.ConsensusSetSubscribe(c, c.lastChange, c.tg.StopChan()) 335 } 336 if err != nil { 337 return nil, errors.New("contractor subscription failed: " + err.Error()) 338 } 339 // Unsubscribe from the consensus set upon shutdown. 340 c.tg.OnStop(func() { 341 cs.Unsubscribe(c) 342 }) 343 344 // We may have upgraded persist or resubscribed. Save now so that we don't 345 // lose our work. 346 c.mu.Lock() 347 err = c.save() 348 c.mu.Unlock() 349 if err != nil { 350 return nil, err 351 } 352 353 // Update the allowance in the hostdb with the one that was loaded from 354 // disk. 355 err = c.hdb.SetAllowance(c.allowance) 356 if err != nil { 357 return nil, err 358 } 359 return c, nil 360 } 361 362 // managedInitRecoveryScan starts scanning the whole blockchain at a certain 363 // ChangeID for recoverable contracts within a separate thread. 364 func (c *Contractor) managedInitRecoveryScan(scanStart modules.ConsensusChangeID) (err error) { 365 // Check if we are already scanning the blockchain. 366 if !atomic.CompareAndSwapUint32(&c.atomicScanInProgress, 0, 1) { 367 return errors.New("scan for recoverable contracts is already in progress") 368 } 369 // Reset the progress and status if there was an error. 370 defer func() { 371 if err != nil { 372 c.log.Debug("Setting ScanInProgress and RecoveryScanHeight to zero due to ") 373 c.log.Debugf("Error during managedInitRecoveryScan: %+v", err) 374 atomic.StoreUint32(&c.atomicScanInProgress, 0) 375 atomic.StoreInt64(&c.atomicRecoveryScanHeight, 0) 376 } 377 }() 378 // Get the wallet seed. 379 s, _, err := c.wallet.PrimarySeed() 380 if err != nil { 381 return errors.AddContext(err, "failed to get wallet seed") 382 } 383 // Get the renter seed and wipe it once done. 384 rs := proto.DeriveRenterSeed(s) 385 // Reset the scan progress before starting the scan. 386 atomic.StoreInt64(&c.atomicRecoveryScanHeight, 0) 387 // Create the scanner. 388 scanner := c.newRecoveryScanner(rs) 389 // Start the scan. 390 go func() { 391 // Add scanning thread to threadgroup. 392 if err := c.tg.Add(); err != nil { 393 return 394 } 395 defer c.tg.Done() 396 // Scan blockchain. 397 if err := scanner.threadedScan(c.cs, scanStart, c.tg.StopChan()); err != nil { 398 c.log.Println("Scan failed", err) 399 } 400 if c.staticDeps.Disrupt("disableRecoveryStatusReset") { 401 return 402 } 403 // Reset the scan related fields. 404 if !atomic.CompareAndSwapUint32(&c.atomicScanInProgress, 1, 0) { 405 build.Critical("finished recovery scan but scanInProgress was already set to 0") 406 } 407 atomic.StoreInt64(&c.atomicRecoveryScanHeight, 0) 408 // Save the renter. 409 c.mu.Lock() 410 c.save() 411 c.mu.Unlock() 412 }() 413 return nil 414 }