gitlab.com/SkynetLabs/skyd@v1.6.9/skymodules/renter/proto/downloader.go (about)

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