gitlab.com/SiaPrime/SiaPrime@v1.4.1/modules/renter/proto/downloader.go (about)

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