github.com/ethersphere/bee/v2@v2.2.0/pkg/pusher/pusher_test.go (about) 1 // Copyright 2020 The Swarm Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package pusher_test 6 7 import ( 8 "context" 9 "errors" 10 "sync" 11 "sync/atomic" 12 "testing" 13 "time" 14 15 "github.com/ethereum/go-ethereum/common" 16 17 "github.com/ethersphere/bee/v2/pkg/crypto" 18 "github.com/ethersphere/bee/v2/pkg/log" 19 "github.com/ethersphere/bee/v2/pkg/postage" 20 "github.com/ethersphere/bee/v2/pkg/pusher" 21 "github.com/ethersphere/bee/v2/pkg/pushsync" 22 pushsyncmock "github.com/ethersphere/bee/v2/pkg/pushsync/mock" 23 "github.com/ethersphere/bee/v2/pkg/spinlock" 24 storage "github.com/ethersphere/bee/v2/pkg/storage" 25 testingc "github.com/ethersphere/bee/v2/pkg/storage/testing" 26 "github.com/ethersphere/bee/v2/pkg/swarm" 27 "github.com/ethersphere/bee/v2/pkg/topology" 28 "github.com/ethersphere/bee/v2/pkg/util/testutil" 29 ) 30 31 // time to wait for received response from pushsync 32 const spinTimeout = time.Second * 3 33 34 var ( 35 block = common.HexToHash("0x1").Bytes() 36 defaultMockValidStamp = func(ch swarm.Chunk) (swarm.Chunk, error) { 37 return ch, nil 38 } 39 defaultRetryCount = 3 40 ) 41 42 type mockStorer struct { 43 chunks chan swarm.Chunk 44 reportedMu sync.Mutex 45 reportedSynced []swarm.Chunk 46 reportedFailed []swarm.Chunk 47 reportedStored []swarm.Chunk 48 storedChunks map[string]swarm.Chunk 49 } 50 51 func (m *mockStorer) SubscribePush(ctx context.Context) (c <-chan swarm.Chunk, stop func()) { 52 return m.chunks, func() { close(m.chunks) } 53 } 54 55 func (m *mockStorer) Report(ctx context.Context, chunk swarm.Chunk, state storage.ChunkState) error { 56 m.reportedMu.Lock() 57 defer m.reportedMu.Unlock() 58 59 switch state { 60 case storage.ChunkSynced: 61 m.reportedSynced = append(m.reportedSynced, chunk) 62 case storage.ChunkCouldNotSync: 63 m.reportedFailed = append(m.reportedFailed, chunk) 64 case storage.ChunkStored: 65 m.reportedStored = append(m.reportedStored, chunk) 66 } 67 return nil 68 } 69 70 func (m *mockStorer) isReported(chunk swarm.Chunk, state storage.ChunkState) bool { 71 m.reportedMu.Lock() 72 defer m.reportedMu.Unlock() 73 74 switch state { 75 case storage.ChunkSynced: 76 for _, ch := range m.reportedSynced { 77 if ch.Equal(chunk) { 78 return true 79 } 80 } 81 case storage.ChunkCouldNotSync: 82 for _, ch := range m.reportedFailed { 83 if ch.Equal(chunk) { 84 return true 85 } 86 } 87 case storage.ChunkStored: 88 for _, ch := range m.reportedStored { 89 if ch.Equal(chunk) { 90 return true 91 } 92 } 93 } 94 95 return false 96 } 97 98 func (m *mockStorer) ReservePutter() storage.Putter { 99 return storage.PutterFunc( 100 func(ctx context.Context, chunk swarm.Chunk) error { 101 if m.storedChunks == nil { 102 m.storedChunks = make(map[string]swarm.Chunk) 103 } 104 m.storedChunks[chunk.Address().ByteString()] = chunk 105 return nil 106 }, 107 ) 108 } 109 110 // TestSendChunkToPushSync sends a chunk to pushsync to be sent to its closest peer and get a receipt. 111 // once the receipt is got this check to see if the localstore is updated to see if the chunk is set 112 // as ModeSetSync status. 113 func TestChunkSyncing(t *testing.T) { 114 t.Parallel() 115 116 key, _ := crypto.GenerateSecp256k1Key() 117 signer := crypto.NewDefaultSigner(key) 118 119 pushSyncService := pushsyncmock.New(func(ctx context.Context, chunk swarm.Chunk) (*pushsync.Receipt, error) { 120 signature, _ := signer.Sign(chunk.Address().Bytes()) 121 receipt := &pushsync.Receipt{ 122 Address: swarm.NewAddress(chunk.Address().Bytes()), 123 Signature: signature, 124 Nonce: block, 125 } 126 return receipt, nil 127 }) 128 129 storer := &mockStorer{ 130 chunks: make(chan swarm.Chunk), 131 } 132 133 pusherSvc := createPusher( 134 t, 135 storer, 136 pushSyncService, 137 defaultMockValidStamp, 138 defaultRetryCount, 139 ) 140 141 t.Run("deferred", func(t *testing.T) { 142 chunk := testingc.GenerateTestRandomChunk() 143 storer.chunks <- chunk 144 145 err := spinlock.Wait(spinTimeout, func() bool { 146 return storer.isReported(chunk, storage.ChunkSynced) 147 }) 148 if err != nil { 149 t.Fatal(err) 150 } 151 }) 152 153 t.Run("direct", func(t *testing.T) { 154 chunk := testingc.GenerateTestRandomChunk() 155 156 newFeed := make(chan *pusher.Op) 157 errC := make(chan error, 1) 158 pusherSvc.AddFeed(newFeed) 159 160 newFeed <- &pusher.Op{Chunk: chunk, Err: errC, Direct: true} 161 162 err := <-errC 163 if err != nil { 164 t.Fatalf("unexpected error on push %v", err) 165 } 166 }) 167 } 168 169 func TestChunkStored(t *testing.T) { 170 t.Parallel() 171 172 pushSyncService := pushsyncmock.New(func(ctx context.Context, chunk swarm.Chunk) (*pushsync.Receipt, error) { 173 return nil, topology.ErrWantSelf 174 }) 175 176 storer := &mockStorer{ 177 chunks: make(chan swarm.Chunk), 178 } 179 180 pusherSvc := createPusher( 181 t, 182 storer, 183 pushSyncService, 184 defaultMockValidStamp, 185 defaultRetryCount, 186 ) 187 188 t.Run("deferred", func(t *testing.T) { 189 chunk := testingc.GenerateTestRandomChunk() 190 storer.chunks <- chunk 191 192 err := spinlock.Wait(spinTimeout, func() bool { 193 return storer.isReported(chunk, storage.ChunkStored) 194 }) 195 if err != nil { 196 t.Fatal(err) 197 } 198 if ch, found := storer.storedChunks[chunk.Address().ByteString()]; !found || !ch.Equal(chunk) { 199 t.Fatalf("chunk not found in the store") 200 } 201 }) 202 203 t.Run("direct", func(t *testing.T) { 204 chunk := testingc.GenerateTestRandomChunk() 205 206 newFeed := make(chan *pusher.Op) 207 errC := make(chan error, 1) 208 pusherSvc.AddFeed(newFeed) 209 210 newFeed <- &pusher.Op{Chunk: chunk, Err: errC, Direct: true} 211 212 err := <-errC 213 if err != nil { 214 t.Fatalf("unexpected error on push %v", err) 215 } 216 if ch, found := storer.storedChunks[chunk.Address().ByteString()]; !found || !ch.Equal(chunk) { 217 t.Fatalf("chunk not found in the store") 218 } 219 }) 220 } 221 222 // TestSendChunkAndReceiveInvalidReceipt sends a chunk to pushsync to be sent to its closest peer and 223 // get a invalid receipt (not with the address of the chunk sent). The test makes sure that this error 224 // is received and the ModeSetSync is not set for the chunk. 225 func TestSendChunkAndReceiveInvalidReceipt(t *testing.T) { 226 t.Parallel() 227 228 chunk := testingc.GenerateTestRandomChunk() 229 230 pushSyncService := pushsyncmock.New(func(ctx context.Context, chunk swarm.Chunk) (*pushsync.Receipt, error) { 231 return nil, errors.New("invalid receipt") 232 }) 233 234 storer := &mockStorer{ 235 chunks: make(chan swarm.Chunk), 236 } 237 238 _ = createPusher( 239 t, 240 storer, 241 pushSyncService, 242 defaultMockValidStamp, 243 defaultRetryCount, 244 ) 245 246 storer.chunks <- chunk 247 248 err := spinlock.Wait(spinTimeout, func() bool { 249 return storer.isReported(chunk, storage.ChunkSynced) 250 }) 251 if err == nil { 252 t.Fatalf("chunk not syned error expected") 253 } 254 } 255 256 // TestSendChunkAndTimeoutinReceivingReceipt sends a chunk to pushsync to be sent to its closest peer and 257 // expects a timeout to get instead of getting a receipt. The test makes sure that timeout error 258 // is received and the ModeSetSync is not set for the chunk. 259 func TestSendChunkAndTimeoutinReceivingReceipt(t *testing.T) { 260 t.Parallel() 261 262 chunk := testingc.GenerateTestRandomChunk() 263 264 key, _ := crypto.GenerateSecp256k1Key() 265 signer := crypto.NewDefaultSigner(key) 266 267 pushSyncService := pushsyncmock.New(func(ctx context.Context, chunk swarm.Chunk) (*pushsync.Receipt, error) { 268 time.Sleep(5 * time.Second) 269 signature, _ := signer.Sign(chunk.Address().Bytes()) 270 receipt := &pushsync.Receipt{ 271 Address: swarm.NewAddress(chunk.Address().Bytes()), 272 Signature: signature, 273 Nonce: block, 274 } 275 return receipt, nil 276 }) 277 278 storer := &mockStorer{ 279 chunks: make(chan swarm.Chunk), 280 } 281 282 _ = createPusher( 283 t, 284 storer, 285 pushSyncService, 286 defaultMockValidStamp, 287 defaultRetryCount, 288 ) 289 290 storer.chunks <- chunk 291 292 err := spinlock.Wait(spinTimeout, func() bool { 293 return storer.isReported(chunk, storage.ChunkSynced) 294 }) 295 if err == nil { 296 t.Fatalf("chunk not syned error expected") 297 } 298 } 299 300 func TestPusherRetryShallow(t *testing.T) { 301 t.Parallel() 302 303 var ( 304 closestPeer = swarm.MustParseHexAddress("f000000000000000000000000000000000000000000000000000000000000000") 305 key, _ = crypto.GenerateSecp256k1Key() 306 signer = crypto.NewDefaultSigner(key) 307 callCount = int32(0) 308 retryCount = 3 // pushync will retry on behalf of push for shallow receipts, so no retries are made on the side of the pusher. 309 ) 310 pushSyncService := pushsyncmock.New(func(ctx context.Context, chunk swarm.Chunk) (*pushsync.Receipt, error) { 311 atomic.AddInt32(&callCount, 1) 312 signature, _ := signer.Sign(chunk.Address().Bytes()) 313 receipt := &pushsync.Receipt{ 314 Address: swarm.NewAddress(chunk.Address().Bytes()), 315 Signature: signature, 316 Nonce: block, 317 } 318 return receipt, pushsync.ErrShallowReceipt 319 }) 320 321 storer := &mockStorer{ 322 chunks: make(chan swarm.Chunk), 323 } 324 325 _ = createPusher( 326 t, 327 storer, 328 pushSyncService, 329 defaultMockValidStamp, 330 defaultRetryCount, 331 ) 332 333 // generate a chunk at PO 1 with closestPeer, meaning that we get a 334 // receipt which is shallower than the pivot peer's depth, resulting 335 // in retries 336 chunk := testingc.GenerateTestRandomChunkAt(t, closestPeer, 1) 337 338 storer.chunks <- chunk 339 340 err := spinlock.Wait(spinTimeout, func() bool { 341 c := int(atomic.LoadInt32(&callCount)) 342 return c == retryCount 343 }) 344 if err != nil { 345 t.Fatal(err) 346 } 347 } 348 349 // TestChunkWithInvalidStampSkipped tests that chunks with invalid stamps are skipped in pusher 350 func TestChunkWithInvalidStampSkipped(t *testing.T) { 351 t.Parallel() 352 353 key, _ := crypto.GenerateSecp256k1Key() 354 signer := crypto.NewDefaultSigner(key) 355 356 pushSyncService := pushsyncmock.New(func(ctx context.Context, chunk swarm.Chunk) (*pushsync.Receipt, error) { 357 signature, _ := signer.Sign(chunk.Address().Bytes()) 358 receipt := &pushsync.Receipt{ 359 Address: swarm.NewAddress(chunk.Address().Bytes()), 360 Signature: signature, 361 Nonce: block, 362 } 363 return receipt, nil 364 }) 365 366 wantErr := errors.New("dummy error") 367 validStamp := func(ch swarm.Chunk) (swarm.Chunk, error) { 368 return nil, wantErr 369 } 370 371 storer := &mockStorer{ 372 chunks: make(chan swarm.Chunk), 373 } 374 375 pusherSvc := createPusher( 376 t, 377 storer, 378 pushSyncService, 379 validStamp, 380 defaultRetryCount, 381 ) 382 383 t.Run("deferred", func(t *testing.T) { 384 chunk := testingc.GenerateTestRandomChunk() 385 storer.chunks <- chunk 386 387 err := spinlock.Wait(spinTimeout, func() bool { 388 return storer.isReported(chunk, storage.ChunkCouldNotSync) 389 }) 390 if err != nil { 391 t.Fatal(err) 392 } 393 }) 394 395 t.Run("direct", func(t *testing.T) { 396 chunk := testingc.GenerateTestRandomChunk() 397 398 newFeed := make(chan *pusher.Op) 399 errC := make(chan error, 1) 400 pusherSvc.AddFeed(newFeed) 401 402 newFeed <- &pusher.Op{Chunk: chunk, Err: errC, Direct: true} 403 404 err := <-errC 405 if !errors.Is(err, wantErr) { 406 t.Fatalf("unexpected error on push %v", err) 407 } 408 }) 409 } 410 411 func createPusher( 412 t *testing.T, 413 storer pusher.Storer, 414 pushSyncService pushsync.PushSyncer, 415 validStamp postage.ValidStampFn, 416 retryCount int, 417 ) *pusher.Service { 418 t.Helper() 419 420 pusherService := pusher.New(1, storer, pushSyncService, validStamp, log.Noop, 0, retryCount) 421 testutil.CleanupCloser(t, pusherService) 422 423 return pusherService 424 }