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 }