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  }