github.com/celestiaorg/celestia-node@v0.15.0-beta.1/share/eds/store_test.go (about) 1 package eds 2 3 import ( 4 "context" 5 "io" 6 "os" 7 "sync" 8 "testing" 9 "time" 10 11 "github.com/filecoin-project/dagstore" 12 "github.com/filecoin-project/dagstore/shard" 13 "github.com/ipfs/go-cid" 14 "github.com/ipfs/go-datastore" 15 ds_sync "github.com/ipfs/go-datastore/sync" 16 dsbadger "github.com/ipfs/go-ds-badger4" 17 "github.com/ipld/go-car" 18 "github.com/stretchr/testify/assert" 19 "github.com/stretchr/testify/require" 20 21 "github.com/celestiaorg/celestia-app/pkg/da" 22 "github.com/celestiaorg/rsmt2d" 23 24 "github.com/celestiaorg/celestia-node/share" 25 "github.com/celestiaorg/celestia-node/share/eds/cache" 26 "github.com/celestiaorg/celestia-node/share/eds/edstest" 27 "github.com/celestiaorg/celestia-node/share/ipld" 28 ) 29 30 func TestEDSStore(t *testing.T) { 31 ctx, cancel := context.WithCancel(context.Background()) 32 t.Cleanup(cancel) 33 34 edsStore, err := newStore(t) 35 require.NoError(t, err) 36 err = edsStore.Start(ctx) 37 require.NoError(t, err) 38 39 // PutRegistersShard tests if Put registers the shard on the underlying DAGStore 40 t.Run("PutRegistersShard", func(t *testing.T) { 41 eds, dah := randomEDS(t) 42 43 // shard hasn't been registered yet 44 has, err := edsStore.Has(ctx, dah.Hash()) 45 assert.False(t, has) 46 assert.NoError(t, err) 47 48 err = edsStore.Put(ctx, dah.Hash(), eds) 49 assert.NoError(t, err) 50 51 _, err = edsStore.dgstr.GetShardInfo(shard.KeyFromString(dah.String())) 52 assert.NoError(t, err) 53 }) 54 55 // PutIndexesEDS ensures that Putting an EDS indexes it into the car index 56 t.Run("PutIndexesEDS", func(t *testing.T) { 57 eds, dah := randomEDS(t) 58 59 stat, _ := edsStore.carIdx.StatFullIndex(shard.KeyFromString(dah.String())) 60 assert.False(t, stat.Exists) 61 62 err = edsStore.Put(ctx, dah.Hash(), eds) 63 assert.NoError(t, err) 64 65 stat, err = edsStore.carIdx.StatFullIndex(shard.KeyFromString(dah.String())) 66 assert.True(t, stat.Exists) 67 assert.NoError(t, err) 68 }) 69 70 // GetCAR ensures that the reader returned from GetCAR is capable of reading the CAR header and 71 // ODS. 72 t.Run("GetCAR", func(t *testing.T) { 73 eds, dah := randomEDS(t) 74 75 err = edsStore.Put(ctx, dah.Hash(), eds) 76 require.NoError(t, err) 77 78 r, err := edsStore.GetCAR(ctx, dah.Hash()) 79 assert.NoError(t, err) 80 defer func() { 81 require.NoError(t, r.Close()) 82 }() 83 carReader, err := car.NewCarReader(r) 84 assert.NoError(t, err) 85 86 for i := 0; i < 4; i++ { 87 for j := 0; j < 4; j++ { 88 original := eds.GetCell(uint(i), uint(j)) 89 block, err := carReader.Next() 90 assert.NoError(t, err) 91 assert.Equal(t, original, share.GetData(block.RawData())) 92 } 93 } 94 }) 95 96 t.Run("item not exist", func(t *testing.T) { 97 root := share.DataHash{1} 98 _, err := edsStore.GetCAR(ctx, root) 99 assert.ErrorIs(t, err, ErrNotFound) 100 101 _, err = edsStore.GetDAH(ctx, root) 102 assert.ErrorIs(t, err, ErrNotFound) 103 104 _, err = edsStore.CARBlockstore(ctx, root) 105 assert.ErrorIs(t, err, ErrNotFound) 106 }) 107 108 t.Run("Remove", func(t *testing.T) { 109 eds, dah := randomEDS(t) 110 111 err = edsStore.Put(ctx, dah.Hash(), eds) 112 require.NoError(t, err) 113 114 // assert that file now exists 115 _, err = os.Stat(edsStore.basepath + blocksPath + dah.String()) 116 assert.NoError(t, err) 117 118 // accessor will be registered in cache async on put, so give it some time to settle 119 time.Sleep(time.Millisecond * 100) 120 121 err = edsStore.Remove(ctx, dah.Hash()) 122 assert.NoError(t, err) 123 124 // shard should no longer be registered on the dagstore 125 _, err = edsStore.dgstr.GetShardInfo(shard.KeyFromString(dah.String())) 126 assert.Error(t, err, "shard not found") 127 128 // shard should have been dropped from the index, which also removes the file under /index/ 129 indexStat, err := edsStore.carIdx.StatFullIndex(shard.KeyFromString(dah.String())) 130 assert.NoError(t, err) 131 assert.False(t, indexStat.Exists) 132 133 // file no longer exists 134 _, err = os.Stat(edsStore.basepath + blocksPath + dah.String()) 135 assert.ErrorContains(t, err, "no such file or directory") 136 }) 137 138 t.Run("Remove after OpShardFail", func(t *testing.T) { 139 eds, dah := randomEDS(t) 140 141 err = edsStore.Put(ctx, dah.Hash(), eds) 142 require.NoError(t, err) 143 144 // assert that shard now exists 145 ok, err := edsStore.Has(ctx, dah.Hash()) 146 assert.NoError(t, err) 147 assert.True(t, ok) 148 149 // assert that file now exists 150 path := edsStore.basepath + blocksPath + dah.String() 151 _, err = os.Stat(path) 152 assert.NoError(t, err) 153 154 err = os.Remove(path) 155 assert.NoError(t, err) 156 157 // accessor will be registered in cache async on put, so give it some time to settle 158 time.Sleep(time.Millisecond * 100) 159 160 // remove non-failed accessor from cache 161 err = edsStore.cache.Load().Remove(shard.KeyFromString(dah.String())) 162 assert.NoError(t, err) 163 164 _, err = edsStore.GetCAR(ctx, dah.Hash()) 165 assert.Error(t, err) 166 167 ticker := time.NewTicker(time.Millisecond * 100) 168 defer ticker.Stop() 169 for { 170 select { 171 case <-ticker.C: 172 has, err := edsStore.Has(ctx, dah.Hash()) 173 if err == nil && !has { 174 // shard no longer exists after OpShardFail was detected from GetCAR call 175 return 176 } 177 case <-ctx.Done(): 178 t.Fatal("timeout waiting for shard to be removed") 179 } 180 } 181 }) 182 183 t.Run("Has", func(t *testing.T) { 184 eds, dah := randomEDS(t) 185 186 ok, err := edsStore.Has(ctx, dah.Hash()) 187 assert.NoError(t, err) 188 assert.False(t, ok) 189 190 err = edsStore.Put(ctx, dah.Hash(), eds) 191 assert.NoError(t, err) 192 193 ok, err = edsStore.Has(ctx, dah.Hash()) 194 assert.NoError(t, err) 195 assert.True(t, ok) 196 }) 197 198 t.Run("RecentBlocksCache", func(t *testing.T) { 199 eds, dah := randomEDS(t) 200 err = edsStore.Put(ctx, dah.Hash(), eds) 201 require.NoError(t, err) 202 203 // accessor will be registered in cache async on put, so give it some time to settle 204 time.Sleep(time.Millisecond * 100) 205 206 // check, that the key is in the cache after put 207 shardKey := shard.KeyFromString(dah.String()) 208 _, err = edsStore.cache.Load().Get(shardKey) 209 assert.NoError(t, err) 210 }) 211 212 t.Run("List", func(t *testing.T) { 213 const amount = 10 214 hashes := make([]share.DataHash, 0, amount) 215 for range make([]byte, amount) { 216 eds, dah := randomEDS(t) 217 err = edsStore.Put(ctx, dah.Hash(), eds) 218 require.NoError(t, err) 219 hashes = append(hashes, dah.Hash()) 220 } 221 222 hashesOut, err := edsStore.List() 223 require.NoError(t, err) 224 for _, hash := range hashes { 225 assert.Contains(t, hashesOut, hash) 226 } 227 }) 228 229 t.Run("Parallel put", func(t *testing.T) { 230 const amount = 20 231 eds, dah := randomEDS(t) 232 233 wg := sync.WaitGroup{} 234 for i := 1; i < amount; i++ { 235 wg.Add(1) 236 go func() { 237 defer wg.Done() 238 err := edsStore.Put(ctx, dah.Hash(), eds) 239 if err != nil { 240 require.ErrorIs(t, err, dagstore.ErrShardExists) 241 } 242 }() 243 } 244 wg.Wait() 245 246 eds, err := edsStore.Get(ctx, dah.Hash()) 247 require.NoError(t, err) 248 newDah, err := da.NewDataAvailabilityHeader(eds) 249 require.NoError(t, err) 250 require.Equal(t, dah.Hash(), newDah.Hash()) 251 }) 252 } 253 254 // TestEDSStore_GC verifies that unused transient shards are collected by the GC periodically. 255 func TestEDSStore_GC(t *testing.T) { 256 ctx, cancel := context.WithCancel(context.Background()) 257 t.Cleanup(cancel) 258 259 edsStore, err := newStore(t) 260 edsStore.gcInterval = time.Second 261 require.NoError(t, err) 262 263 // kicks off the gc goroutine 264 err = edsStore.Start(ctx) 265 require.NoError(t, err) 266 267 eds, dah := randomEDS(t) 268 shardKey := shard.KeyFromString(dah.String()) 269 270 err = edsStore.Put(ctx, dah.Hash(), eds) 271 require.NoError(t, err) 272 273 // accessor will be registered in cache async on put, so give it some time to settle 274 time.Sleep(time.Millisecond * 100) 275 276 // remove links to the shard from cache 277 time.Sleep(time.Millisecond * 100) 278 key := shard.KeyFromString(share.DataHash(dah.Hash()).String()) 279 err = edsStore.cache.Load().Remove(key) 280 require.NoError(t, err) 281 282 // doesn't exist yet 283 assert.NotContains(t, edsStore.lastGCResult.Load().Shards, shardKey) 284 285 // wait for gc to run, retry three times 286 for i := 0; i < 3; i++ { 287 time.Sleep(edsStore.gcInterval) 288 if _, ok := edsStore.lastGCResult.Load().Shards[shardKey]; ok { 289 break 290 } 291 } 292 assert.Contains(t, edsStore.lastGCResult.Load().Shards, shardKey) 293 294 // assert nil in this context means there was no error re-acquiring the shard during GC 295 assert.Nil(t, edsStore.lastGCResult.Load().Shards[shardKey]) 296 } 297 298 func Test_BlockstoreCache(t *testing.T) { 299 ctx, cancel := context.WithCancel(context.Background()) 300 t.Cleanup(cancel) 301 302 edsStore, err := newStore(t) 303 require.NoError(t, err) 304 err = edsStore.Start(ctx) 305 require.NoError(t, err) 306 307 // store eds to the store with noopCache to allow clean cache after put 308 swap := edsStore.cache.Load() 309 edsStore.cache.Store(cache.NewDoubleCache(cache.NoopCache{}, cache.NoopCache{})) 310 eds, dah := randomEDS(t) 311 err = edsStore.Put(ctx, dah.Hash(), eds) 312 require.NoError(t, err) 313 314 // get any key from saved eds 315 bs, err := edsStore.carBlockstore(ctx, dah.Hash()) 316 require.NoError(t, err) 317 defer func() { 318 require.NoError(t, bs.Close()) 319 }() 320 keys, err := bs.AllKeysChan(ctx) 321 require.NoError(t, err) 322 var key cid.Cid 323 select { 324 case key = <-keys: 325 case <-ctx.Done(): 326 t.Fatal("context timeout") 327 } 328 329 // swap back original cache 330 edsStore.cache.Store(swap) 331 332 // key shouldn't be in cache yet, check for returned errCacheMiss 333 shardKey := shard.KeyFromString(dah.String()) 334 _, err = edsStore.cache.Load().Get(shardKey) 335 require.Error(t, err) 336 337 // now get it from blockstore, to trigger storing to cache 338 _, err = edsStore.Blockstore().Get(ctx, key) 339 require.NoError(t, err) 340 341 // should be no errCacheMiss anymore 342 _, err = edsStore.cache.Load().Get(shardKey) 343 require.NoError(t, err) 344 } 345 346 // Test_CachedAccessor verifies that the reader represented by a cached accessor can be read from 347 // multiple times, without exhausting the underlying reader. 348 func Test_CachedAccessor(t *testing.T) { 349 ctx, cancel := context.WithCancel(context.Background()) 350 t.Cleanup(cancel) 351 352 edsStore, err := newStore(t) 353 require.NoError(t, err) 354 err = edsStore.Start(ctx) 355 require.NoError(t, err) 356 357 eds, dah := randomEDS(t) 358 err = edsStore.Put(ctx, dah.Hash(), eds) 359 require.NoError(t, err) 360 361 // accessor will be registered in cache async on put, so give it some time to settle 362 time.Sleep(time.Millisecond * 100) 363 364 // accessor should be in cache 365 _, err = edsStore.cache.Load().Get(shard.KeyFromString(dah.String())) 366 require.NoError(t, err) 367 368 // first read from cached accessor 369 carReader, err := edsStore.getCAR(ctx, dah.Hash()) 370 require.NoError(t, err) 371 firstBlock, err := io.ReadAll(carReader) 372 require.NoError(t, err) 373 require.NoError(t, carReader.Close()) 374 375 // second read from cached accessor 376 carReader, err = edsStore.getCAR(ctx, dah.Hash()) 377 require.NoError(t, err) 378 secondBlock, err := io.ReadAll(carReader) 379 require.NoError(t, err) 380 require.NoError(t, carReader.Close()) 381 382 require.Equal(t, firstBlock, secondBlock) 383 } 384 385 // Test_CachedAccessor verifies that the reader represented by a accessor obtained directly from 386 // dagstore can be read from multiple times, without exhausting the underlying reader. 387 func Test_NotCachedAccessor(t *testing.T) { 388 ctx, cancel := context.WithCancel(context.Background()) 389 t.Cleanup(cancel) 390 391 edsStore, err := newStore(t) 392 require.NoError(t, err) 393 err = edsStore.Start(ctx) 394 require.NoError(t, err) 395 // replace cache with noopCache to 396 edsStore.cache.Store(cache.NewDoubleCache(cache.NoopCache{}, cache.NoopCache{})) 397 398 eds, dah := randomEDS(t) 399 err = edsStore.Put(ctx, dah.Hash(), eds) 400 require.NoError(t, err) 401 402 // accessor will be registered in cache async on put, so give it some time to settle 403 time.Sleep(time.Millisecond * 100) 404 405 // accessor should not be in cache 406 _, err = edsStore.cache.Load().Get(shard.KeyFromString(dah.String())) 407 require.Error(t, err) 408 409 // first read from direct accessor (not from cache) 410 carReader, err := edsStore.getCAR(ctx, dah.Hash()) 411 require.NoError(t, err) 412 firstBlock, err := io.ReadAll(carReader) 413 require.NoError(t, err) 414 require.NoError(t, carReader.Close()) 415 416 // second read from direct accessor (not from cache) 417 carReader, err = edsStore.getCAR(ctx, dah.Hash()) 418 require.NoError(t, err) 419 secondBlock, err := io.ReadAll(carReader) 420 require.NoError(t, err) 421 require.NoError(t, carReader.Close()) 422 423 require.Equal(t, firstBlock, secondBlock) 424 } 425 426 func BenchmarkStore(b *testing.B) { 427 ctx, cancel := context.WithCancel(context.Background()) 428 b.Cleanup(cancel) 429 430 ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) 431 edsStore, err := NewStore(DefaultParameters(), b.TempDir(), ds) 432 require.NoError(b, err) 433 err = edsStore.Start(ctx) 434 require.NoError(b, err) 435 436 // BenchmarkStore/bench_put_128-10 10 3231859283 ns/op (~3sec) 437 b.Run("bench put 128", func(b *testing.B) { 438 b.ResetTimer() 439 for i := 0; i < b.N; i++ { 440 // pause the timer for initializing test data 441 b.StopTimer() 442 eds := edstest.RandEDS(b, 128) 443 dah, err := share.NewRoot(eds) 444 require.NoError(b, err) 445 b.StartTimer() 446 447 err = edsStore.Put(ctx, dah.Hash(), eds) 448 require.NoError(b, err) 449 } 450 }) 451 452 // BenchmarkStore/bench_read_128-10 14 78970661 ns/op (~70ms) 453 b.Run("bench read 128", func(b *testing.B) { 454 b.ResetTimer() 455 for i := 0; i < b.N; i++ { 456 // pause the timer for initializing test data 457 b.StopTimer() 458 eds := edstest.RandEDS(b, 128) 459 dah, err := share.NewRoot(eds) 460 require.NoError(b, err) 461 _ = edsStore.Put(ctx, dah.Hash(), eds) 462 b.StartTimer() 463 464 _, err = edsStore.Get(ctx, dah.Hash()) 465 require.NoError(b, err) 466 } 467 }) 468 } 469 470 // BenchmarkCacheEviction benchmarks the time it takes to load a block to the cache, when the 471 // cache size is set to 1. This forces cache eviction on every read. 472 // BenchmarkCacheEviction-10/128 384 3533586 ns/op (~3ms) 473 func BenchmarkCacheEviction(b *testing.B) { 474 const ( 475 blocks = 4 476 size = 128 477 ) 478 479 ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) 480 b.Cleanup(cancel) 481 482 dir := b.TempDir() 483 ds, err := dsbadger.NewDatastore(dir, &dsbadger.DefaultOptions) 484 require.NoError(b, err) 485 486 newStore := func(params *Parameters) *Store { 487 edsStore, err := NewStore(params, dir, ds) 488 require.NoError(b, err) 489 err = edsStore.Start(ctx) 490 require.NoError(b, err) 491 return edsStore 492 } 493 edsStore := newStore(DefaultParameters()) 494 495 // generate EDSs and store them 496 cids := make([]cid.Cid, blocks) 497 for i := range cids { 498 eds := edstest.RandEDS(b, size) 499 dah, err := da.NewDataAvailabilityHeader(eds) 500 require.NoError(b, err) 501 err = edsStore.Put(ctx, dah.Hash(), eds) 502 require.NoError(b, err) 503 504 // store cids for read loop later 505 cids[i] = ipld.MustCidFromNamespacedSha256(dah.RowRoots[0]) 506 } 507 508 // restart store to clear cache 509 require.NoError(b, edsStore.Stop(ctx)) 510 511 // set BlockstoreCacheSize to 1 to force eviction on every read 512 params := DefaultParameters() 513 params.BlockstoreCacheSize = 1 514 bstore := newStore(params).Blockstore() 515 516 // start benchmark 517 b.ResetTimer() 518 for i := 0; i < b.N; i++ { 519 h := cids[i%blocks] 520 // every read will trigger eviction 521 _, err := bstore.Get(ctx, h) 522 require.NoError(b, err) 523 } 524 } 525 526 func newStore(t *testing.T) (*Store, error) { 527 t.Helper() 528 529 ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) 530 return NewStore(DefaultParameters(), t.TempDir(), ds) 531 } 532 533 func randomEDS(t *testing.T) (*rsmt2d.ExtendedDataSquare, *share.Root) { 534 eds := edstest.RandEDS(t, 4) 535 dah, err := share.NewRoot(eds) 536 require.NoError(t, err) 537 538 return eds, dah 539 }