github.com/ethersphere/bee/v2@v2.2.0/pkg/storageincentives/agent_test.go (about)

     1  // Copyright 2022 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 storageincentives_test
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"math/big"
    11  	"sync"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/ethereum/go-ethereum/common"
    16  	"github.com/ethereum/go-ethereum/core/types"
    17  	"github.com/ethersphere/bee/v2/pkg/log"
    18  	"github.com/ethersphere/bee/v2/pkg/postage"
    19  	contractMock "github.com/ethersphere/bee/v2/pkg/postage/postagecontract/mock"
    20  	erc20mock "github.com/ethersphere/bee/v2/pkg/settlement/swap/erc20/mock"
    21  	statestore "github.com/ethersphere/bee/v2/pkg/statestore/mock"
    22  	"github.com/ethersphere/bee/v2/pkg/storageincentives"
    23  	"github.com/ethersphere/bee/v2/pkg/storageincentives/redistribution"
    24  	"github.com/ethersphere/bee/v2/pkg/storageincentives/staking/mock"
    25  	"github.com/ethersphere/bee/v2/pkg/storer"
    26  	resMock "github.com/ethersphere/bee/v2/pkg/storer/mock"
    27  	"github.com/ethersphere/bee/v2/pkg/swarm"
    28  	transactionmock "github.com/ethersphere/bee/v2/pkg/transaction/mock"
    29  	"github.com/ethersphere/bee/v2/pkg/util/testutil"
    30  )
    31  
    32  func TestAgent(t *testing.T) {
    33  	t.Parallel()
    34  
    35  	bigBalance := big.NewInt(4_000_000_000)
    36  	tests := []struct {
    37  		name           string
    38  		blocksPerRound uint64
    39  		blocksPerPhase uint64
    40  		incrementBy    uint64
    41  		limit          uint64
    42  		expectedCalls  bool
    43  		balance        *big.Int
    44  	}{{
    45  		name:           "3 blocks per phase, same block number returns twice",
    46  		blocksPerRound: 9,
    47  		blocksPerPhase: 3,
    48  		incrementBy:    1,
    49  		expectedCalls:  true,
    50  		limit:          108, // computed with blocksPerRound * (exptectedCalls + 2)
    51  		balance:        bigBalance,
    52  	}, {
    53  		name:           "3 blocks per phase, block number returns every block",
    54  		blocksPerRound: 9,
    55  		blocksPerPhase: 3,
    56  		incrementBy:    1,
    57  		expectedCalls:  true,
    58  		limit:          108,
    59  		balance:        bigBalance,
    60  	}, {
    61  		name:           "no expected calls - block number returns late after each phase",
    62  		blocksPerRound: 9,
    63  		blocksPerPhase: 3,
    64  		incrementBy:    6,
    65  		expectedCalls:  false,
    66  		limit:          108,
    67  		balance:        bigBalance,
    68  	}, {
    69  		name:           "4 blocks per phase, block number returns every other block",
    70  		blocksPerRound: 12,
    71  		blocksPerPhase: 4,
    72  		incrementBy:    2,
    73  		expectedCalls:  true,
    74  		limit:          144,
    75  		balance:        bigBalance,
    76  	}, {
    77  		// This test case is based on previous, but this time agent will not have enough
    78  		// balance to participate in the game so no calls are going to be made.
    79  		name:           "no expected calls - insufficient balance",
    80  		blocksPerRound: 12,
    81  		blocksPerPhase: 4,
    82  		incrementBy:    2,
    83  		expectedCalls:  false,
    84  		limit:          144,
    85  		balance:        big.NewInt(0),
    86  	},
    87  	}
    88  
    89  	for _, tc := range tests {
    90  		tc := tc
    91  		t.Run(tc.name, func(t *testing.T) {
    92  			t.Parallel()
    93  
    94  			wait := make(chan struct{})
    95  			addr := swarm.RandAddress(t)
    96  
    97  			backend := &mockchainBackend{
    98  				limit: tc.limit,
    99  				limitCallback: func() {
   100  					select {
   101  					case wait <- struct{}{}:
   102  					default:
   103  					}
   104  				},
   105  				incrementBy: tc.incrementBy,
   106  				block:       tc.blocksPerRound,
   107  				balance:     tc.balance,
   108  			}
   109  			contract := &mockContract{}
   110  
   111  			service, _ := createService(t, addr, backend, contract, tc.blocksPerRound, tc.blocksPerPhase)
   112  			testutil.CleanupCloser(t, service)
   113  
   114  			<-wait
   115  
   116  			if !tc.expectedCalls {
   117  				if len(contract.callsList) > 0 {
   118  					t.Fatal("got unexpected calls")
   119  				} else {
   120  					return
   121  				}
   122  			}
   123  
   124  			assertOrder := func(t *testing.T, want, got contractCall) {
   125  				t.Helper()
   126  				if want != got {
   127  					t.Fatalf("expected call %s, got %s", want, got)
   128  				}
   129  			}
   130  
   131  			contract.mtx.Lock()
   132  			defer contract.mtx.Unlock()
   133  
   134  			prevCall := contract.callsList[0]
   135  
   136  			for i := 1; i < len(contract.callsList); i++ {
   137  
   138  				switch contract.callsList[i] {
   139  				case isWinnerCall:
   140  					assertOrder(t, revealCall, prevCall)
   141  				case revealCall:
   142  					assertOrder(t, commitCall, prevCall)
   143  				case commitCall:
   144  					assertOrder(t, isWinnerCall, prevCall)
   145  				}
   146  
   147  				prevCall = contract.callsList[i]
   148  			}
   149  		})
   150  	}
   151  }
   152  
   153  func createService(
   154  	t *testing.T,
   155  	addr swarm.Address,
   156  	backend storageincentives.ChainBackend,
   157  	contract redistribution.Contract,
   158  	blocksPerRound uint64,
   159  	blocksPerPhase uint64) (*storageincentives.Agent, error) {
   160  	t.Helper()
   161  
   162  	postageContract := contractMock.New(contractMock.WithExpiresBatchesFunc(func(context.Context) error {
   163  		return nil
   164  	}),
   165  	)
   166  	stakingContract := mock.New(mock.WithIsFrozen(func(context.Context, uint64) (bool, error) {
   167  		return false, nil
   168  	}))
   169  
   170  	reserve := resMock.NewReserve(
   171  		resMock.WithRadius(0),
   172  		resMock.WithSample(storer.RandSample(t, nil)),
   173  	)
   174  
   175  	return storageincentives.New(
   176  		addr, common.Address{},
   177  		backend,
   178  		contract,
   179  		postageContract,
   180  		stakingContract,
   181  		reserve,
   182  		func() bool { return true },
   183  		time.Millisecond*100,
   184  		blocksPerRound,
   185  		blocksPerPhase,
   186  		statestore.NewStateStore(),
   187  		&postage.NoOpBatchStore{},
   188  		erc20mock.New(),
   189  		transactionmock.New(),
   190  		&mockHealth{},
   191  		log.Noop,
   192  	)
   193  }
   194  
   195  type mockchainBackend struct {
   196  	mu            sync.Mutex
   197  	incrementBy   uint64
   198  	block         uint64
   199  	limit         uint64
   200  	limitCallback func()
   201  	balance       *big.Int
   202  }
   203  
   204  func (m *mockchainBackend) BlockNumber(context.Context) (uint64, error) {
   205  	m.mu.Lock()
   206  	defer m.mu.Unlock()
   207  
   208  	ret := m.block
   209  	lim := m.limit
   210  	inc := m.incrementBy
   211  
   212  	if lim == 0 || ret+inc < lim {
   213  		m.block += inc
   214  	} else if m.limitCallback != nil {
   215  		m.limitCallback()
   216  		return 0, errors.New("reached limit")
   217  	}
   218  
   219  	return ret, nil
   220  }
   221  
   222  func (m *mockchainBackend) HeaderByNumber(context.Context, *big.Int) (*types.Header, error) {
   223  	return &types.Header{
   224  		Time: uint64(time.Now().Unix()),
   225  	}, nil
   226  }
   227  
   228  func (m *mockchainBackend) BalanceAt(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error) {
   229  	return m.balance, nil
   230  }
   231  
   232  func (m *mockchainBackend) SuggestGasPrice(ctx context.Context) (*big.Int, error) {
   233  	return big.NewInt(4), nil
   234  }
   235  
   236  type contractCall int
   237  
   238  func (c contractCall) String() string {
   239  	switch c {
   240  	case isWinnerCall:
   241  		return "isWinnerCall"
   242  	case revealCall:
   243  		return "revealCall"
   244  	case commitCall:
   245  		return "commitCall"
   246  	case claimCall:
   247  		return "claimCall"
   248  	}
   249  	return "unknown"
   250  }
   251  
   252  const (
   253  	isWinnerCall contractCall = iota
   254  	revealCall
   255  	commitCall
   256  	claimCall
   257  )
   258  
   259  type mockContract struct {
   260  	callsList []contractCall
   261  	mtx       sync.Mutex
   262  }
   263  
   264  func (m *mockContract) ReserveSalt(context.Context) ([]byte, error) {
   265  	return nil, nil
   266  }
   267  
   268  func (m *mockContract) IsPlaying(context.Context, uint8) (bool, error) {
   269  	return true, nil
   270  }
   271  
   272  func (m *mockContract) IsWinner(context.Context) (bool, error) {
   273  	m.mtx.Lock()
   274  	defer m.mtx.Unlock()
   275  	m.callsList = append(m.callsList, isWinnerCall)
   276  	return false, nil
   277  }
   278  
   279  func (m *mockContract) Claim(context.Context, redistribution.ChunkInclusionProofs) (common.Hash, error) {
   280  	m.mtx.Lock()
   281  	defer m.mtx.Unlock()
   282  	m.callsList = append(m.callsList, claimCall)
   283  	return common.Hash{}, nil
   284  }
   285  
   286  func (m *mockContract) Commit(context.Context, []byte, uint64) (common.Hash, error) {
   287  	m.mtx.Lock()
   288  	defer m.mtx.Unlock()
   289  	m.callsList = append(m.callsList, commitCall)
   290  	return common.Hash{}, nil
   291  }
   292  
   293  func (m *mockContract) Reveal(context.Context, uint8, []byte, []byte) (common.Hash, error) {
   294  	m.mtx.Lock()
   295  	defer m.mtx.Unlock()
   296  	m.callsList = append(m.callsList, revealCall)
   297  	return common.Hash{}, nil
   298  }
   299  
   300  type mockHealth struct{}
   301  
   302  func (m *mockHealth) IsHealthy() bool { return true }