github.com/johnathanhowell/sia@v0.5.1-beta.0.20160524050156-83dcc3d37c94/modules/renter/proto/downloader.go (about)

     1  package proto
     2  
     3  import (
     4  	"errors"
     5  	"net"
     6  	"time"
     7  
     8  	"github.com/NebulousLabs/Sia/crypto"
     9  	"github.com/NebulousLabs/Sia/encoding"
    10  	"github.com/NebulousLabs/Sia/modules"
    11  	"github.com/NebulousLabs/Sia/types"
    12  )
    13  
    14  // A Downloader retrieves sectors by calling the download RPC on a host.
    15  // Downloaders are NOT thread- safe; calls to Sector must be serialized.
    16  type Downloader struct {
    17  	host     modules.HostDBEntry
    18  	contract modules.RenterContract // updated after each revision
    19  	conn     net.Conn
    20  
    21  	// metrics
    22  	DownloadSpending types.Currency
    23  }
    24  
    25  // Sector retrieves the sector with the specified Merkle root, and revises
    26  // the underlying contract to pay the host proportionally to the data
    27  // retrieve.
    28  func (hd *Downloader) Sector(root crypto.Hash) (modules.RenterContract, []byte, error) {
    29  	extendDeadline(hd.conn, modules.NegotiateDownloadTime)
    30  	defer extendDeadline(hd.conn, time.Hour) // reset deadline when finished
    31  
    32  	// calculate price
    33  	sectorPrice := hd.host.DownloadBandwidthPrice.Mul64(modules.SectorSize)
    34  	if hd.contract.LastRevision.NewValidProofOutputs[0].Value.Cmp(sectorPrice) < 0 {
    35  		return modules.RenterContract{}, nil, errors.New("contract has insufficient funds to support download")
    36  	}
    37  
    38  	// initiate download by confirming host settings
    39  	if err := startDownload(hd.conn, hd.host); err != nil {
    40  		return modules.RenterContract{}, nil, err
    41  	}
    42  
    43  	// send download action
    44  	err := encoding.WriteObject(hd.conn, []modules.DownloadAction{{
    45  		MerkleRoot: root,
    46  		Offset:     0,
    47  		Length:     modules.SectorSize,
    48  	}})
    49  	if err != nil {
    50  		return modules.RenterContract{}, nil, err
    51  	}
    52  
    53  	// create and send revision to host for approval
    54  	rev := newDownloadRevision(hd.contract.LastRevision, sectorPrice)
    55  	signedTxn, err := negotiateRevision(hd.conn, rev, hd.contract.SecretKey)
    56  	if err != nil {
    57  		return modules.RenterContract{}, nil, err
    58  	}
    59  
    60  	// read sector data, completing one iteration of the download loop
    61  	var sectors [][]byte
    62  	if err := encoding.ReadObject(hd.conn, &sectors, modules.SectorSize+16); err != nil {
    63  		return modules.RenterContract{}, nil, err
    64  	} else if len(sectors) != 1 {
    65  		return modules.RenterContract{}, nil, errors.New("host did not send enough sectors")
    66  	}
    67  	sector := sectors[0]
    68  	if uint64(len(sector)) != modules.SectorSize {
    69  		return modules.RenterContract{}, nil, errors.New("host did not send enough sector data")
    70  	} else if crypto.MerkleRoot(sector) != root {
    71  		return modules.RenterContract{}, nil, errors.New("host sent bad sector data")
    72  	}
    73  
    74  	// update contract and metrics
    75  	hd.contract.LastRevision = rev
    76  	hd.contract.LastRevisionTxn = signedTxn
    77  	hd.DownloadSpending = hd.DownloadSpending.Add(sectorPrice)
    78  
    79  	return hd.contract, sector, nil
    80  }
    81  
    82  // Close cleanly terminates the download loop with the host and closes the
    83  // connection.
    84  func (hd *Downloader) Close() error {
    85  	extendDeadline(hd.conn, modules.NegotiateSettingsTime)
    86  	// don't care about these errors
    87  	_, _ = verifySettings(hd.conn, hd.host)
    88  	_ = modules.WriteNegotiationStop(hd.conn)
    89  	return hd.conn.Close()
    90  }
    91  
    92  // NewDownloader initiates the download request loop with a host, and returns a
    93  // Downloader.
    94  func NewDownloader(host modules.HostDBEntry, contract modules.RenterContract) (*Downloader, error) {
    95  	// check that contract has enough value to support a download
    96  	if len(contract.LastRevision.NewValidProofOutputs) != 2 {
    97  		return nil, errors.New("invalid contract")
    98  	}
    99  	if !host.DownloadBandwidthPrice.IsZero() {
   100  		bytes, errOverflow := contract.LastRevision.NewValidProofOutputs[0].Value.Div(host.DownloadBandwidthPrice).Uint64()
   101  		if errOverflow == nil && bytes < modules.SectorSize {
   102  			return nil, errors.New("contract has insufficient funds to support download")
   103  		}
   104  	}
   105  
   106  	// initiate download loop
   107  	conn, err := net.DialTimeout("tcp", string(contract.NetAddress), 15*time.Second)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	// allot 2 minutes for RPC request + revision exchange
   112  	extendDeadline(conn, modules.NegotiateRecentRevisionTime)
   113  	defer extendDeadline(conn, time.Hour)
   114  	if err := encoding.WriteObject(conn, modules.RPCDownload); err != nil {
   115  		return nil, errors.New("couldn't initiate RPC: " + err.Error())
   116  	}
   117  	if err := verifyRecentRevision(conn, contract); err != nil {
   118  		return nil, errors.New("revision exchange failed: " + err.Error())
   119  	}
   120  
   121  	// the host is now ready to accept revisions
   122  	return &Downloader{
   123  		contract: contract,
   124  		host:     host,
   125  		conn:     conn,
   126  	}, nil
   127  }