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 }