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

     1  package renter
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"reflect"
     8  	"strings"
     9  	"testing"
    10  	"time"
    11  
    12  	"gitlab.com/NebulousLabs/errors"
    13  	"gitlab.com/NebulousLabs/fastrand"
    14  	"gitlab.com/SkynetLabs/skyd/skymodules"
    15  	"go.sia.tech/siad/crypto"
    16  	"go.sia.tech/siad/modules"
    17  	"go.sia.tech/siad/types"
    18  )
    19  
    20  // mockProjectChunkWorkerSet is a mock object implementing the chunkFetcher
    21  // interface
    22  type mockProjectChunkWorkerSet struct {
    23  	staticDownloadResponseChan chan *downloadResponse
    24  	staticDownloadData         []byte
    25  	staticEC                   skymodules.ErasureCoder
    26  	staticErr                  error
    27  }
    28  
    29  // newTestDataSource is a helper that creates a mostly valid skylinkDataSource
    30  // for testing. It doesn't have a valid renter or chunk fetchers.
    31  func newTestDataSource(fileName string, data []byte) *skylinkDataSource {
    32  	// create renter
    33  	renter := new(Renter)
    34  	renter.staticBaseSectorDownloadStats = skymodules.NewSectorDownloadStats()
    35  	renter.staticFanoutSectorDownloadStats = skymodules.NewSectorDownloadStats()
    36  
    37  	// Create some metadata.
    38  	fileSize := uint64(len(data))
    39  	md := TusSkyfileMetadata(fileName, "", fileSize, 0777)
    40  	mdRaw, err := json.Marshal(md)
    41  	if err != nil {
    42  		panic(err)
    43  	}
    44  
    45  	// Create the layout and sector.
    46  	sl := skymodules.NewSkyfileLayout(fileSize, uint64(len(mdRaw)), 0, skymodules.NewPassthroughErasureCoder(), crypto.TypePlain)
    47  	sector, fetchLen, _ := skymodules.BuildBaseSector(sl.Encode(), nil, mdRaw, data)
    48  
    49  	// Create the skylink.
    50  	sectorRoot := crypto.MerkleRoot(sector)
    51  	skylink, err := skymodules.NewSkylinkV1(sectorRoot, 0, fetchLen)
    52  	if err != nil {
    53  		panic(err)
    54  	}
    55  
    56  	ctx, cancel := context.WithCancel(renter.tg.StopCtx())
    57  	return &skylinkDataSource{
    58  		staticID:                     skylink.DataSourceID(),
    59  		staticLayout:                 sl,
    60  		staticMetadata:               md,
    61  		staticRawMetadata:            mdRaw,
    62  		staticSkylink:                skylink,
    63  		staticSkylinkSector:          sector,
    64  		staticLayoutOffset:           0,
    65  		staticDecryptedSkylinkSector: sector,
    66  		staticChunkFetchers:          make([]chunkFetcher, 0),
    67  
    68  		staticCancelFunc: cancel,
    69  		staticCtx:        ctx,
    70  	}
    71  }
    72  
    73  // newTestDownloadResponse creates a new downloadResponse for testing.
    74  func newTestDownloadResponse(ec skymodules.ErasureCoder, fullData []byte, offset, length uint64) (*downloadResponse, error) {
    75  	// Erasure Code the full data.
    76  	fullData = append([]byte{}, fullData...)
    77  	data, err := ec.Encode(fullData)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  	// Cut off all segments before the ones we are interested in. The actual
    82  	// download code will also only download these.
    83  	toCut := uint64(offset) / (crypto.SegmentSize * uint64(ec.MinPieces()))
    84  	for i := 0; i < len(data); i++ {
    85  		data[i] = data[i][toCut*crypto.SegmentSize:]
    86  	}
    87  	// Create response and send it.
    88  	return newDownloadResponse(offset, length, ec, data, nil, nil), nil
    89  }
    90  
    91  // Download implements the chunkFetcher interface.
    92  func (m *mockProjectChunkWorkerSet) Download(ctx context.Context, pricePerMS types.Currency, offset, length uint64, _ bool) (chan *downloadResponse, error) {
    93  	dr, err := newTestDownloadResponse(m.staticEC, m.staticDownloadData, offset, length)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  	m.staticDownloadResponseChan <- dr
    98  	return m.staticDownloadResponseChan, m.staticErr
    99  }
   100  
   101  // newChunkFetcher returns a chunk fetcher.
   102  func newChunkFetcher(data []byte, err error, ec skymodules.ErasureCoder) chunkFetcher {
   103  	// For convenience the test only passes the data they are interested in
   104  	// but we need to make sure it's sector size aligned.
   105  	if uint64(len(data)) < modules.SectorSize {
   106  		data = append(data, make([]byte, modules.SectorSize-uint64(len(data)))...)
   107  	}
   108  	responseChan := make(chan *downloadResponse, 1)
   109  	return &mockProjectChunkWorkerSet{
   110  		staticDownloadResponseChan: responseChan,
   111  		staticDownloadData:         data,
   112  		staticEC:                   ec,
   113  		staticErr:                  err,
   114  	}
   115  }
   116  
   117  // TestSkylinkDataSource is a unit test that verifies the behaviour of a
   118  // SkylinkDataSource. Note that we are using mocked data, testing of the
   119  // datasource with live PCWSs attached will happen through integration tests.
   120  func TestSkylinkDataSource(t *testing.T) {
   121  	t.Parallel()
   122  	t.Run("small", testSkylinkDataSourceSmallFile)
   123  	t.Run("large", testSkylinkDataSourceLargeFile)
   124  	t.Run("managedSkylinkDataSource", testManagedSkylinkDataSource)
   125  	t.Run("managedReadLayout", testSkylinkDataSourceReadLayout)
   126  }
   127  
   128  // testSkylinkDataSourceSmallFile verifies we can read from a datasource for a
   129  // small skyfile.
   130  func testSkylinkDataSourceSmallFile(t *testing.T) {
   131  	data := fastrand.Bytes(int(modules.SectorSize) / 2)
   132  	datasize := uint64(len(data))
   133  
   134  	sds := newTestDataSource("thisisafilename", data)
   135  	skylink := sds.staticSkylink
   136  
   137  	if sds.DataSize() != datasize {
   138  		t.Fatal("unexpected", sds.DataSize(), datasize)
   139  	}
   140  	if sds.ID() != skylink.DataSourceID() {
   141  		t.Fatal("unexpected")
   142  	}
   143  	if !reflect.DeepEqual(sds.Metadata(), TusSkyfileMetadata("thisisafilename", "", datasize, 0777)) {
   144  		t.Fatal("unexpected")
   145  	}
   146  	if sds.RequestSize() != SkylinkDataSourceRequestSize {
   147  		t.Fatal("unexpected")
   148  	}
   149  
   150  	// verify invalid index.
   151  	_, err := sds.ReadSection(context.Background(), (uint64(len(data))/uint64(sds.RequestSize()) + 1), types.ZeroCurrency)
   152  	if err == nil || !strings.Contains(err.Error(), "ReadSection: offset out-of-bounds 2560 >= 2048") {
   153  		t.Fatal(err)
   154  	}
   155  
   156  	index := uint64(1)
   157  	responseChan, err := sds.ReadSection(context.Background(), index, types.ZeroCurrency)
   158  	if err != nil {
   159  		t.Fatal(err)
   160  	}
   161  	select {
   162  	case resp := <-responseChan:
   163  		dd, err := resp.Data()
   164  		if resp == nil || err != nil {
   165  			t.Fatal("unexpected")
   166  		}
   167  		respData, err := dd.Recover()
   168  		if err != nil {
   169  			t.Fatal(err)
   170  		}
   171  		if !bytes.Equal(respData, data[sds.RequestSize():2*sds.RequestSize()]) {
   172  			t.Log("expected: ", data[sds.RequestSize():2*sds.RequestSize()], sds.RequestSize())
   173  			t.Log("actual:   ", respData, len(respData))
   174  			t.Fatal("unexepected data")
   175  		}
   176  	case <-time.After(time.Second):
   177  		t.Fatal("unexpected")
   178  	}
   179  
   180  	index = uint64(len(data)/int(sds.RequestSize())) - 1
   181  	responseChan, err = sds.ReadSection(context.Background(), index, types.ZeroCurrency)
   182  	if err != nil {
   183  		t.Fatal(err)
   184  	}
   185  	select {
   186  	case resp := <-responseChan:
   187  		dd, err := resp.Data()
   188  		if resp == nil || err != nil {
   189  			t.Fatal("unexpected")
   190  		}
   191  		respData, err := dd.Recover()
   192  		if err != nil {
   193  			t.Fatal(err)
   194  		}
   195  		if !bytes.Equal(respData, data[index*sds.RequestSize():]) {
   196  			t.Log("expected: ", data[index*sds.RequestSize():], len(data[sds.RequestSize():]))
   197  			t.Log("actual:   ", respData, len(respData))
   198  			t.Fatal("unexepected data")
   199  		}
   200  	case <-time.After(time.Second):
   201  		t.Fatal("unexpected")
   202  	}
   203  
   204  	select {
   205  	case <-sds.staticCtx.Done():
   206  		t.Fatal("unexpected")
   207  	case <-time.After(10 * time.Millisecond):
   208  		sds.SilentClose()
   209  	}
   210  	select {
   211  	case <-sds.staticCtx.Done():
   212  	case <-time.After(10 * time.Millisecond):
   213  		t.Fatal("unexpected")
   214  	}
   215  }
   216  
   217  // testSkylinkDataSourceLargeFile verifies we can read from a datasource for a
   218  // large skyfile.
   219  func testSkylinkDataSourceLargeFile(t *testing.T) {
   220  	fanoutChunk1 := fastrand.Bytes(int(modules.SectorSize))
   221  	fanoutChunk2 := fastrand.Bytes(int(modules.SectorSize) / 2)
   222  	fanoutChunks := [][]byte{fanoutChunk1, fanoutChunk2}
   223  	allData := append(fanoutChunk1, fanoutChunk2...)
   224  	datasize := uint64(len(allData))
   225  	fanoutDataPieces := uint64(1)
   226  	fanoutParityPieces := uint64(9)
   227  	ec, err := skymodules.NewRSSubCode(int(fanoutDataPieces), int(fanoutParityPieces), crypto.SegmentSize)
   228  	if err != nil {
   229  		t.Fatal(err)
   230  	}
   231  
   232  	// create renter
   233  	renter := new(Renter)
   234  	renter.staticBaseSectorDownloadStats = skymodules.NewSectorDownloadStats()
   235  	renter.staticFanoutSectorDownloadStats = skymodules.NewSectorDownloadStats()
   236  
   237  	ctx, cancel := context.WithCancel(renter.tg.StopCtx())
   238  
   239  	// create chunk fetcher and no-op loaders
   240  	chunkFetchers := make([]chunkFetcher, 2)
   241  	chunkFetchersAvailable := make([]chan struct{}, 2)
   242  	chunkFetcherLoaders := make([]chunkFetcherLoaderFn, 2)
   243  	for i := 0; i < 2; i++ {
   244  		chunkFetchers[i] = newChunkFetcher(fanoutChunks[i], nil, ec)
   245  		chunkFetchersAvailable[i] = make(chan struct{})
   246  		close(chunkFetchersAvailable[i])
   247  		chunkFetcherLoaders[i] = func() {}
   248  	}
   249  
   250  	sds := &skylinkDataSource{
   251  		staticID: skymodules.DataSourceID(crypto.Hash{1, 2, 3}),
   252  		staticLayout: skymodules.SkyfileLayout{
   253  			Version:            skymodules.SkyfileVersion,
   254  			Filesize:           datasize,
   255  			MetadataSize:       14e3,
   256  			FanoutSize:         75e3,
   257  			FanoutDataPieces:   uint8(fanoutDataPieces),
   258  			FanoutParityPieces: uint8(fanoutParityPieces),
   259  			CipherType:         crypto.TypePlain,
   260  		},
   261  		staticMetadata: skymodules.SkyfileMetadata{
   262  			Filename: "thisisafilename",
   263  			Length:   datasize,
   264  		},
   265  
   266  		staticSkylinkSector:          make([]byte, 0),
   267  		staticChunkFetcherLoaders:    chunkFetcherLoaders,
   268  		staticChunkFetchers:          chunkFetchers,
   269  		staticChunkFetchersAvailable: chunkFetchersAvailable,
   270  		staticChunkErrs:              []error{nil, nil},
   271  
   272  		staticCancelFunc: cancel,
   273  		staticCtx:        ctx,
   274  	}
   275  
   276  	if sds.DataSize() != datasize {
   277  		t.Fatal("unexpected", sds.DataSize(), datasize)
   278  	}
   279  	if sds.ID() != skymodules.DataSourceID(crypto.Hash{1, 2, 3}) {
   280  		t.Fatal("unexpected")
   281  	}
   282  	if !reflect.DeepEqual(sds.Metadata(), skymodules.SkyfileMetadata{
   283  		Filename: "thisisafilename",
   284  		Length:   datasize,
   285  	}) {
   286  		t.Fatal("unexpected")
   287  	}
   288  	if sds.RequestSize() != SkylinkDataSourceRequestSize {
   289  		t.Fatal("unexpected")
   290  	}
   291  
   292  	responseChan, err := sds.ReadSection(context.Background(), 0, types.ZeroCurrency)
   293  	if err != nil {
   294  		t.Fatal(err)
   295  	}
   296  	select {
   297  	case resp := <-responseChan:
   298  		dd, err := resp.Data()
   299  		if resp == nil || err != nil {
   300  			t.Fatal("unexpected", err)
   301  		}
   302  		respData, err := dd.Recover()
   303  		if err != nil {
   304  			t.Fatal(err)
   305  		}
   306  		if !bytes.Equal(respData, allData[:sds.RequestSize()]) {
   307  			t.Log("expected: ", allData[:sds.RequestSize()], sds.RequestSize())
   308  			t.Log("actual:   ", respData, len(respData))
   309  			t.Fatal("unexepected data")
   310  		}
   311  	case <-time.After(time.Second):
   312  		t.Fatal("unexpected")
   313  	}
   314  
   315  	select {
   316  	case <-sds.staticCtx.Done():
   317  		t.Fatal("unexpected")
   318  	case <-time.After(10 * time.Millisecond):
   319  		sds.SilentClose()
   320  	}
   321  	select {
   322  	case <-sds.staticCtx.Done():
   323  	case <-time.After(10 * time.Millisecond):
   324  		t.Fatal("unexpected")
   325  	}
   326  }
   327  
   328  // testManagedSkylinkDataSource is a unit test that verifies the chunk fetchers
   329  // are preloaded up until a certain point, and all remaining chunk fetchers get
   330  // lazy loaded when ReadStream is called
   331  func testManagedSkylinkDataSource(t *testing.T) {
   332  	if testing.Short() {
   333  		t.SkipNow()
   334  	}
   335  
   336  	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
   337  	defer cancel()
   338  
   339  	// Create a renter tester
   340  	rt, err := newRenterTester(t.Name())
   341  	if err != nil {
   342  		t.Fatal(err)
   343  	}
   344  	defer func() {
   345  		if err := rt.Close(); err != nil {
   346  			t.Fatal(err)
   347  		}
   348  	}()
   349  	r := rt.renter
   350  
   351  	// Set an allowance.
   352  	err = r.staticHostContractor.SetAllowance(skymodules.DefaultAllowance)
   353  	if err != nil {
   354  		t.Fatal(err)
   355  	}
   356  
   357  	// Add some hosts
   358  	_, err1 := rt.addHost(t.Name() + "1")
   359  	_, err2 := rt.addHost(t.Name() + "2")
   360  	_, err3 := rt.addHost(t.Name() + "3")
   361  	if err = errors.Compose(err1, err2, err3); err != nil {
   362  		t.Fatal(err)
   363  	}
   364  
   365  	// Create upload params for a large file, ensure it has 4 fanout chunks,
   366  	// which is one more than 'chunkFetchersMaximumPreload'
   367  	data := fastrand.Bytes(int(4 * modules.SectorSize))
   368  	sup := skymodules.SkyfileUploadParameters{
   369  		SiaPath:             skymodules.RandomSiaPath(),
   370  		Force:               false,
   371  		Root:                false,
   372  		BaseChunkRedundancy: 2,
   373  		Filename:            "file.tt",
   374  		Reader:              bytes.NewReader(data),
   375  	}
   376  
   377  	// Upload the skyfile
   378  	reader := skymodules.NewSkyfileReader(sup.Reader, sup)
   379  	sl, err := r.managedUploadSkyfileLargeFile(ctx, sup, reader)
   380  	if err != nil {
   381  		t.Fatal(err)
   382  	}
   383  
   384  	// Create a data source from the skylink
   385  	ppms := skymodules.DefaultSkynetPricePerMS
   386  	sbds, err := r.managedSkylinkDataSource(ctx, sl, ppms)
   387  	if err != nil {
   388  		t.Fatal(err)
   389  	}
   390  	sds, ok := sbds.(*skylinkDataSource)
   391  	if !ok {
   392  		t.Fatal("could not cast to skylinkDataSource")
   393  	}
   394  
   395  	// Create a helper that checks if the chunk is available w/o blocking
   396  	checkAvailable := func(index int) bool {
   397  		select {
   398  		case <-sds.staticChunkFetchersAvailable[index]:
   399  		case <-time.After(5 * time.Second):
   400  			return false
   401  		}
   402  		return true
   403  	}
   404  	if !checkAvailable(0) || !checkAvailable(1) || !checkAvailable(2) {
   405  		t.Fatal("expected the first three chunks to be available")
   406  	}
   407  	if checkAvailable(3) {
   408  		t.Fatal("expected the fourth chunk to not be available")
   409  	}
   410  
   411  	// Create a helper function that waits for the read response
   412  	checkData := func(c <-chan *downloadResponse) []byte {
   413  		var data []byte
   414  		select {
   415  		case resp := <-c:
   416  			dd, err := resp.Data()
   417  			if err != nil {
   418  				t.Fatal(err)
   419  			}
   420  			data, err = dd.Recover()
   421  			if err != nil {
   422  				t.Fatal(err)
   423  			}
   424  		case <-time.After(5 * time.Second):
   425  			t.Fatal("read timed out")
   426  		}
   427  		return data
   428  	}
   429  
   430  	// Read from the fourth chunk
   431  	index := 3 * modules.SectorSize / sds.RequestSize()
   432  	resp, err := sds.ReadSection(ctx, index, ppms)
   433  	if err != nil {
   434  		t.Fatal(err)
   435  	}
   436  	download := checkData(resp)
   437  	if len(download) != int(sds.RequestSize()) {
   438  		t.Fatal("unexpected data", len(download), sds.RequestSize())
   439  	}
   440  
   441  	// Assert the chunk fetcher was lazy loaded
   442  	if !checkAvailable(3) {
   443  		t.Fatal("expected the fourth chunk to be available")
   444  	}
   445  }
   446  
   447  // testSkylinkDataSourceReadLayout is a unit-test for managedReadLayout.
   448  func testSkylinkDataSourceReadLayout(t *testing.T) {
   449  	sds := newTestDataSource("somefile", []byte{1, 2, 3})
   450  
   451  	layout, data, proof := sds.managedReadLayout()
   452  
   453  	// Check returned layout.
   454  	if !reflect.DeepEqual(sds.staticLayout, layout) {
   455  		t.Fatal("layout mismatch")
   456  	}
   457  
   458  	// Data should be segment-aligned. That means 2 segments since the
   459  	// layout is always 99 bytes big.
   460  	if len(data) != 2*crypto.SegmentSize {
   461  		t.Fatalf("unexpected data length: %v != %v", len(data), 2*crypto.SegmentSize)
   462  	}
   463  
   464  	// The merkle proof for 2 segments should be 5 hashes for a merkle tree
   465  	// with 64 segments.
   466  	if len(proof) != 5 {
   467  		t.Fatalf("unexpected proof length %v != %v", len(proof), 5)
   468  	}
   469  
   470  	// Verify the proof.
   471  	valid := crypto.VerifyRangeProof(data, proof, 0, 2, sds.staticSkylink.MerkleRoot())
   472  	if !valid {
   473  		t.Fatal("proof is not valid")
   474  	}
   475  }