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

     1  package renter
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  
     8  	"github.com/opentracing/opentracing-go"
     9  	"gitlab.com/SkynetLabs/skyd/build"
    10  	"gitlab.com/SkynetLabs/skyd/skykey"
    11  	"gitlab.com/SkynetLabs/skyd/skymodules"
    12  	"go.sia.tech/siad/crypto"
    13  	"go.sia.tech/siad/modules"
    14  	"go.sia.tech/siad/types"
    15  
    16  	"gitlab.com/NebulousLabs/errors"
    17  )
    18  
    19  var (
    20  	// SkylinkDataSourceRequestSize is the size that is suggested by the data
    21  	// source to be used when reading data from it.
    22  	SkylinkDataSourceRequestSize = build.Select(build.Var{
    23  		Dev:      uint64(1 << 18), // 256 KiB
    24  		Standard: uint64(1 << 20), // 1 MiB
    25  		Testing:  uint64(1 << 9),  // 512 B
    26  	}).(uint64)
    27  
    28  	// chunkFetchersMaximumPreload defines the amount of chunk fetchers we
    29  	// preload when the data source gets created. The remaining chunk fetchers
    30  	// are lazy loaded when data sections get created, which in turn are also
    31  	// created before they are being read from, giving those PCWS some buffer to
    32  	// execute their has sector jobs before being used to download the chunk.
    33  	chunkFetchersMaximumPreload = build.Select(build.Var{
    34  		Dev:      3,
    35  		Standard: 16, // ~32MiB worth of PCWS
    36  		Testing:  3,
    37  	}).(int)
    38  )
    39  
    40  type (
    41  	// skylinkDataSource implements streamBufferDataSource on a Skylink.
    42  	// Notably, it creates a pcws for every single chunk in the Skylink and
    43  	// keeps them in memory, to reduce latency on seeking through the file.
    44  	skylinkDataSource struct {
    45  		// Metadata.
    46  		staticID          skymodules.DataSourceID
    47  		staticLayout      skymodules.SkyfileLayout
    48  		staticMetadata    skymodules.SkyfileMetadata
    49  		staticRawMetadata []byte
    50  		staticSkylink     skymodules.Skylink
    51  
    52  		// staticSkylinkSector will contain the raw data of the sector
    53  		// referenced by the skylink. That way we can provide proofs for
    54  		// arbitrary ranges of the sector later.
    55  		// NOTE: The skylink sector is never decrypted to make sure the
    56  		// merkle proofs can still be verified against the sector root.
    57  		staticSkylinkSector []byte
    58  
    59  		// staticLayoutOffset is the offset of the layout within the
    60  		// sector.
    61  		staticLayoutOffset int
    62  
    63  		staticDecryptedSkylinkSector []byte
    64  
    65  		// staticChunkFetcherLoaders holds a loader function for every chunk in
    66  		// the fanout. This loaders creates a PCWS for that chunk, and thus
    67  		// spins up a bunch of has sector jobs. On initial read, we want to
    68  		// limit the amount of PCWS created, to avoid unnecessary network load.
    69  		// That is why we only preload a certain amount, and lazy load the
    70  		// others when a new data section gets created.
    71  		//
    72  		// staticChunkFetchers holds the PCWS after it became available
    73  		//
    74  		// staticChunkFetchersAvailable is a channel that gets closed by the
    75  		// loader after the PCWS got created and its state was updated for the
    76  		// first time
    77  		//
    78  		// staticChunkErrs contains a potential error, this error should be
    79  		// checked when the chunk fetcher becomes available
    80  		staticChunkFetcherLoaders    []chunkFetcherLoaderFn
    81  		staticChunkFetchersAvailable []chan struct{}
    82  		staticChunkFetchers          []chunkFetcher
    83  		staticChunkErrs              []error
    84  
    85  		// Utilities
    86  		staticCtx        context.Context
    87  		staticCancelFunc context.CancelFunc
    88  	}
    89  
    90  	// chunkFetcherLoaderFn is a helper type that represents a function that
    91  	// loads a chunk fetcher. Every chunk in the fanout will have a
    92  	// corresponding loader function that creates the PCWS for that chunk.
    93  	chunkFetcherLoaderFn func()
    94  )
    95  
    96  // DataSize implements streamBufferDataSource
    97  func (sds *skylinkDataSource) DataSize() uint64 {
    98  	return sds.staticLayout.Filesize
    99  }
   100  
   101  // ID implements streamBufferDataSource
   102  func (sds *skylinkDataSource) ID() skymodules.DataSourceID {
   103  	return sds.staticID
   104  }
   105  
   106  // ReadBaseSectorPayload reads data from the data source's base sector payload.
   107  // This will return an error when called on anything but a small skyfile.
   108  func (sds *skylinkDataSource) ReadBaseSectorPayload(off, length uint64) (*downloadResponse, error) {
   109  	return sds.managedReadSmallFileSection(off, length)
   110  }
   111  
   112  // HasRecursiveFanout returns whether or not the datasource belongs to a skylink
   113  // with a recursive fanout.
   114  func (sds *skylinkDataSource) HasRecursiveFanout() bool {
   115  	return sds.staticLayout.HasRecursiveFanout(uint64(sds.staticLayoutOffset))
   116  }
   117  
   118  // Layout implements streamBufferDataSource
   119  func (sds *skylinkDataSource) Layout() skymodules.SkyfileLayout {
   120  	return sds.staticLayout
   121  }
   122  
   123  // RawLayout implements streamBufferDataSource
   124  func (sds *skylinkDataSource) RawLayout() (skymodules.SkyfileLayout, []byte, []crypto.Hash) {
   125  	return sds.managedReadLayout()
   126  }
   127  
   128  // Metadata implements streamBufferDataSource
   129  func (sds *skylinkDataSource) Metadata() skymodules.SkyfileMetadata {
   130  	return sds.staticMetadata
   131  }
   132  
   133  // RawMetadata implements streamBufferDataSource
   134  func (sds *skylinkDataSource) RawMetadata() []byte {
   135  	return sds.staticRawMetadata
   136  }
   137  
   138  // RequestSize implements streamBufferDataSource
   139  func (sds *skylinkDataSource) RequestSize() uint64 {
   140  	return SkylinkDataSourceRequestSize
   141  }
   142  
   143  // Skylink implements streamBufferDataSource
   144  func (sds *skylinkDataSource) Skylink() skymodules.Skylink {
   145  	return sds.staticSkylink
   146  }
   147  
   148  // SilentClose implements streamBufferDataSource
   149  func (sds *skylinkDataSource) SilentClose() {
   150  	// Canceling the context for the data source should be sufficient. As
   151  	// all child processes (such as the pcws for each chunk) should be using
   152  	// contexts derived from the sds context.
   153  	sds.staticCancelFunc()
   154  }
   155  
   156  // managedReadSection downloads a section for the given section index from the
   157  // data source. It returns a response that can be awaited,
   158  func (sds *skylinkDataSource) managedReadSection(ctx context.Context, sectionIndex uint64, pricePerMS types.Currency) (<-chan *downloadResponse, uint64, error) {
   159  	// Translate input to offset and fetchSize within DataSource.
   160  	off := sectionIndex * sds.RequestSize()
   161  	if off >= sds.staticLayout.Filesize {
   162  		return nil, 0, fmt.Errorf("ReadSection: offset out-of-bounds %v >= %v", off, sds.staticLayout.Filesize)
   163  	}
   164  
   165  	// We tolerate out-of-bounds if at least the offset was within bounds.
   166  	// That just means we are at the end of the file and there is not a full
   167  	// section left.
   168  	fetchSize := sds.RequestSize()
   169  	if off+fetchSize > sds.staticLayout.Filesize {
   170  		fetchSize = sds.staticLayout.Filesize - off
   171  	}
   172  
   173  	// If we are dealing with a small skyfile without fanout bytes, we can
   174  	// simply read from that and return early.
   175  	if sds.staticLayout.FanoutSize == 0 {
   176  		dr, err := sds.managedReadSmallFileSection(off, fetchSize)
   177  		respChan := make(chan *downloadResponse, 1)
   178  		respChan <- dr
   179  		close(respChan)
   180  		return respChan, 0, err
   181  	}
   182  
   183  	// Determine how large each uploaded chunk is.
   184  	chunkSize := skymodules.ChunkSize(sds.staticLayout.CipherType, uint64(sds.staticLayout.FanoutDataPieces))
   185  
   186  	// The fetchSize should never exceed the size of a chunk. That way we
   187  	// only need to deal with a single set of proofs per data section and
   188  	// not multiple.
   189  	if fetchSize > chunkSize {
   190  		err := fmt.Errorf("ReadSection: fetchSize > SectorSize %v > %v", fetchSize, modules.SectorSize)
   191  		build.Critical(err)
   192  		return nil, 0, err
   193  	}
   194  
   195  	// Determine which chunk the offset is currently in.
   196  	chunkIndex := off / chunkSize
   197  	offsetInChunk := off % chunkSize
   198  
   199  	// Trigger the loader, this function will only be executed once and
   200  	// ensures the chunk fetcher is being initialized. Consecutive calls are
   201  	// essentially a no-op
   202  	sds.staticChunkFetcherLoaders[chunkIndex]()
   203  
   204  	// Wait until the chunk fetcher is ready, and check if there was any
   205  	// error in initializing the chunk fetcher.
   206  	select {
   207  	case <-sds.staticChunkFetchersAvailable[chunkIndex]:
   208  	case <-sds.staticCtx.Done():
   209  		return nil, 0, errors.New("stream fetch aborted because of cancel")
   210  	}
   211  	if sds.staticChunkErrs[chunkIndex] != nil {
   212  		return nil, 0, errors.AddContext(sds.staticChunkErrs[chunkIndex], "unable to start download")
   213  	}
   214  
   215  	// Schedule the download.
   216  	respChan, err := sds.staticChunkFetchers[chunkIndex].Download(ctx, pricePerMS, offsetInChunk, fetchSize, false)
   217  	if err != nil {
   218  		return nil, 0, errors.AddContext(err, "unable to start download")
   219  	}
   220  	return respChan, chunkIndex, nil
   221  }
   222  
   223  // ReadFanout reads a single piece root from the fanout of the datasource and
   224  // returns the proof for that root as well as the offset within the sector.
   225  func (sds *skylinkDataSource) ReadFanout(chunkIndex, pieceIndex uint64) ([]byte, []crypto.Hash, uint32, error) {
   226  	layout := sds.staticLayout
   227  
   228  	// Get the offset of where the fanout starts within the base sector.
   229  	fanoutOff := layout.FanoutOffset(uint64(sds.staticLayoutOffset))
   230  
   231  	// Get the offset of the right piece root within the fanout. If it is
   232  	// compressed, each chunk only contains 1 piece. So we can ignore the
   233  	// pieceIndex.
   234  	chunkOff := fanoutOff + layout.FanoutPiecesPerChunk()*chunkIndex*crypto.HashSize
   235  	rootOff := chunkOff + pieceIndex*crypto.HashSize
   236  
   237  	dr, err := sds.managedReadBaseSector(rootOff, crypto.HashSize)
   238  	if err != nil {
   239  		return nil, nil, 0, err
   240  	}
   241  	return dr.externDownloadedData.LogicalChunkData[0], dr.externDownloadedData.Proofs[0], uint32(rootOff), nil
   242  }
   243  
   244  // ReadSection implements streamBufferDataSource
   245  func (sds *skylinkDataSource) ReadSection(ctx context.Context, sectionIndex uint64, pricePerMS types.Currency) (<-chan *downloadResponse, error) {
   246  	respChan, _, err := sds.managedReadSection(ctx, sectionIndex, pricePerMS)
   247  	return respChan, err
   248  }
   249  
   250  // managedDownloadByRoot will fetch data using the merkle root of that data.
   251  func (r *Renter) managedDownloadByRoot(ctx context.Context, root crypto.Hash, offset, length uint64, pricePerMS types.Currency) ([]byte, *downloadedData, *pcwsWorkerState, error) {
   252  	// Create a context that dies when the function ends, this will cancel all
   253  	// of the worker jobs that get created by this function.
   254  	ctx, cancel := context.WithCancel(ctx)
   255  	defer cancel()
   256  
   257  	// Capture the base sector download in a new span.
   258  	span, ctx := opentracing.StartSpanFromContext(ctx, "managedDownloadByRoot")
   259  	span.SetTag("root", root)
   260  	defer span.Finish()
   261  
   262  	// Create the pcws for the first chunk. We use a passthrough cipher and
   263  	// erasure coder. If the base sector is encrypted, we will notice and be
   264  	// able to decrypt it once we have fully downloaded it and are able to
   265  	// access the layout. We can make the assumption on the erasure coding being
   266  	// of 1-N seeing as we currently always upload the basechunk using 1-N
   267  	// redundancy.
   268  	ptec := skymodules.NewPassthroughErasureCoder()
   269  	tpsk, err := crypto.NewSiaKey(crypto.TypePlain, nil)
   270  	if err != nil {
   271  		return nil, nil, nil, errors.AddContext(err, "unable to create plain skykey")
   272  	}
   273  	pcws, err := r.newPCWSByRoots(ctx, []crypto.Hash{root}, ptec, tpsk, 0)
   274  	if err != nil {
   275  		return nil, nil, nil, errors.AddContext(err, "unable to create the worker set for this skylink")
   276  	}
   277  
   278  	// Download the base sector. The base sector contains the metadata, without
   279  	// it we can't provide a completed data source.
   280  	//
   281  	// NOTE: we pass in the provided context here, if the user imposed a timeout
   282  	// on the download request, this will fire if it takes too long.
   283  	respChan, err := pcws.managedDownload(ctx, pricePerMS, offset, length, false)
   284  	if err != nil {
   285  		return nil, nil, nil, errors.AddContext(err, "unable to start download")
   286  	}
   287  	resp := <-respChan
   288  	dd, err := resp.Data()
   289  	if err != nil {
   290  		return nil, nil, nil, errors.AddContext(resp.err, "base sector download did not succeed")
   291  	}
   292  	data, err := dd.Recover()
   293  	return data, dd, pcws.managedWorkerState(), err
   294  }
   295  
   296  // managedReadLayout returns the data sources layout, the segment-aligned raw
   297  // layout and a proof for the raw layout.
   298  func (sds *skylinkDataSource) managedReadLayout() (skymodules.SkyfileLayout, []byte, []crypto.Hash) {
   299  	// Get offset and length of the layout.
   300  	layoutOff := sds.staticLayoutOffset
   301  	layoutLen := skymodules.SkyfileLayoutSize
   302  
   303  	// The downloaded data needs to be segment aligned for the proof.
   304  	layoutOffAligned := layoutOff
   305  	if mod := layoutOffAligned % crypto.SegmentSize; mod != 0 {
   306  		layoutOffAligned -= mod
   307  	}
   308  	endLayoutOffAligned := layoutOff + layoutLen
   309  	if mod := endLayoutOffAligned % crypto.SegmentSize; mod != 0 {
   310  		endLayoutOffAligned += (crypto.SegmentSize - mod)
   311  	}
   312  
   313  	// Get the requested, segment-aligned data.
   314  	data := sds.staticSkylinkSector[layoutOffAligned:endLayoutOffAligned]
   315  
   316  	// Create a range proof for the payload.
   317  	proofStart := int(layoutOffAligned / crypto.SegmentSize)
   318  	proofEnd := int(endLayoutOffAligned / crypto.SegmentSize)
   319  	proof := crypto.MerkleRangeProof(sds.staticSkylinkSector, proofStart, proofEnd)
   320  
   321  	// Sanity check the proof.
   322  	if build.Release == "testing" {
   323  		if !crypto.VerifyRangeProof(data, proof, proofStart, proofEnd, sds.Skylink().MerkleRoot()) {
   324  			build.Critical("managedReadLayout: created merkle proof is invalid")
   325  		}
   326  	}
   327  	return sds.staticLayout, data, proof
   328  }
   329  
   330  // managedReadBaseSector handles reading a part of a base sector. It returns a
   331  // proof for the section that was read.
   332  func (sds *skylinkDataSource) managedReadBaseSector(off, fetchSize uint64) (*downloadResponse, error) {
   333  	if off > modules.SectorSize {
   334  		return nil, fmt.Errorf("managedReadBaseSectorRange: off can't be greater than sector size: %v > %v", off, modules.SectorSize)
   335  	}
   336  	bytesLeft := modules.SectorSize - off
   337  	if fetchSize > bytesLeft {
   338  		return nil, fmt.Errorf("managedReadBaseSectorRange: read is out-of-bounds for off %v fetchSize %v and sector size %v", off, fetchSize, modules.SectorSize)
   339  	}
   340  
   341  	// Convenience var.
   342  	offsetInChunk := off
   343  
   344  	// The data we grab needs to be segment aligned so we adjust the offset
   345  	// and length that we use.
   346  	offsetInChunkAligned := offsetInChunk
   347  	if mod := offsetInChunkAligned % crypto.SegmentSize; mod != 0 {
   348  		offsetInChunkAligned -= mod
   349  	}
   350  	endOffsetInChunkAligned := offsetInChunk + fetchSize
   351  	if mod := endOffsetInChunkAligned % crypto.SegmentSize; mod != 0 {
   352  		endOffsetInChunkAligned += (crypto.SegmentSize - mod)
   353  	}
   354  
   355  	// Prepare slice for returning the segment-aligned data. We just copy
   356  	// the raw, decrypted sector data of the range we want to return.
   357  	data := append([]byte{}, sds.staticDecryptedSkylinkSector[offsetInChunkAligned:endOffsetInChunkAligned]...)
   358  
   359  	// Create a range proof for the payload. The range proof is created over
   360  	// the encrypted data because otherwise the proof doesn't add up to the
   361  	// root in the skylink.
   362  	proofStart := int(offsetInChunkAligned / crypto.SegmentSize)
   363  	proofEnd := int(endOffsetInChunkAligned / crypto.SegmentSize)
   364  	proof := crypto.MerkleRangeProof(sds.staticSkylinkSector, proofStart, proofEnd)
   365  
   366  	// Sanity check the proof. We do this only for unencrypted sectors. For
   367  	// encrypted ones, the client needs to encrypt the data again for it to
   368  	// match the proof.
   369  	if build.Release == "testing" && !skymodules.IsEncryptedLayout(sds.staticLayout) {
   370  		if !crypto.VerifyRangeProof(data, proof, proofStart, proofEnd, sds.Skylink().MerkleRoot()) {
   371  			build.Critical("managedReadSmallFileSection: created merkle proof is invalid")
   372  		}
   373  	}
   374  
   375  	// Create the response.
   376  	dr := newDownloadResponse(offsetInChunk, fetchSize, skymodules.NewPassthroughErasureCoder(), [][]byte{data}, [][]crypto.Hash{proof}, nil)
   377  	return dr, nil
   378  }
   379  
   380  // managedReadSmallFileSection handles reading a section from the buffered
   381  // sector in-memory in case the file is a small one.
   382  func (sds *skylinkDataSource) managedReadSmallFileSection(off, fetchSize uint64) (*downloadResponse, error) {
   383  	if !sds.staticLayout.IsSmallFile() {
   384  		return nil, errors.New("file is not a small file")
   385  	}
   386  	if off > sds.staticLayout.Filesize {
   387  		return nil, fmt.Errorf("managedReadSmallFileSection: off can't be greater than payload length: %v > %v", off, sds.staticLayout.Filesize)
   388  	}
   389  	bytesLeft := sds.staticLayout.Filesize - off
   390  	if fetchSize > bytesLeft {
   391  		fetchSize = bytesLeft
   392  	}
   393  
   394  	// Get the offset of the payload within the sector.
   395  	payloadOff := uint64(sds.staticLayoutOffset + skymodules.SkyfileLayoutSize + len(sds.staticRawMetadata))
   396  	return sds.managedReadBaseSector(payloadOff+off, fetchSize)
   397  }
   398  
   399  // managedSkylinkDataSource will create a streamBufferDataSource for the data
   400  // contained inside of a Skylink. The function will not return until the base
   401  // sector and all skyfile metadata has been retrieved.
   402  //
   403  // NOTE: Skylink data sources are cached and outlive the user's request because
   404  // multiple different callers may want to use the same data source. We do have
   405  // to pass in a context though to adhere to a possible user-imposed request
   406  // timeout. This can be optimized to always create the data source when it was
   407  // requested, but we should only do so after gathering some real world feedback
   408  // that indicates we would benefit from this.
   409  func (r *Renter) managedSkylinkDataSource(ctx context.Context, skylink skymodules.Skylink, pricePerMS types.Currency) (streamBufferDataSource, error) {
   410  	skylinkSector, dd, _, err := r.managedDownloadByRoot(ctx, skylink.MerkleRoot(), 0, modules.SectorSize, pricePerMS)
   411  	if err != nil {
   412  		return nil, errors.AddContext(err, "failed to download baseSector")
   413  	}
   414  
   415  	// Store the skylink sector in the cache. We call this even if the sector
   416  	// was cached. That way we count the cache hit and refresh the lru.
   417  	err = r.staticStreamBufferSet.staticCache.Put(skylink.DataSourceID(), baseSectorSectionIndex, dd)
   418  	if err != nil {
   419  		return nil, errors.AddContext(err, "failed to update the cache")
   420  	}
   421  
   422  	// Get the offset and fetchsize of the base sector within the full
   423  	// sector.
   424  	slOffset, slFetchSize, err := skylink.OffsetAndFetchSize()
   425  	if err != nil {
   426  		return nil, errors.AddContext(err, "failed to get offset and fetchsize from skylink")
   427  	}
   428  	baseSector := skylinkSector[slOffset : slOffset+slFetchSize]
   429  	decryptedSkylinkSector := skylinkSector
   430  
   431  	// Check if the base sector is encrypted, and attempt to decrypt it.
   432  	// This will fail if we don't have the decryption key.
   433  	var fileSpecificSkykey skykey.Skykey
   434  	if skymodules.IsEncryptedBaseSector(baseSector) {
   435  		// Deep-copy the sector to avoid decrypting the
   436  		// original skylinkSector.
   437  		decryptedSkylinkSector = append([]byte{}, decryptedSkylinkSector...)
   438  		baseSector = decryptedSkylinkSector[slOffset : slOffset+slFetchSize]
   439  		fileSpecificSkykey, err = r.managedDecryptBaseSector(baseSector)
   440  		if err != nil {
   441  			return nil, errors.AddContext(err, "unable to decrypt skyfile base sector")
   442  		}
   443  	}
   444  
   445  	// Parse out the metadata of the skyfile.
   446  	// TODO: (f/u?) it might be better to resolve the parts of the fanout we
   447  	// need on demand. But that's quite the undertaking by itself.
   448  	// e.g. if we don't start resolving the recursive fanout right away we
   449  	// lose the benefit of the workerset, because we will add some latency
   450  	// later once we actually know what the user wants to download.
   451  	layout, fanoutBytes, metadata, rawMetadata, _, _, err := r.ParseSkyfileMetadata(baseSector)
   452  	if err != nil {
   453  		return nil, errors.AddContext(err, "unable to parse skyfile metadata")
   454  	}
   455  
   456  	// Create the context for the data source - a child of the renter
   457  	// threadgroup but otherwise independent.
   458  	dsCtx, cancelFunc := context.WithCancel(r.tg.StopCtx())
   459  
   460  	// Tag the span with its size. We tag it with 64kb, 1mb, 4mb and 10mb as
   461  	// those are the size increments used by the benchmark tool. This way we can
   462  	// run the benchmark and then filter the results using these tags.
   463  	//
   464  	// NOTE: the sizes used are "exact sizes", meaning they are as close as
   465  	// possible to their eventual size after taking into account the size of the
   466  	// metadata. See cmd/skynet-benchmark/dl.go for more info.
   467  	if span := opentracing.SpanFromContext(ctx); span != nil {
   468  		switch length := metadata.Length; {
   469  		case length <= 61e3:
   470  			span.SetTag("length", "64kb")
   471  		case length <= 982e3:
   472  			span.SetTag("length", "1mb")
   473  		case length <= 3931e3:
   474  			span.SetTag("length", "4mb")
   475  		default:
   476  			span.SetTag("length", "10mb")
   477  		}
   478  
   479  		// Attach the span to the ctx
   480  		dsCtx = opentracing.ContextWithSpan(dsCtx, span)
   481  	}
   482  
   483  	// If there's a fanout create a PCWS for every chunk.
   484  	var fanoutChunkFetcherLoaders []chunkFetcherLoaderFn
   485  	var fanoutChunkFetchersAvailable []chan struct{}
   486  	var fanoutChunkFetchers []chunkFetcher
   487  	var fanoutChunkErrs []error
   488  	var ec skymodules.ErasureCoder
   489  	if len(fanoutBytes) > 0 {
   490  		// Derive the fanout key
   491  		fanoutKey, err := skymodules.DeriveFanoutKey(&layout, fileSpecificSkykey)
   492  		if err != nil {
   493  			cancelFunc()
   494  			return nil, errors.AddContext(err, "unable to derive encryption key")
   495  		}
   496  
   497  		// Create the erasure coder
   498  		ec, err = skymodules.NewRSSubCode(int(layout.FanoutDataPieces), int(layout.FanoutParityPieces), crypto.SegmentSize)
   499  		if err != nil {
   500  			cancelFunc()
   501  			return nil, errors.AddContext(err, "unable to derive erasure coding settings for fanout")
   502  		}
   503  
   504  		// Create the list of chunks from the fanout.
   505  		fanoutChunks, err := layout.DecodeFanoutIntoChunks(fanoutBytes)
   506  		if err != nil {
   507  			cancelFunc()
   508  			return nil, errors.AddContext(err, "error parsing skyfile fanout")
   509  		}
   510  
   511  		// Initialize the fanout chunk fetcher loaders. Note that we only
   512  		// initialize the loaders here and we do not block on initializing the
   513  		// actual PCWS objects for every chunk. We will preload a certain amount
   514  		// to improve TTFB, we do not however want to precreate a PCWS for every
   515  		// chunk on initial read because that might put too much strain on the
   516  		// network because of the amount of has sector jobs. The loaders will
   517  		// send a PCWS down the channel when it is ready, the caller (Stream)
   518  		// can block on that channel.
   519  		numChunks := len(fanoutChunks)
   520  		fanoutChunkFetcherLoaders = make([]chunkFetcherLoaderFn, numChunks)
   521  		fanoutChunkFetchersAvailable = make([]chan struct{}, numChunks)
   522  		fanoutChunkFetchers = make([]chunkFetcher, numChunks)
   523  		fanoutChunkErrs = make([]error, numChunks)
   524  		for i := 0; i < numChunks; i++ {
   525  			fanoutChunkFetchersAvailable[i] = make(chan struct{})
   526  		}
   527  
   528  		// Create the PCWS loaders
   529  		for i, chunk := range fanoutChunks {
   530  			chunkIndex := uint64(i)
   531  			chunkCopy := chunk
   532  			var once sync.Once
   533  			fanoutChunkFetcherLoaders[i] = func() {
   534  				once.Do(func() {
   535  					pcws, err := r.newPCWSByRoots(dsCtx, chunkCopy, ec, fanoutKey, chunkIndex)
   536  					fanoutChunkErrs[chunkIndex] = err
   537  					fanoutChunkFetchers[chunkIndex] = pcws
   538  					close(fanoutChunkFetchersAvailable[chunkIndex])
   539  				})
   540  			}
   541  		}
   542  
   543  		// Launch a portion of the loaders in a goroutine, this makes it so we
   544  		// can return the data source faster and we also preload the first
   545  		// couple of PCWSs, the others are lazy loaded when new data sections
   546  		// get created. Since we use a `minimumLookahead` there, we will load
   547  		// the PCWSs before they are used, giving them some time to execute
   548  		// their has sector jobs prior to being downloaded from.
   549  		err = r.tg.Launch(func() {
   550  			for i := 0; i < len(fanoutChunks) && i < chunkFetchersMaximumPreload; i++ {
   551  				fanoutChunkFetcherLoaders[i]()
   552  			}
   553  		})
   554  		if err != nil {
   555  			cancelFunc()
   556  			return nil, errors.AddContext(err, "unable to launch thread to preload one of the initial chunk fetchers")
   557  		}
   558  	}
   559  
   560  	sds := &skylinkDataSource{
   561  		staticID:           skylink.DataSourceID(),
   562  		staticLayout:       layout,
   563  		staticLayoutOffset: int(slOffset),
   564  		staticMetadata:     metadata,
   565  		staticRawMetadata:  rawMetadata,
   566  		staticSkylink:      skylink,
   567  
   568  		staticSkylinkSector:          skylinkSector,
   569  		staticDecryptedSkylinkSector: decryptedSkylinkSector,
   570  
   571  		staticChunkFetcherLoaders:    fanoutChunkFetcherLoaders,
   572  		staticChunkFetchersAvailable: fanoutChunkFetchersAvailable,
   573  		staticChunkFetchers:          fanoutChunkFetchers,
   574  		staticChunkErrs:              fanoutChunkErrs,
   575  
   576  		staticCtx:        dsCtx,
   577  		staticCancelFunc: cancelFunc,
   578  	}
   579  	return sds, nil
   580  }