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  }