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

     1  package renter
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/opentracing/opentracing-go"
    10  	"gitlab.com/NebulousLabs/fastrand"
    11  	"gitlab.com/SkynetLabs/skyd/build"
    12  	"gitlab.com/SkynetLabs/skyd/persist"
    13  	"gitlab.com/SkynetLabs/skyd/skymodules"
    14  	"go.sia.tech/siad/crypto"
    15  	"go.sia.tech/siad/modules"
    16  	"go.sia.tech/siad/types"
    17  
    18  	"gitlab.com/NebulousLabs/errors"
    19  )
    20  
    21  var (
    22  	// ErrRootNotFound is returned if all workers were unable to recover the
    23  	// root
    24  	ErrRootNotFound = errors.New("workers were unable to recover the data by sector root - all workers failed")
    25  
    26  	// ErrSkylinkDeleted is returned if the registry for a resolver skylink
    27  	// was set to the sentinel value for deleted skylinks.
    28  	ErrSkylinkDeleted = errors.New("resolver skylink was deleted")
    29  
    30  	// ErrProjectTimedOut is returned when the project timed out
    31  	ErrProjectTimedOut = errors.New("project timed out")
    32  
    33  	// pcwsWorkerStateResetTime defines the amount of time that the pcws will
    34  	// wait before resetting / refreshing the worker state, meaning that all of
    35  	// the workers will do another round of HasSector queries on the network.
    36  	pcwsWorkerStateResetTime = build.Select(build.Var{
    37  		Dev:      time.Hour,
    38  		Standard: time.Hour * 9,
    39  		Testing:  time.Minute * 5,
    40  	}).(time.Duration)
    41  
    42  	// pcwsHasSectorTimeout defines the amount of time that the pcws will wait
    43  	// before giving up on receiving a HasSector response from a single worker.
    44  	// This value is set as a global timeout because different download queries
    45  	// that have different timeouts will use the same projectChunkWorkerSet.
    46  	pcwsHasSectorTimeout = build.Select(build.Var{
    47  		Dev:      time.Minute * 1,
    48  		Standard: time.Minute * 3,
    49  		Testing:  time.Second * 30,
    50  	}).(time.Duration)
    51  )
    52  
    53  const (
    54  	// maxOverdriveWorkers defines the maximum amount of overdrive workers to
    55  	// select as part of a worker set
    56  	maxOverdriveWorkers = 10
    57  )
    58  
    59  // pcwsUnresolvedWorker tracks an unresolved worker that is associated with a
    60  // specific projectChunkWorkerSet. The timestamp indicates when the unresolved
    61  // worker is expected to have a resolution, and is an estimate based on historic
    62  // performance from the worker.
    63  type pcwsUnresolvedWorker struct {
    64  	// The expected time that the worker will resolve. A worker is considered
    65  	// resolved if the HasSector job has finished.
    66  	staticExpectedResolvedTime time.Time
    67  
    68  	// The worker that is performing the HasSector job.
    69  	staticWorker *worker
    70  }
    71  
    72  // pcwsWorkerResponse contains a worker's response to a HasSector query. There
    73  // is a list of piece indices where the worker responded that they had the piece
    74  // at that index. There is also an error field that will be set in the event an
    75  // error occurred while performing the HasSector query.
    76  type pcwsWorkerResponse struct {
    77  	worker       *worker
    78  	pieceIndices []uint64
    79  	err          error
    80  }
    81  
    82  // pcwsWorkerState contains the worker state for a single thread that is
    83  // resolving which workers have which pieces. When the projectChunkWorkerSet
    84  // resets, it does so by spinning up a new pcwsWorkerState and then replacing
    85  // the old worker state with the new worker state. The new worker state will
    86  // send out a new round of HasSector queries to the network.
    87  type pcwsWorkerState struct {
    88  	// unresolvedWorkers is the set of workers that are currently running
    89  	// HasSector programs and have not yet finished.
    90  	//
    91  	// A map is used so that workers can be removed from the set in constant
    92  	// time as they complete their HasSector jobs.
    93  	unresolvedWorkers map[string]*pcwsUnresolvedWorker
    94  
    95  	// ResolvedWorkers is an array that tracks which workers have responded to
    96  	// HasSector queries and which sectors are available. This array is only
    97  	// appended to as workers come back, meaning that chunk downloads can track
    98  	// internally which elements of the array they have already looked at,
    99  	// saving computational time when updating.
   100  	resolvedWorkers []pcwsWorkerResponse
   101  
   102  	// workerUpdateChans is used by download objects to block until more
   103  	// information about the unresolved workers is available. All of the worker
   104  	// update chans will be closed each time an unresolved worker returns a
   105  	// response (regardless of whether the response is positive or negative).
   106  	// The array will then be cleared.
   107  	//
   108  	// NOTE: Once 'unresolvedWorkers' has a length of zero, any attempt to add a
   109  	// channel to the set of workerUpdateChans should fail, as there will be no
   110  	// more updates. This is specific to this particular worker state, the
   111  	// pcwsWorkerSet as a whole can be reset by replacing the worker state.
   112  	workerUpdateChans []chan struct{}
   113  
   114  	// Utilities.
   115  	staticDeps       skymodules.SkydDependencies
   116  	staticLog        *persist.Logger
   117  	staticWorkerPool *workerPool
   118  	mu               sync.Mutex
   119  }
   120  
   121  // projectChunkWorkerSet is an object that contains a set of workers that can be
   122  // used to download a single chunk. The object can be initialized with either a
   123  // set of roots (for Skynet downloads) or with a siafile where the host-root
   124  // pairs are already known (for traditional renter downloads).
   125  //
   126  // If the pcws is initialized with only a set of roots, it will immediately spin
   127  // up a bunch of worker jobs to locate those roots on the network using
   128  // HasSector programs.
   129  //
   130  // Once the pcws has been initialized, it can be used repeatedly to download
   131  // data from the chunk, and it will not need to repeat the network lookups.
   132  // Every few hours (pcwsWorkerStateResetTime), it will re-do the lookups to
   133  // ensure that it is up-to-date on the best way to download the file.
   134  type projectChunkWorkerSet struct {
   135  	// workerState is a pointer to a single pcwsWorkerState, specifically the
   136  	// most recent worker state that has launched. The workerState is
   137  	// responsible for querying the network with HasSector requests and
   138  	// determining which workers are able to download which pieces of the chunk.
   139  	//
   140  	// workerStateLaunchTime indicates when the workerState was launched, which
   141  	// is used to figure out when the worker state should be refreshed.
   142  	//
   143  	// updateInProgress and updateFinishedChan are used to ensure that only one
   144  	// worker state is being refreshed at a time. Before a workerState refresh
   145  	// begins, the projectChunkWorkerSet is locked and the updateInProgress
   146  	// value is set to 'true'. At the same time, a new 'updateFinishedChan' is
   147  	// created. Then the projectChunkWorkerSet is unlocked. New threads that try
   148  	// to launch downloads will see that there is an update in progress and will
   149  	// wait on the 'updateFinishedChan' to close before grabbing the new
   150  	// workerState. When the new workerState is done being initialized, the
   151  	// projectChunkWorkerSet is locked and the updateInProgress field is set to
   152  	// false, the workerState is updated to the new state, and the
   153  	// updateFinishedChan is closed.
   154  	updateInProgress      bool
   155  	updateFinishedChan    chan struct{}
   156  	workerState           *pcwsWorkerState
   157  	workerStateLaunchTime time.Time
   158  
   159  	// Decoding and decryption information for the chunk.
   160  	staticChunkIndex   uint64
   161  	staticErasureCoder skymodules.ErasureCoder
   162  	staticMasterKey    crypto.CipherKey
   163  	staticPieceRoots   []crypto.Hash
   164  
   165  	// Stats
   166  	staticBaseSectorDownloadStats   *skymodules.DownloadOverdriveStats
   167  	staticFanoutSectorDownloadStats *skymodules.DownloadOverdriveStats
   168  
   169  	// Utilities
   170  	staticCtx        context.Context
   171  	staticDeps       skymodules.SkydDependencies
   172  	staticWorkerPool *workerPool
   173  	staticLog        *persist.Logger
   174  	mu               sync.Mutex
   175  }
   176  
   177  // chunkFetcher is an interface that exposes a download function, the PCWS
   178  // implements this interface.
   179  type chunkFetcher interface {
   180  	Download(ctx context.Context, pricePerMS types.Currency, offset, length uint64, lowPrio bool) (chan *downloadResponse, error)
   181  }
   182  
   183  // Download will download a range from a chunk.
   184  func (pcws *projectChunkWorkerSet) Download(ctx context.Context, pricePerMS types.Currency, offset, length uint64, lowPrio bool) (chan *downloadResponse, error) {
   185  	return pcws.managedDownload(ctx, pricePerMS, offset, length, lowPrio)
   186  }
   187  
   188  // closeUpdateChans will close all of the update chans and clear out the slice.
   189  // This will cause any threads waiting for more results from the unresolved
   190  // workers to unblock.
   191  //
   192  // Typically there will be a small number of channels, often 0 and often just 1.
   193  func (ws *pcwsWorkerState) closeUpdateChans() {
   194  	for _, c := range ws.workerUpdateChans {
   195  		close(c)
   196  	}
   197  	ws.workerUpdateChans = nil
   198  }
   199  
   200  // registerForWorkerUpdate will create a channel and append it to the list of
   201  // update chans in the worker state. When there is more information available
   202  // about which worker is the best worker to select, the channel will be closed.
   203  func (ws *pcwsWorkerState) registerForWorkerUpdate() <-chan struct{} {
   204  	// Return a nil channel if there are no more unresolved workers.
   205  	if len(ws.unresolvedWorkers) == 0 {
   206  		return nil
   207  	}
   208  
   209  	// Create the channel that will be closed when the set of unresolved workers
   210  	// has been updated.
   211  	c := make(chan struct{})
   212  	ws.workerUpdateChans = append(ws.workerUpdateChans, c)
   213  	return c
   214  }
   215  
   216  // removeUnresolvedWorker removes an unresolved worker from the worker state.
   217  func (ws *pcwsWorkerState) removeUnresolvedWorker(hpk string) {
   218  	uw, exists := ws.unresolvedWorkers[hpk]
   219  	if exists {
   220  		staticPoolUnresolvedWorkers.Put(uw)
   221  		delete(ws.unresolvedWorkers, hpk)
   222  	}
   223  
   224  	// If the map is empty now, release some memory.
   225  	if len(ws.unresolvedWorkers) == 0 {
   226  		ws.unresolvedWorkers = make(map[string]*pcwsUnresolvedWorker)
   227  	}
   228  }
   229  
   230  // managedHandleResponse will handle a HasSector response from a worker,
   231  // updating the workerState accordingly.
   232  //
   233  // The worker's response will be included into the resolvedWorkers even if it is
   234  // emptied or errored because the worker selection algorithms in the downloads
   235  // may wish to be able to view which workers have failed. This is currently
   236  // unused, but certain computational optimizations in the future depend on it.
   237  func (ws *pcwsWorkerState) managedHandleResponse(resp *jobHasSectorResponse) {
   238  	ws.mu.Lock()
   239  	defer ws.mu.Unlock()
   240  
   241  	// Defer closing the update chans to signal we've received and processed an
   242  	// HS response.
   243  	defer ws.closeUpdateChans()
   244  
   245  	// Delete the worker from the set of unresolved workers.
   246  	w := resp.staticWorker
   247  	if w == nil {
   248  		ws.staticLog.Critical("nil worker provided in resp")
   249  	}
   250  
   251  	// Return the worker to the pool and delete it from the map.
   252  	ws.removeUnresolvedWorker(w.staticHostPubKeyStr)
   253  
   254  	// If the response contained an error, add this worker to the set of
   255  	// resolved workers as supporting no indices.
   256  	if resp.staticErr != nil {
   257  		ws.resolvedWorkers = append(ws.resolvedWorkers, pcwsWorkerResponse{
   258  			worker: w,
   259  			err:    resp.staticErr,
   260  		})
   261  		return
   262  	}
   263  
   264  	// Add this worker to the set of resolved workers (even if there are no
   265  	// indices that the worker can fetch).
   266  	ws.resolvedWorkers = append(ws.resolvedWorkers, pcwsWorkerResponse{
   267  		worker:       w,
   268  		pieceIndices: resp.staticAvailbleIndices,
   269  	})
   270  }
   271  
   272  // managedRegisterForWorkerUpdate will create a channel and append it to the
   273  // list of update chans in the worker state. When there is more information
   274  // available about which worker is the best worker to select, the channel will
   275  // be closed.
   276  func (ws *pcwsWorkerState) managedRegisterForWorkerUpdate() <-chan struct{} {
   277  	ws.mu.Lock()
   278  	defer ws.mu.Unlock()
   279  	return ws.registerForWorkerUpdate()
   280  }
   281  
   282  // WaitForResults waits for all workers of the state to resolve up until ctx is
   283  // closed. Once the ctx is closed, all available responses are returned.
   284  func (ws *pcwsWorkerState) WaitForResults(ctx context.Context) []pcwsWorkerResponse {
   285  	for {
   286  		ws.mu.Lock()
   287  		rw := ws.resolvedWorkers
   288  		noUnresolvedWorkers := len(ws.unresolvedWorkers) == 0
   289  		updateChan := ws.registerForWorkerUpdate()
   290  		ws.mu.Unlock()
   291  
   292  		// If there are no more unresolved workers, we are done.
   293  		if noUnresolvedWorkers {
   294  			return rw
   295  		}
   296  
   297  		// Otherwise we wait for either an update or the timeout.
   298  		select {
   299  		case <-updateChan:
   300  			continue
   301  		case <-ctx.Done():
   302  			// Timeout reached. Return what we got.
   303  			return rw
   304  		}
   305  	}
   306  }
   307  
   308  // managedLaunchWorker will launch a job to determine which sectors of a chunk
   309  // are available through that worker. The resulting unresolved worker is
   310  // returned so it can be added to the pending worker state.
   311  func (pcws *projectChunkWorkerSet) managedLaunchWorker(w *worker, responseChan chan *jobHasSectorResponse, ws *pcwsWorkerState) error {
   312  	// Check for gouging.
   313  	cache := w.staticCache()
   314  	pt := w.staticPriceTable().staticPriceTable
   315  	numWorkers := pcws.staticWorkerPool.callNumWorkers()
   316  
   317  	err := staticPCWSGougingCache.IsGouging(w.staticHostPubKeyStr, pt, cache.staticRenterAllowance, numWorkers, len(pcws.staticPieceRoots))
   318  	if err != nil {
   319  		return errors.AddContext(err, fmt.Sprintf("price gouging for chunk worker set detected in worker %v", w.staticHostPubKeyStr))
   320  	}
   321  
   322  	// Check whether the worker is on a cooldown. Because the PCWS is cached, we
   323  	// do not want to exclude this worker if it is on a cooldown, however we do
   324  	// want to take into consideration the cooldown period when we estimate the
   325  	// expected resolve time.
   326  	var coolDownPenalty time.Duration
   327  	if w.managedOnMaintenanceCooldown() {
   328  		wms := w.staticMaintenanceState
   329  		wms.mu.Lock()
   330  		coolDownPenalty = time.Until(wms.cooldownUntil)
   331  		wms.mu.Unlock()
   332  	}
   333  
   334  	// Don't bother scheduling a job if the worker doesn't have a valid
   335  	// price table.
   336  	if wpt := w.staticPriceTable(); !wpt.staticValid() {
   337  		return errInvalidPriceTable
   338  	}
   339  
   340  	// Create and launch the job.
   341  	ctx, cancel := context.WithTimeout(pcws.staticCtx, pcwsHasSectorTimeout)
   342  	jhs := w.newJobHasSectorWithPostExecutionHook(ctx, responseChan, func(resp *jobHasSectorResponse) {
   343  		ws.managedHandleResponse(resp)
   344  		staticPoolJobHasSectorResponse.Put(resp)
   345  		cancel()
   346  	}, pcws.staticErasureCoder.NumPieces(), pcws.staticPieceRoots...)
   347  
   348  	expectedJobTime, err := w.staticJobHasSectorQueue.callAddWithEstimate(jhs, pcwsHasSectorTimeout)
   349  	if err != nil {
   350  		cancel()
   351  		return errors.AddContext(err, fmt.Sprintf("unable to add has sector job to %v", w.staticHostPubKeyStr))
   352  	}
   353  	expectedResolveTime := expectedJobTime.Add(coolDownPenalty)
   354  
   355  	// Create the unresolved worker for this job.
   356  	uw := staticPoolUnresolvedWorkers.Get()
   357  	uw.staticWorker = w
   358  	uw.staticExpectedResolvedTime = expectedResolveTime
   359  
   360  	// Add the unresolved worker to the worker state. Technically this doesn't
   361  	// need to be wrapped in a lock, but that's not obvious from the function
   362  	// context so we wrap it in a lock anyway. There will be no contention, so
   363  	// there should be minimal performance overhead.
   364  	ws.mu.Lock()
   365  	ws.unresolvedWorkers[w.staticHostPubKeyStr] = uw
   366  	ws.mu.Unlock()
   367  	return nil
   368  }
   369  
   370  // managedLaunchWorkers will spin up a bunch of jobs to determine which workers
   371  // have what pieces for the pcws, and then update the input worker state with
   372  // the results.
   373  func (pcws *projectChunkWorkerSet) managedLaunchWorkers(ws *pcwsWorkerState) {
   374  	// Launch all of the HasSector jobs for each worker. A channel is needed to
   375  	// receive the responses, and the channel needs to be buffered to be equal
   376  	// in size to the number of queries so that none of the workers sending
   377  	// reponses get blocked sending down the channel.
   378  	workers := ws.staticWorkerPool.callWorkers()
   379  
   380  	responseChan := make(chan *jobHasSectorResponse, len(workers))
   381  	for _, w := range workers {
   382  		err := pcws.managedLaunchWorker(w, responseChan, ws)
   383  		if err != nil && !errors.Contains(err, errEstimateAboveMax) {
   384  			pcws.staticLog.Debugf("failed to launch worker: %v", err)
   385  		}
   386  	}
   387  }
   388  
   389  // managedWorkerState returns a pointer to the current worker state object
   390  func (pcws *projectChunkWorkerSet) managedWorkerState() *pcwsWorkerState {
   391  	pcws.mu.Lock()
   392  	defer pcws.mu.Unlock()
   393  	return pcws.workerState
   394  }
   395  
   396  // managedTryUpdateWorkerState will check whether the worker state needs to be
   397  // refreshed. If so, it will refresh the worker state.
   398  func (pcws *projectChunkWorkerSet) managedTryUpdateWorkerState() error {
   399  	// The worker state does not need to be refreshed if it is recent or if
   400  	// there is another refresh currently in progress.
   401  	pcws.mu.Lock()
   402  	if pcws.updateInProgress || time.Since(pcws.workerStateLaunchTime) < pcwsWorkerStateResetTime {
   403  		c := pcws.updateFinishedChan
   404  		pcws.mu.Unlock()
   405  		// If there is no update in progress, the channel will already be
   406  		// closed, and therefore listening on the channel will return
   407  		// immediately.
   408  		<-c
   409  		return nil
   410  	}
   411  	// An update is needed. Set the flag that an update is in progress.
   412  	pcws.updateInProgress = true
   413  	pcws.updateFinishedChan = make(chan struct{})
   414  	pcws.mu.Unlock()
   415  
   416  	// Create the new worker state and launch the thread that will create worker
   417  	// jobs and collect responses from the workers.
   418  	//
   419  	// The concurrency here is a bit awkward because jobs cannot be launched
   420  	// while the pcws lock is held, the workerState of the pcws cannot be set
   421  	// until all the jobs are launched, and the context for timing out the
   422  	// worker jobs needs to be created in the same thread that listens for the
   423  	// responses. Though there are a lot of concurrency patterns at play here,
   424  	// it was the cleanest thing I could come up with.
   425  	numWorkers := pcws.staticWorkerPool.callNumWorkers()
   426  	ws := &pcwsWorkerState{
   427  		unresolvedWorkers: make(map[string]*pcwsUnresolvedWorker, numWorkers),
   428  		resolvedWorkers:   make([]pcwsWorkerResponse, 0, numWorkers),
   429  
   430  		staticDeps:       pcws.staticDeps,
   431  		staticLog:        pcws.staticLog,
   432  		staticWorkerPool: pcws.staticWorkerPool,
   433  	}
   434  
   435  	// Launch the thread to find the workers for this launch state.
   436  	pcws.managedLaunchWorkers(ws)
   437  
   438  	// Update the pcws so that the workerState in the pcws is the newest worker
   439  	// state.
   440  	pcws.mu.Lock()
   441  	pcws.updateInProgress = false
   442  	pcws.workerState = ws
   443  	pcws.workerStateLaunchTime = time.Now()
   444  	pcws.mu.Unlock()
   445  	close(pcws.updateFinishedChan)
   446  	return nil
   447  }
   448  
   449  // managedDownload will download a range from a chunk. This call is
   450  // asynchronous. It will return as soon as the initial sector download requests
   451  // have been sent to the workers. This means that it will block until enough
   452  // workers have reported back with HasSector results that the optimal download
   453  // request can be made. Where possible, the projectChunkWorkerSet should be
   454  // created in advance of the download call, so that the HasSector calls have as
   455  // long as possible to complete, reducing the latency of the actual download
   456  // call.
   457  //
   458  // Blocking until all of the piece downloads have been put into job queues
   459  // ensures that the workers will account for the bandwidth overheads associated
   460  // with these jobs before new downloads are requested. Multiple calls to
   461  // 'managedDownload' from the same thread will generally follow the rule that
   462  // the first calls will return first. This rule cannot be enforced if the call
   463  // to managedDownload returns before the download jobs are queued into the
   464  // workers.
   465  //
   466  // pricePerMS is "price per millisecond". This gives the download code a budget
   467  // to spend on faster workers. For example, if a faster set of workers is
   468  // expected to trim 100 milliseconds off of the download time, the download code
   469  // will select those workers only if the additional expense of using those
   470  // workers is less than 100 * pricePerMS.
   471  func (pcws *projectChunkWorkerSet) managedDownload(ctx context.Context, pricePerMS types.Currency, offset, length uint64, lowPrio bool) (chan *downloadResponse, error) {
   472  	// Potentially force a timeout via a disrupt for testing.
   473  	if pcws.staticDeps.Disrupt("timeoutProjectDownloadByRoot") {
   474  		return nil, errors.Compose(ErrProjectTimedOut, ErrRootNotFound)
   475  	}
   476  
   477  	// Convenience variables.
   478  	ec := pcws.staticErasureCoder
   479  
   480  	// Start a span for the PDC.
   481  	_, ctx = opentracing.StartSpanFromContext(ctx, "managedDownload")
   482  
   483  	// Depending on the encryption type we might have to download the entire
   484  	// entire chunk. For the ciphers we support, this will be the case when the
   485  	// overhead is not zero. This is due to the overhead being a checksum that
   486  	// has to be verified against the entire piece.
   487  	//
   488  	// NOTE: These checks assume that any upload with encryption overhead needs
   489  	// to be downloaded as full sectors. This feels reasonable because smaller
   490  	// sectors were not supported when encryption schemes with overhead were
   491  	// being suggested.
   492  	if pcws.staticMasterKey.Type().Overhead() != 0 && (offset != 0 || length != modules.SectorSize*uint64(ec.MinPieces())) {
   493  		return nil, errors.New("invalid request performed - this chunk has encryption overhead and therefore the full chunk must be downloaded")
   494  	}
   495  
   496  	// Refresh the pcws. This will only cause a refresh if one is necessary.
   497  	err := pcws.managedTryUpdateWorkerState()
   498  	if err != nil {
   499  		return nil, errors.AddContext(err, "unable to initiate download")
   500  	}
   501  
   502  	// After refresh, grab the worker state.
   503  	ws := pcws.managedWorkerState()
   504  
   505  	// Determine the offset and length that needs to be downloaded from the
   506  	// pieces. This is non-trivial because both the network itself and also the
   507  	// erasure coder have required segment sizes.
   508  	pieceOffset, pieceLength := GetPieceOffsetAndLen(ec, offset, length)
   509  
   510  	// If the pricePerMS is zero, initialize it to 1H to avoid division by zero,
   511  	// or multiplication by zero, possibly resulting in unwanted side-effects in
   512  	// the worker selection and/or any other algorithms.
   513  	if pricePerMS.IsZero() {
   514  		pricePerMS = types.NewCurrency64(1)
   515  	}
   516  
   517  	// Build static piece indices, chimera workers use this static piece index
   518  	// array to avoid creating it for every chimera worker over and over again.
   519  	pieceIndices := make([]uint64, ec.NumPieces())
   520  	for i := 0; i < len(pieceIndices); i++ {
   521  		pieceIndices[i] = uint64(i)
   522  	}
   523  
   524  	// Build the full pdc.
   525  	pdc := &projectDownloadChunk{
   526  		lengthInChunk: length,
   527  		offsetInChunk: offset,
   528  
   529  		pieceOffset: pieceOffset,
   530  		pieceLength: pieceLength,
   531  
   532  		pricePerMS: pricePerMS,
   533  
   534  		workerProgressMap: make(map[uint32]workerProgress),
   535  
   536  		piecesData:   make([][]byte, ec.NumPieces()),
   537  		piecesProofs: make([][]crypto.Hash, ec.NumPieces()),
   538  		piecesInfo:   make([]pieceInfo, ec.NumPieces()),
   539  
   540  		staticIsLowPrio:  lowPrio,
   541  		staticLaunchTime: time.Now(),
   542  
   543  		staticBaseSectorDownloadStats:   pcws.staticBaseSectorDownloadStats,
   544  		staticFanoutSectorDownloadStats: pcws.staticFanoutSectorDownloadStats,
   545  
   546  		staticPieceIndices:   pieceIndices,
   547  		ctx:                  ctx,
   548  		workerResponseChan:   make(chan *jobReadResponse),
   549  		downloadResponseChan: make(chan *downloadResponse, 1),
   550  		workerSet:            pcws,
   551  		workerState:          ws,
   552  	}
   553  
   554  	// Set debug variables on the pdc
   555  	fastrand.Read(pdc.uid[:])
   556  
   557  	// Launch the download project in a separate go routine
   558  	go pdc.threadedLaunchProjectDownload()
   559  
   560  	return pdc.downloadResponseChan, nil
   561  }
   562  
   563  // newPCWSByRoots will create a worker set to download a chunk given just the
   564  // set of sector roots associated with the pieces. The hosts that correspond to
   565  // the roots will be determined by scanning the network with a large number of
   566  // HasSector queries. Once opened, the projectChunkWorkerSet can be used to
   567  // initiate many downloads. If it is already known what pieces a worker is
   568  // expected to have, it can be provided as a seedWorker. A seedWorker is
   569  // considered to be resolved right away.
   570  func (r *Renter) newPCWSByRoots(ctx context.Context, roots []crypto.Hash, ec skymodules.ErasureCoder, masterKey crypto.CipherKey, chunkIndex uint64) (*projectChunkWorkerSet, error) {
   571  	// Check that the number of roots provided is consistent with the erasure
   572  	// coder provided.
   573  	//
   574  	// NOTE: There's a legacy special case where 1-of-N only needs 1 root.
   575  	if len(roots) != ec.NumPieces() && !(len(roots) == 1 && ec.MinPieces() == 1) {
   576  		return nil, fmt.Errorf("%v roots provided, but erasure coder specifies %v pieces", len(roots), ec.NumPieces())
   577  	}
   578  
   579  	// Check if enough roots are known.
   580  	var knownRoots int
   581  	for _, root := range roots {
   582  		if root != (crypto.Hash{}) {
   583  			knownRoots++
   584  		}
   585  	}
   586  	if knownRoots < ec.MinPieces() {
   587  		return nil, fmt.Errorf("only %v roots are known which is smaller than the minimum of %v", knownRoots, ec.MinPieces())
   588  	}
   589  
   590  	// Check that the given cipher is not nil, if no encryption is required a
   591  	// plain text cipher key should be passed
   592  	if masterKey == nil {
   593  		return nil, errors.New("master key is nil, if no decryption is required pass a plaintext cipher key")
   594  	}
   595  
   596  	// Create the worker set.
   597  	pcws := &projectChunkWorkerSet{
   598  		staticChunkIndex:   chunkIndex,
   599  		staticErasureCoder: ec,
   600  		staticMasterKey:    masterKey,
   601  		staticPieceRoots:   roots,
   602  
   603  		staticBaseSectorDownloadStats:   r.staticBaseSectorDownloadStats,
   604  		staticFanoutSectorDownloadStats: r.staticFanoutSectorDownloadStats,
   605  
   606  		staticCtx:        ctx,
   607  		staticDeps:       r.staticDeps,
   608  		staticLog:        r.staticLog,
   609  		staticWorkerPool: r.staticWorkerPool,
   610  	}
   611  
   612  	// The worker state is blank, ensure that everything can get started.
   613  	err := pcws.managedTryUpdateWorkerState()
   614  	if err != nil {
   615  		return nil, errors.AddContext(err, "cannot create a new PCWS")
   616  	}
   617  
   618  	// Return the worker set.
   619  	return pcws, nil
   620  }