github.com/nebulouslabs/sia@v1.3.7/modules/renter/proto/downloader.go (about)

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