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  }