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  }