github.com/celestiaorg/celestia-node@v0.15.0-beta.1/share/getters/getter_test.go (about)

     1  package getters
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"sync"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/ipfs/boxo/exchange/offline"
    11  	"github.com/ipfs/go-datastore"
    12  	ds_sync "github.com/ipfs/go-datastore/sync"
    13  	dsbadger "github.com/ipfs/go-ds-badger4"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  
    17  	"github.com/celestiaorg/celestia-app/pkg/da"
    18  	"github.com/celestiaorg/celestia-app/pkg/wrapper"
    19  	"github.com/celestiaorg/rsmt2d"
    20  
    21  	"github.com/celestiaorg/celestia-node/header"
    22  	"github.com/celestiaorg/celestia-node/header/headertest"
    23  	"github.com/celestiaorg/celestia-node/share"
    24  	"github.com/celestiaorg/celestia-node/share/eds"
    25  	"github.com/celestiaorg/celestia-node/share/eds/edstest"
    26  	"github.com/celestiaorg/celestia-node/share/ipld"
    27  	"github.com/celestiaorg/celestia-node/share/sharetest"
    28  )
    29  
    30  func TestStoreGetter(t *testing.T) {
    31  	ctx, cancel := context.WithCancel(context.Background())
    32  	t.Cleanup(cancel)
    33  
    34  	tmpDir := t.TempDir()
    35  	storeCfg := eds.DefaultParameters()
    36  	ds := ds_sync.MutexWrap(datastore.NewMapDatastore())
    37  	edsStore, err := eds.NewStore(storeCfg, tmpDir, ds)
    38  	require.NoError(t, err)
    39  
    40  	err = edsStore.Start(ctx)
    41  	require.NoError(t, err)
    42  
    43  	sg := NewStoreGetter(edsStore)
    44  
    45  	t.Run("GetShare", func(t *testing.T) {
    46  		randEds, eh := randomEDS(t)
    47  		err = edsStore.Put(ctx, eh.DAH.Hash(), randEds)
    48  		require.NoError(t, err)
    49  
    50  		squareSize := int(randEds.Width())
    51  		for i := 0; i < squareSize; i++ {
    52  			for j := 0; j < squareSize; j++ {
    53  				share, err := sg.GetShare(ctx, eh, i, j)
    54  				require.NoError(t, err)
    55  				assert.Equal(t, randEds.GetCell(uint(i), uint(j)), share)
    56  			}
    57  		}
    58  
    59  		// doesn't panic on indexes too high
    60  		_, err := sg.GetShare(ctx, eh, squareSize, squareSize)
    61  		require.ErrorIs(t, err, share.ErrOutOfBounds)
    62  
    63  		// root not found
    64  		_, eh = randomEDS(t)
    65  		_, err = sg.GetShare(ctx, eh, 0, 0)
    66  		require.ErrorIs(t, err, share.ErrNotFound)
    67  	})
    68  
    69  	t.Run("GetEDS", func(t *testing.T) {
    70  		randEds, eh := randomEDS(t)
    71  		err = edsStore.Put(ctx, eh.DAH.Hash(), randEds)
    72  		require.NoError(t, err)
    73  
    74  		retrievedEDS, err := sg.GetEDS(ctx, eh)
    75  		require.NoError(t, err)
    76  		assert.True(t, randEds.Equals(retrievedEDS))
    77  
    78  		// root not found
    79  		emptyRoot := da.MinDataAvailabilityHeader()
    80  		eh.DAH = &emptyRoot
    81  		_, err = sg.GetEDS(ctx, eh)
    82  		require.ErrorIs(t, err, share.ErrNotFound)
    83  	})
    84  
    85  	t.Run("GetSharesByNamespace", func(t *testing.T) {
    86  		randEds, namespace, eh := randomEDSWithDoubledNamespace(t, 4)
    87  		err = edsStore.Put(ctx, eh.DAH.Hash(), randEds)
    88  		require.NoError(t, err)
    89  
    90  		shares, err := sg.GetSharesByNamespace(ctx, eh, namespace)
    91  		require.NoError(t, err)
    92  		require.NoError(t, shares.Verify(eh.DAH, namespace))
    93  		assert.Len(t, shares.Flatten(), 2)
    94  
    95  		// namespace not found
    96  		randNamespace := sharetest.RandV0Namespace()
    97  		emptyShares, err := sg.GetSharesByNamespace(ctx, eh, randNamespace)
    98  		require.NoError(t, err)
    99  		require.Empty(t, emptyShares.Flatten())
   100  
   101  		// root not found
   102  		emptyRoot := da.MinDataAvailabilityHeader()
   103  		eh.DAH = &emptyRoot
   104  		_, err = sg.GetSharesByNamespace(ctx, eh, namespace)
   105  		require.ErrorIs(t, err, share.ErrNotFound)
   106  	})
   107  
   108  	t.Run("GetSharesFromNamespace removes corrupted shard", func(t *testing.T) {
   109  		randEds, namespace, eh := randomEDSWithDoubledNamespace(t, 4)
   110  		err = edsStore.Put(ctx, eh.DAH.Hash(), randEds)
   111  		require.NoError(t, err)
   112  
   113  		// available
   114  		shares, err := sg.GetSharesByNamespace(ctx, eh, namespace)
   115  		require.NoError(t, err)
   116  		require.NoError(t, shares.Verify(eh.DAH, namespace))
   117  		assert.Len(t, shares.Flatten(), 2)
   118  
   119  		// 'corrupt' existing CAR by overwriting with a random EDS
   120  		f, err := os.OpenFile(tmpDir+"/blocks/"+eh.DAH.String(), os.O_WRONLY, 0644)
   121  		require.NoError(t, err)
   122  		edsToOverwriteWith, eh := randomEDS(t)
   123  		err = eds.WriteEDS(ctx, edsToOverwriteWith, f)
   124  		require.NoError(t, err)
   125  
   126  		shares, err = sg.GetSharesByNamespace(ctx, eh, namespace)
   127  		require.ErrorIs(t, err, share.ErrNotFound)
   128  		require.Nil(t, shares)
   129  
   130  		// corruption detected, shard is removed
   131  		// try every 200ms until it passes or the context ends
   132  		ticker := time.NewTicker(200 * time.Millisecond)
   133  		defer ticker.Stop()
   134  		for {
   135  			select {
   136  			case <-ctx.Done():
   137  				t.Fatal("context ended before successful retrieval")
   138  			case <-ticker.C:
   139  				has, err := edsStore.Has(ctx, eh.DAH.Hash())
   140  				if err != nil {
   141  					t.Fatal(err)
   142  				}
   143  				if !has {
   144  					require.NoError(t, err)
   145  					return
   146  				}
   147  			}
   148  		}
   149  	})
   150  }
   151  
   152  func TestIPLDGetter(t *testing.T) {
   153  	ctx, cancel := context.WithCancel(context.Background())
   154  	t.Cleanup(cancel)
   155  
   156  	storeCfg := eds.DefaultParameters()
   157  	ds := ds_sync.MutexWrap(datastore.NewMapDatastore())
   158  	edsStore, err := eds.NewStore(storeCfg, t.TempDir(), ds)
   159  	require.NoError(t, err)
   160  
   161  	err = edsStore.Start(ctx)
   162  	require.NoError(t, err)
   163  
   164  	bStore := edsStore.Blockstore()
   165  	bserv := ipld.NewBlockservice(bStore, offline.Exchange(edsStore.Blockstore()))
   166  	sg := NewIPLDGetter(bserv)
   167  
   168  	t.Run("GetShare", func(t *testing.T) {
   169  		ctx, cancel := context.WithTimeout(ctx, time.Second)
   170  		t.Cleanup(cancel)
   171  
   172  		randEds, eh := randomEDS(t)
   173  		err = edsStore.Put(ctx, eh.DAH.Hash(), randEds)
   174  		require.NoError(t, err)
   175  
   176  		squareSize := int(randEds.Width())
   177  		for i := 0; i < squareSize; i++ {
   178  			for j := 0; j < squareSize; j++ {
   179  				share, err := sg.GetShare(ctx, eh, i, j)
   180  				require.NoError(t, err)
   181  				assert.Equal(t, randEds.GetCell(uint(i), uint(j)), share)
   182  			}
   183  		}
   184  
   185  		// doesn't panic on indexes too high
   186  		_, err := sg.GetShare(ctx, eh, squareSize+1, squareSize+1)
   187  		require.ErrorIs(t, err, share.ErrOutOfBounds)
   188  
   189  		// root not found
   190  		_, eh = randomEDS(t)
   191  		_, err = sg.GetShare(ctx, eh, 0, 0)
   192  		require.ErrorIs(t, err, share.ErrNotFound)
   193  	})
   194  
   195  	t.Run("GetEDS", func(t *testing.T) {
   196  		ctx, cancel := context.WithTimeout(ctx, time.Second)
   197  		t.Cleanup(cancel)
   198  
   199  		randEds, eh := randomEDS(t)
   200  		err = edsStore.Put(ctx, eh.DAH.Hash(), randEds)
   201  		require.NoError(t, err)
   202  
   203  		retrievedEDS, err := sg.GetEDS(ctx, eh)
   204  		require.NoError(t, err)
   205  		assert.True(t, randEds.Equals(retrievedEDS))
   206  
   207  		// Ensure blocks still exist after cleanup
   208  		colRoots, _ := retrievedEDS.ColRoots()
   209  		has, err := bStore.Has(ctx, ipld.MustCidFromNamespacedSha256(colRoots[0]))
   210  		assert.NoError(t, err)
   211  		assert.True(t, has)
   212  	})
   213  
   214  	t.Run("GetSharesByNamespace", func(t *testing.T) {
   215  		ctx, cancel := context.WithTimeout(ctx, time.Second)
   216  		t.Cleanup(cancel)
   217  
   218  		randEds, namespace, eh := randomEDSWithDoubledNamespace(t, 4)
   219  		err = edsStore.Put(ctx, eh.DAH.Hash(), randEds)
   220  		require.NoError(t, err)
   221  
   222  		// first check that shares are returned correctly if they exist
   223  		shares, err := sg.GetSharesByNamespace(ctx, eh, namespace)
   224  		require.NoError(t, err)
   225  		require.NoError(t, shares.Verify(eh.DAH, namespace))
   226  		assert.Len(t, shares.Flatten(), 2)
   227  
   228  		// namespace not found
   229  		randNamespace := sharetest.RandV0Namespace()
   230  		emptyShares, err := sg.GetSharesByNamespace(ctx, eh, randNamespace)
   231  		require.NoError(t, err)
   232  		require.Empty(t, emptyShares.Flatten())
   233  
   234  		// nid doesn't exist in root
   235  		emptyRoot := da.MinDataAvailabilityHeader()
   236  		eh.DAH = &emptyRoot
   237  		emptyShares, err = sg.GetSharesByNamespace(ctx, eh, namespace)
   238  		require.NoError(t, err)
   239  		require.Empty(t, emptyShares.Flatten())
   240  	})
   241  }
   242  
   243  // BenchmarkIPLDGetterOverBusyCache benchmarks the performance of the IPLDGetter when the
   244  // cache size of the underlying blockstore is less than the number of blocks being requested in
   245  // parallel. This is to ensure performance doesn't degrade when the cache is being frequently
   246  // evicted.
   247  // BenchmarkIPLDGetterOverBusyCache-10/128    	       1	12460428417 ns/op (~12s)
   248  func BenchmarkIPLDGetterOverBusyCache(b *testing.B) {
   249  	const (
   250  		blocks = 10
   251  		size   = 128
   252  	)
   253  
   254  	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
   255  	b.Cleanup(cancel)
   256  
   257  	dir := b.TempDir()
   258  	ds, err := dsbadger.NewDatastore(dir, &dsbadger.DefaultOptions)
   259  	require.NoError(b, err)
   260  
   261  	newStore := func(params *eds.Parameters) *eds.Store {
   262  		edsStore, err := eds.NewStore(params, dir, ds)
   263  		require.NoError(b, err)
   264  		err = edsStore.Start(ctx)
   265  		require.NoError(b, err)
   266  		return edsStore
   267  	}
   268  	edsStore := newStore(eds.DefaultParameters())
   269  
   270  	// generate EDSs and store them
   271  	headers := make([]*header.ExtendedHeader, blocks)
   272  	for i := range headers {
   273  		eds := edstest.RandEDS(b, size)
   274  		dah, err := da.NewDataAvailabilityHeader(eds)
   275  		require.NoError(b, err)
   276  		err = edsStore.Put(ctx, dah.Hash(), eds)
   277  		require.NoError(b, err)
   278  
   279  		eh := headertest.RandExtendedHeader(b)
   280  		eh.DAH = &dah
   281  
   282  		// store cids for read loop later
   283  		headers[i] = eh
   284  	}
   285  
   286  	// restart store to clear cache
   287  	require.NoError(b, edsStore.Stop(ctx))
   288  
   289  	// set BlockstoreCacheSize to 1 to force eviction on every read
   290  	params := eds.DefaultParameters()
   291  	params.BlockstoreCacheSize = 1
   292  	edsStore = newStore(params)
   293  	bstore := edsStore.Blockstore()
   294  	bserv := ipld.NewBlockservice(bstore, offline.Exchange(bstore))
   295  
   296  	// start client
   297  	getter := NewIPLDGetter(bserv)
   298  
   299  	// request blocks in parallel
   300  	b.ResetTimer()
   301  	g := sync.WaitGroup{}
   302  	g.Add(blocks)
   303  	for _, h := range headers {
   304  		h := h
   305  		go func() {
   306  			defer g.Done()
   307  			_, err := getter.GetEDS(ctx, h)
   308  			require.NoError(b, err)
   309  		}()
   310  	}
   311  	g.Wait()
   312  }
   313  
   314  func randomEDS(t *testing.T) (*rsmt2d.ExtendedDataSquare, *header.ExtendedHeader) {
   315  	eds := edstest.RandEDS(t, 4)
   316  	dah, err := share.NewRoot(eds)
   317  	require.NoError(t, err)
   318  	eh := headertest.RandExtendedHeaderWithRoot(t, dah)
   319  	return eds, eh
   320  }
   321  
   322  // randomEDSWithDoubledNamespace generates a random EDS and ensures that there are two shares in the
   323  // middle that share a namespace.
   324  func randomEDSWithDoubledNamespace(
   325  	t *testing.T,
   326  	size int,
   327  ) (*rsmt2d.ExtendedDataSquare, []byte, *header.ExtendedHeader) {
   328  	n := size * size
   329  	randShares := sharetest.RandShares(t, n)
   330  	idx1 := (n - 1) / 2
   331  	idx2 := n / 2
   332  
   333  	// Make it so that the two shares in two different rows have a common
   334  	// namespace. For example if size=4, the original data square looks like
   335  	// this:
   336  	// _ _ _ _
   337  	// _ _ _ D
   338  	// D _ _ _
   339  	// _ _ _ _
   340  	// where the D shares have a common namespace.
   341  	copy(share.GetNamespace(randShares[idx2]), share.GetNamespace(randShares[idx1]))
   342  
   343  	eds, err := rsmt2d.ComputeExtendedDataSquare(
   344  		randShares,
   345  		share.DefaultRSMT2DCodec(),
   346  		wrapper.NewConstructor(uint64(size)),
   347  	)
   348  	require.NoError(t, err, "failure to recompute the extended data square")
   349  	dah, err := share.NewRoot(eds)
   350  	require.NoError(t, err)
   351  	eh := headertest.RandExtendedHeaderWithRoot(t, dah)
   352  
   353  	return eds, share.GetNamespace(randShares[idx1]), eh
   354  }