github.com/avahowell/sia@v0.5.1-beta.0.20160524050156-83dcc3d37c94/modules/renter/contractor/formcontract.go (about)

     1  package contractor
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/NebulousLabs/Sia/modules"
     9  	"github.com/NebulousLabs/Sia/modules/renter/proto"
    10  	"github.com/NebulousLabs/Sia/types"
    11  )
    12  
    13  var (
    14  	// the contractor will not form contracts above this price
    15  	maxStoragePrice = types.SiacoinPrecision.Mul64(500e3).Mul(modules.BlockBytesPerMonthTerabyte) // 500k SC / TB / Month
    16  	// the contractor will not download data above this price (3x the maximum monthly storage price)
    17  	maxDownloadPrice = maxStoragePrice.Mul64(3 * 4320)
    18  
    19  	errInsufficientAllowance = errors.New("allowance is not large enough to perform contract creation")
    20  	errTooExpensive          = errors.New("host price was too high")
    21  )
    22  
    23  // managedNewContract negotiates an initial file contract with the specified
    24  // host, saves it, and returns it.
    25  func (c *Contractor) managedNewContract(host modules.HostDBEntry, filesize uint64, endHeight types.BlockHeight) (modules.RenterContract, error) {
    26  	// reject hosts that are too expensive
    27  	if host.StoragePrice.Cmp(maxStoragePrice) > 0 {
    28  		return modules.RenterContract{}, errTooExpensive
    29  	}
    30  
    31  	// get an address to use for negotiation
    32  	uc, err := c.wallet.NextAddress()
    33  	if err != nil {
    34  		return modules.RenterContract{}, err
    35  	}
    36  
    37  	// create contract params
    38  	c.mu.RLock()
    39  	params := proto.ContractParams{
    40  		Host:          host,
    41  		Filesize:      filesize,
    42  		StartHeight:   c.blockHeight,
    43  		EndHeight:     endHeight,
    44  		RefundAddress: uc.UnlockHash(),
    45  	}
    46  	c.mu.RUnlock()
    47  
    48  	// create transaction builder
    49  	txnBuilder := c.wallet.StartTransaction()
    50  
    51  	contract, err := proto.FormContract(params, txnBuilder, c.tpool)
    52  	if err != nil {
    53  		txnBuilder.Drop()
    54  		return modules.RenterContract{}, err
    55  	}
    56  
    57  	c.mu.Lock()
    58  	c.contracts[contract.ID] = contract
    59  	c.saveSync()
    60  	c.mu.Unlock()
    61  
    62  	contractValue := contract.LastRevision.NewValidProofOutputs[0].Value
    63  	c.log.Printf("Formed contract with %v for %v SC", host.NetAddress, contractValue.Div(types.SiacoinPrecision))
    64  
    65  	return contract, nil
    66  }
    67  
    68  // formContracts forms contracts with hosts using the allowance parameters.
    69  func (c *Contractor) formContracts(a modules.Allowance) error {
    70  	// Sample at least 10 hosts.
    71  	nRandomHosts := 2 * int(a.Hosts)
    72  	if nRandomHosts < 10 {
    73  		nRandomHosts = 10
    74  	}
    75  	hosts := c.hdb.RandomHosts(nRandomHosts, nil)
    76  	if uint64(len(hosts)) < a.Hosts/2 { // TODO: /2 is temporary until more hosts are online
    77  		return errors.New("not enough hosts")
    78  	}
    79  	// Calculate average host price.
    80  	var sum types.Currency
    81  	for _, h := range hosts {
    82  		sum = sum.Add(h.StoragePrice)
    83  	}
    84  	avgPrice := sum.Div64(uint64(len(hosts)))
    85  
    86  	// Check that allowance is sufficient to store at least one sector per
    87  	// host for the specified duration.
    88  	costPerSector := avgPrice.Mul64(a.Hosts).Mul64(modules.SectorSize).Mul64(uint64(a.Period))
    89  	if a.Funds.Cmp(costPerSector) < 0 {
    90  		return errInsufficientAllowance
    91  	}
    92  
    93  	// Calculate the filesize of the contracts by using the average host price
    94  	// and rounding down to the nearest sector.
    95  	numSectors, err := a.Funds.Div(costPerSector).Uint64()
    96  	if err != nil {
    97  		// if there was an overflow, something is definitely wrong
    98  		return errors.New("allowance resulted in unexpectedly large contract size")
    99  	}
   100  	filesize := numSectors * modules.SectorSize
   101  
   102  	c.mu.RLock()
   103  	endHeight := c.blockHeight + a.Period
   104  	c.mu.RUnlock()
   105  
   106  	// Form contracts with each host.
   107  	var numContracts uint64
   108  	var errs []string
   109  	for _, h := range hosts {
   110  		_, err := c.managedNewContract(h, filesize, endHeight)
   111  		if err != nil {
   112  			errs = append(errs, fmt.Sprintf("\t%v: %v", h.NetAddress, err))
   113  			continue
   114  		}
   115  		if numContracts++; numContracts >= a.Hosts {
   116  			break
   117  		}
   118  	}
   119  	// If we couldn't form any contracts, return an error. Otherwise, just log
   120  	// the failures.
   121  	// TODO: is there a better way to handle failure here? Should we prefer an
   122  	// all-or-nothing approach? We can't pick new hosts to negotiate with
   123  	// because they'll probably be more expensive than we can afford.
   124  	if numContracts == 0 {
   125  		return errors.New("could not form any contracts:\n" + strings.Join(errs, "\n"))
   126  	} else if numContracts < a.Hosts {
   127  		c.log.Printf("WARN: failed to form desired number of contracts (wanted %v, got %v):\n%v", a.Hosts, numContracts, strings.Join(errs, "\n"))
   128  	}
   129  	c.mu.Lock()
   130  	c.renewHeight = endHeight
   131  	c.mu.Unlock()
   132  	return nil
   133  }