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

     1  package renter
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"reflect"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"gitlab.com/NebulousLabs/errors"
    14  	"gitlab.com/NebulousLabs/fastrand"
    15  	"gitlab.com/SkynetLabs/skyd/build"
    16  	"gitlab.com/SkynetLabs/skyd/skymodules"
    17  	"go.sia.tech/siad/crypto"
    18  	"go.sia.tech/siad/modules"
    19  )
    20  
    21  // TestIsCompressedFanout is a unit test for isCompressedFanout.
    22  func TestIsCompressedFanout(t *testing.T) {
    23  	tests := []struct {
    24  		minPieces int
    25  		ct        crypto.CipherType
    26  		expected  bool
    27  	}{
    28  		{
    29  			minPieces: 1,
    30  			ct:        crypto.TypePlain,
    31  			expected:  true,
    32  		},
    33  		{
    34  			minPieces: 1,
    35  			ct:        crypto.TypeTwofish,
    36  			expected:  false,
    37  		},
    38  		{
    39  			minPieces: 1,
    40  			ct:        crypto.TypeThreefish,
    41  			expected:  false,
    42  		},
    43  		{
    44  			minPieces: 1,
    45  			ct:        crypto.TypeXChaCha20,
    46  			expected:  false,
    47  		},
    48  		{
    49  			minPieces: 2,
    50  			ct:        crypto.TypePlain,
    51  			expected:  false,
    52  		},
    53  	}
    54  
    55  	for i, test := range tests {
    56  		ec, err := skymodules.NewRSSubCode(test.minPieces, 1, crypto.SegmentSize)
    57  		if err != nil {
    58  			t.Fatal(err)
    59  		}
    60  		if isCompressedFanout(ec, test.ct) != test.expected {
    61  			t.Fatalf("testcase %v failed", i+1)
    62  		}
    63  	}
    64  }
    65  
    66  // TestTryResolveSkylinkV2 is a unit test for managedTryResolveSkylinkV2.
    67  func TestTryResolveSkylinkV2(t *testing.T) {
    68  	if testing.Short() {
    69  		t.SkipNow()
    70  	}
    71  	t.Parallel()
    72  
    73  	wt, err := newWorkerTester(t.Name())
    74  	if err != nil {
    75  		t.Fatal(err)
    76  	}
    77  
    78  	var mr crypto.Hash
    79  	fastrand.Read(mr[:])
    80  	skylinkV1, err := skymodules.NewSkylinkV1(mr, 0, 0)
    81  	if err != nil {
    82  		t.Fatal(err)
    83  	}
    84  
    85  	// Set skylink on host.
    86  	srv, spk, sk := randomRegistryValue()
    87  	srv.Data = skylinkV1.Bytes()
    88  	srv.Revision++
    89  	srv = srv.Sign(sk)
    90  	err = wt.UpdateRegistry(context.Background(), spk, srv)
    91  	if err != nil {
    92  		t.Fatal(err)
    93  	}
    94  
    95  	// Get the v2 skylink.
    96  	skylinkV2 := skymodules.NewSkylinkV2(spk, srv.Tweak)
    97  
    98  	// Resolve it.
    99  	slV1, entries, err := wt.rt.renter.managedTryResolveSkylinkV2(context.Background(), skylinkV2, true, false)
   100  	if err != nil {
   101  		t.Fatal(err)
   102  	}
   103  
   104  	// Skylinks should match.
   105  	if !reflect.DeepEqual(skylinkV1, slV1) {
   106  		t.Fatal("skylinks don't match")
   107  	}
   108  
   109  	// Should only be one entry
   110  	if len(entries) != 1 {
   111  		t.Fatal("Expected only 1 entry, got", len(entries))
   112  	}
   113  	entry := entries[0]
   114  
   115  	// Entry shouldn't be nil.
   116  	expectedSRV := skymodules.NewRegistryEntry(spk, srv)
   117  	if !reflect.DeepEqual(entry, expectedSRV) {
   118  		t.Log(entry)
   119  		t.Log(expectedSRV)
   120  		t.Fatal("entry mismatch")
   121  	}
   122  
   123  	// Try resolving the v1 skylink. Should be a no-op.
   124  	slV1, entries, err = wt.rt.renter.managedTryResolveSkylinkV2(context.Background(), skylinkV1, true, false)
   125  	if err != nil {
   126  		t.Fatal(err)
   127  	}
   128  	if !reflect.DeepEqual(skylinkV1, slV1) {
   129  		t.Fatal("skylinks don't match")
   130  	}
   131  	if entries != nil {
   132  		t.Fatal("entries should be nil")
   133  	}
   134  }
   135  
   136  // TestShortFanoutPanic is a regression test for when the size of a fanout
   137  // doesn't match the filesize.
   138  func TestShortFanoutPanic(t *testing.T) {
   139  	if testing.Short() {
   140  		t.SkipNow()
   141  	}
   142  	t.Parallel()
   143  
   144  	wt, err := newWorkerTester(t.Name())
   145  	if err != nil {
   146  		t.Fatal(err)
   147  	}
   148  
   149  	// Add 2 more hosts.
   150  	if _, err = wt.rt.addHost(t.Name() + "1"); err != nil {
   151  		t.Fatal(err)
   152  	}
   153  	if _, err = wt.rt.addHost(t.Name() + "2"); err != nil {
   154  		t.Fatal(err)
   155  	}
   156  	r := wt.rt.renter
   157  
   158  	// Wait for them to show up as workers.
   159  	err = build.Retry(600, 100*time.Millisecond, func() error {
   160  		_, err := wt.rt.miner.AddBlock()
   161  		if err != nil {
   162  			return err
   163  		}
   164  		r.staticWorkerPool.callUpdate(r)
   165  		workers := r.staticWorkerPool.callWorkers()
   166  		if len(workers) < 3 {
   167  			return fmt.Errorf("expected %v workers but got %v", 3, len(workers))
   168  		}
   169  		return nil
   170  	})
   171  	if err != nil {
   172  		t.Fatal(err)
   173  	}
   174  
   175  	// Prepare a metadata for a basic file.
   176  	fileSize := modules.SectorSize * 3
   177  	md := skymodules.SkyfileMetadata{
   178  		Filename: "test",
   179  		Length:   fileSize,
   180  	}
   181  	metadataBytes, err := skymodules.SkyfileMetadataBytes(md)
   182  	if err != nil {
   183  		t.Fatal(err)
   184  	}
   185  
   186  	// Get some random data and fanout.
   187  	data := fastrand.Bytes(int(fileSize))
   188  	ec, err := skymodules.NewRSSubCode(2, 1, crypto.SegmentSize)
   189  	if err != nil {
   190  		t.Fatal(err)
   191  	}
   192  
   193  	// Init the upload.
   194  	fileNode, err := r.managedInitFileNode(skymodules.FileUploadParams{
   195  		CipherType:  crypto.TypePlain,
   196  		ErasureCode: ec,
   197  		SiaPath:     skymodules.RandomSiaPath(),
   198  	}, 0)
   199  	if err != nil {
   200  		t.Fatal(err)
   201  	}
   202  
   203  	// Upload the fanout.
   204  	chunkReader := NewFanoutChunkReader(bytes.NewReader(data), fileNode.ErasureCode(), fileNode.MasterKey())
   205  	_, err = r.callUploadStreamFromReaderWithFileNode(context.Background(), fileNode, chunkReader, 0)
   206  	if err != nil {
   207  		t.Fatal(err)
   208  	}
   209  	if err := fileNode.Close(); err != nil {
   210  		t.Fatal(err)
   211  	}
   212  	fanout := chunkReader.Fanout()
   213  
   214  	// Shrink the fanout to 1 chunk.
   215  	fanout = fanout[:ec.NumPieces()*crypto.HashSize]
   216  
   217  	// Prepare a layout.
   218  	sl := skymodules.SkyfileLayout{
   219  		Version:            skymodules.SkyfileVersion,
   220  		Filesize:           fileSize,
   221  		MetadataSize:       uint64(len(metadataBytes)),
   222  		FanoutSize:         uint64(len(fanout)),
   223  		FanoutDataPieces:   uint8(ec.MinPieces()),
   224  		FanoutParityPieces: uint8(ec.NumPieces() - ec.MinPieces()),
   225  		CipherType:         crypto.TypePlain,
   226  	}
   227  
   228  	// Prepare a base sector with fanout but also with the file data.
   229  	bs, fetchSize, _ := skymodules.BuildBaseSector(sl.Encode(), fanout, metadataBytes, nil)
   230  	if err != nil {
   231  		t.Fatal(err)
   232  	}
   233  	skylink, err := skymodules.NewSkylinkV1(crypto.MerkleRoot(bs), 0, fetchSize)
   234  	if err != nil {
   235  		t.Fatal(err)
   236  	}
   237  
   238  	// Upload the base sector.
   239  	err = r.managedUploadBaseSector(context.Background(), skymodules.SkyfileUploadParameters{
   240  		SiaPath: skymodules.RandomSiaPath(),
   241  	}, bs, skylink)
   242  	if err != nil {
   243  		t.Fatal(err)
   244  	}
   245  
   246  	// Download the file. This should fail due to the short fanout.
   247  	_, _, err = r.DownloadSkylink(skylink, time.Hour, skymodules.DefaultSkynetPricePerMS)
   248  	if err == nil || !strings.Contains(err.Error(), skymodules.ErrMalformedBaseSector.Error()) {
   249  		t.Fatal(err)
   250  	}
   251  }
   252  
   253  // TestParseSkyfileMetadata tests parsing a recursive skyfile metadata.
   254  func TestParseSkyfileMetadataRecursive(t *testing.T) {
   255  	if testing.Short() {
   256  		t.SkipNow()
   257  	}
   258  
   259  	wt, err := newWorkerTester(t.Name())
   260  	if err != nil {
   261  		t.Fatal(err)
   262  	}
   263  	defer func() {
   264  		if err := wt.Close(); err != nil {
   265  			t.Fatal(err)
   266  		}
   267  	}()
   268  	r := wt.rt.renter
   269  
   270  	// Prepare a metadata for a basic file.
   271  	fileSize := 3 * modules.SectorSize * modules.SectorSize
   272  	md := skymodules.SkyfileMetadata{
   273  		Filename: "test",
   274  		Length:   fileSize,
   275  	}
   276  	metadataBytes, err := skymodules.SkyfileMetadataBytes(md)
   277  	if err != nil {
   278  		t.Fatal(err)
   279  	}
   280  
   281  	// Upload a huge amount of data to force a base sector recursion of
   282  	// depth 1.
   283  	data := fastrand.Bytes(int(fileSize))
   284  	ec, err := skymodules.NewRSSubCode(2, 1, crypto.SegmentSize)
   285  	if err != nil {
   286  		t.Fatal(err)
   287  	}
   288  
   289  	// Upload the fanout. We don't really upload it here. Instead we just
   290  	// use a chunkreader to figure out what the fanout would be for that
   291  	// file.
   292  	fileNode, err := r.managedInitFileNode(skymodules.FileUploadParams{
   293  		CipherType:  crypto.TypePlain,
   294  		ErasureCode: ec,
   295  		SiaPath:     skymodules.RandomSiaPath(),
   296  	}, 0)
   297  	if err != nil {
   298  		t.Fatal(err)
   299  	}
   300  	chunkReader := NewFanoutChunkReader(bytes.NewReader(data), fileNode.ErasureCode(), fileNode.MasterKey())
   301  	if err := fileNode.Close(); err != nil {
   302  		t.Fatal(err)
   303  	}
   304  
   305  	// Read all chunks.
   306  	for {
   307  		_, _, err := chunkReader.ReadChunk()
   308  		if errors.Contains(err, io.EOF) {
   309  			break // done
   310  		}
   311  		if err != nil {
   312  			t.Fatal(err)
   313  		}
   314  	}
   315  	fanout := chunkReader.Fanout()
   316  
   317  	// Prepare a valid layout.
   318  	sl := skymodules.NewSkyfileLayout(fileSize, uint64(len(metadataBytes)), uint64(len(fanout)), ec, crypto.TypePlain)
   319  
   320  	// Prepare a base sector with fanout but also with the file data.
   321  	bs, fetchSize, extension := skymodules.BuildBaseSector(sl.Encode(), fanout, metadataBytes, nil)
   322  	if err != nil {
   323  		t.Fatal(err)
   324  	}
   325  	if len(extension) != 2 {
   326  		t.Fatal("the depth of the returned extension should be 2", len(extension))
   327  	}
   328  	if len(bs) != int(modules.SectorSize) {
   329  		t.Fatal("basesector should be exactly a sectorsize large at this point", len(bs))
   330  	}
   331  
   332  	skylink, err := skymodules.NewSkylinkV1(crypto.MerkleRoot(bs), 0, fetchSize)
   333  	if err != nil {
   334  		t.Fatal(err)
   335  	}
   336  	for _, ext := range extension {
   337  		bs = append(bs, ext...)
   338  	}
   339  
   340  	// Upload the base sector.
   341  	err = r.managedUploadBaseSector(context.Background(), skymodules.SkyfileUploadParameters{
   342  		SiaPath: skymodules.RandomSkynetFilePath(),
   343  	}, bs, skylink)
   344  	if err != nil {
   345  		t.Fatal(err)
   346  	}
   347  
   348  	// Download the base sector using the skylink and parse it.
   349  	offset, fetchSize, err := skylink.OffsetAndFetchSize()
   350  	if err != nil {
   351  		t.Fatal(err)
   352  	}
   353  	var bs2 []byte
   354  	err = build.Retry(600, 100*time.Millisecond, func() error {
   355  		bs2, _, _, err = r.managedDownloadByRoot(context.Background(), skylink.MerkleRoot(), offset, fetchSize, skymodules.DefaultSkynetPricePerMS)
   356  		if err != nil {
   357  			r.staticWorkerPool.callUpdate(r)
   358  			return err
   359  		}
   360  		return nil
   361  	})
   362  	if err != nil {
   363  		t.Fatal(err)
   364  	}
   365  
   366  	// Compare base sectors.
   367  	if !bytes.Equal(bs[:modules.SectorSize], bs2) {
   368  		t.Fatal("base sectors don't match")
   369  	}
   370  
   371  	var sl2 skymodules.SkyfileLayout
   372  	var fanout2, rawSM []byte
   373  	err = build.Retry(600, 100*time.Millisecond, func() error {
   374  		sl2, fanout2, _, rawSM, _, _, err = r.ParseSkyfileMetadata(bs2)
   375  		if err != nil {
   376  			r.staticWorkerPool.callUpdate(r)
   377  			return err
   378  		}
   379  		return nil
   380  	})
   381  	if err != nil {
   382  		t.Fatal(err)
   383  	}
   384  
   385  	// Compare fanouts.
   386  	if !bytes.Equal(sl.Encode(), sl2.Encode()) {
   387  		t.Fatal("fanout mismatch")
   388  	}
   389  	if !bytes.Equal(metadataBytes, rawSM) {
   390  		t.Fatal("md mismatch")
   391  	}
   392  	if !bytes.Equal(fanout, fanout2) {
   393  		t.Fatal("fanout mismatch")
   394  	}
   395  }