github.com/fozzysec/SiaPrime@v0.0.0-20190612043147-66c8e8d11fe3/modules/renter/proto/downloader.go (about)

     1  package proto
     2  
     3  import (
     4  	"net"
     5  	"sync"
     6  	"time"
     7  
     8  	"SiaPrime/crypto"
     9  	"SiaPrime/encoding"
    10  	"SiaPrime/modules"
    11  	"SiaPrime/types"
    12  
    13  	"gitlab.com/NebulousLabs/errors"
    14  )
    15  
    16  // A Downloader retrieves sectors by calling the download RPC on a host.
    17  // Downloaders are NOT thread- safe; calls to Sector must be serialized.
    18  type Downloader struct {
    19  	closeChan   chan struct{}
    20  	conn        net.Conn
    21  	contractID  types.FileContractID
    22  	contractSet *ContractSet
    23  	deps        modules.Dependencies
    24  	hdb         hostDB
    25  	host        modules.HostDBEntry
    26  	once        sync.Once
    27  
    28  	height types.BlockHeight
    29  }
    30  
    31  // Sector retrieves the sector with the specified Merkle root, and revises
    32  // the underlying contract to pay the host proportionally to the data
    33  // retrieve.
    34  func (hd *Downloader) Sector(root crypto.Hash) (_ modules.RenterContract, _ []byte, err error) {
    35  	// Reset deadline when finished.
    36  	defer extendDeadline(hd.conn, time.Hour) // TODO: Constant.
    37  
    38  	// Acquire the contract.
    39  	// TODO: why not just lock the SafeContract directly?
    40  	sc, haveContract := hd.contractSet.Acquire(hd.contractID)
    41  	if !haveContract {
    42  		return modules.RenterContract{}, nil, errors.New("contract not present in contract set")
    43  	}
    44  	defer hd.contractSet.Return(sc)
    45  	contract := sc.header // for convenience
    46  
    47  	// calculate price
    48  	sectorPrice := hd.host.DownloadBandwidthPrice.Mul64(modules.SectorSize)
    49  	if contract.RenterFunds().Cmp(sectorPrice) < 0 {
    50  		return modules.RenterContract{}, nil, errors.New("contract has insufficient funds to support download")
    51  	}
    52  	// To mitigate small errors (e.g. differing block heights), fudge the
    53  	// price and collateral by 0.2%.
    54  	sectorPrice = sectorPrice.MulFloat(1 + hostPriceLeeway)
    55  
    56  	// create the download revision
    57  	rev := newDownloadRevision(contract.LastRevision(), sectorPrice)
    58  
    59  	// initiate download by confirming host settings
    60  	extendDeadline(hd.conn, modules.NegotiateSettingsTime)
    61  	if err := startDownload(hd.conn, hd.host); err != nil {
    62  		return modules.RenterContract{}, nil, err
    63  	}
    64  
    65  	// record the change we are about to make to the contract. If we lose power
    66  	// mid-revision, this allows us to restore either the pre-revision or
    67  	// post-revision contract.
    68  	walTxn, err := sc.recordDownloadIntent(rev, sectorPrice)
    69  	if err != nil {
    70  		return modules.RenterContract{}, nil, err
    71  	}
    72  
    73  	// send download action
    74  	extendDeadline(hd.conn, 2*time.Minute) // TODO: Constant.
    75  	err = encoding.WriteObject(hd.conn, []modules.DownloadAction{{
    76  		MerkleRoot: root,
    77  		Offset:     0,
    78  		Length:     modules.SectorSize,
    79  	}})
    80  	if err != nil {
    81  		return modules.RenterContract{}, nil, err
    82  	}
    83  
    84  	// Increase Successful/Failed interactions accordingly
    85  	defer func() {
    86  		if err != nil {
    87  			hd.hdb.IncrementFailedInteractions(contract.HostPublicKey())
    88  			err = errors.Extend(err, modules.ErrHostFault)
    89  		} else if err == nil {
    90  			hd.hdb.IncrementSuccessfulInteractions(contract.HostPublicKey())
    91  		}
    92  	}()
    93  
    94  	// Disrupt before sending the signed revision to the host.
    95  	if hd.deps.Disrupt("InterruptDownloadBeforeSendingRevision") {
    96  		return modules.RenterContract{}, nil,
    97  			errors.New("InterruptDownloadBeforeSendingRevision disrupt")
    98  	}
    99  
   100  	// send the revision to the host for approval
   101  	extendDeadline(hd.conn, connTimeout)
   102  	signedTxn, err := negotiateRevision(hd.conn, rev, contract.SecretKey, hd.height)
   103  	if err == modules.ErrStopResponse {
   104  		// if host gracefully closed, close our connection as well; this will
   105  		// cause the next download to fail. However, we must delay closing
   106  		// until we've finished downloading the sector.
   107  		defer hd.conn.Close()
   108  	} else if err != nil {
   109  		return modules.RenterContract{}, nil, err
   110  	}
   111  
   112  	// Disrupt after sending the signed revision to the host.
   113  	if hd.deps.Disrupt("InterruptDownloadAfterSendingRevision") {
   114  		return modules.RenterContract{}, nil,
   115  			errors.New("InterruptDownloadAfterSendingRevision disrupt")
   116  	}
   117  
   118  	// read sector data, completing one iteration of the download loop
   119  	extendDeadline(hd.conn, modules.NegotiateDownloadTime)
   120  	var sectors [][]byte
   121  	if err := encoding.ReadObject(hd.conn, &sectors, modules.SectorSize+16); err != nil {
   122  		return modules.RenterContract{}, nil, err
   123  	} else if len(sectors) != 1 {
   124  		return modules.RenterContract{}, nil, errors.New("host did not send enough sectors")
   125  	}
   126  	sector := sectors[0]
   127  	if uint64(len(sector)) != modules.SectorSize {
   128  		return modules.RenterContract{}, nil, errors.New("host did not send enough sector data")
   129  	} else if crypto.MerkleRoot(sector) != root {
   130  		return modules.RenterContract{}, nil, errors.New("host sent bad sector data")
   131  	}
   132  
   133  	// update contract and metrics
   134  	if err := sc.commitDownload(walTxn, signedTxn, sectorPrice); err != nil {
   135  		return modules.RenterContract{}, nil, err
   136  	}
   137  
   138  	return sc.Metadata(), sector, nil
   139  }
   140  
   141  // shutdown terminates the revision loop and signals the goroutine spawned in
   142  // NewDownloader to return.
   143  func (hd *Downloader) shutdown() {
   144  	extendDeadline(hd.conn, modules.NegotiateSettingsTime)
   145  	// don't care about these errors
   146  	_, _ = verifySettings(hd.conn, hd.host)
   147  	_ = modules.WriteNegotiationStop(hd.conn)
   148  	close(hd.closeChan)
   149  }
   150  
   151  // Close cleanly terminates the download loop with the host and closes the
   152  // connection.
   153  func (hd *Downloader) Close() error {
   154  	// using once ensures that Close is idempotent
   155  	hd.once.Do(hd.shutdown)
   156  	return hd.conn.Close()
   157  }
   158  
   159  // NewDownloader initiates the download request loop with a host, and returns a
   160  // Downloader.
   161  func (cs *ContractSet) NewDownloader(host modules.HostDBEntry, id types.FileContractID, currentHeight types.BlockHeight, hdb hostDB, cancel <-chan struct{}) (_ *Downloader, err error) {
   162  	sc, ok := cs.Acquire(id)
   163  	if !ok {
   164  		return nil, errors.New("invalid contract")
   165  	}
   166  	defer cs.Return(sc)
   167  	contract := sc.header
   168  
   169  	// check that contract has enough value to support a download
   170  	sectorPrice := host.DownloadBandwidthPrice.Mul64(modules.SectorSize)
   171  	if contract.RenterFunds().Cmp(sectorPrice) < 0 {
   172  		return nil, errors.New("contract has insufficient funds to support download")
   173  	}
   174  
   175  	// Increase Successful/Failed interactions accordingly
   176  	defer func() {
   177  		// A revision mismatch might not be the host's fault.
   178  		if err != nil && !IsRevisionMismatch(err) {
   179  			hdb.IncrementFailedInteractions(contract.HostPublicKey())
   180  			err = errors.Extend(err, modules.ErrHostFault)
   181  		} else if err == nil {
   182  			hdb.IncrementSuccessfulInteractions(contract.HostPublicKey())
   183  		}
   184  	}()
   185  
   186  	conn, closeChan, err := initiateRevisionLoop(host, sc, modules.RPCDownload, cancel, cs.rl)
   187  	if err != nil {
   188  		return nil, errors.AddContext(err, "failed to initiate revision loop")
   189  	}
   190  	// if we succeeded, we can safely discard the unappliedTxns
   191  	for _, txn := range sc.unappliedTxns {
   192  		txn.SignalUpdatesApplied()
   193  	}
   194  	sc.unappliedTxns = nil
   195  
   196  	// the host is now ready to accept revisions
   197  	return &Downloader{
   198  		contractID:  id,
   199  		contractSet: cs,
   200  		host:        host,
   201  		conn:        conn,
   202  		closeChan:   closeChan,
   203  		deps:        cs.deps,
   204  		hdb:         hdb,
   205  
   206  		height: currentHeight,
   207  	}, nil
   208  }