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

     1  // Copyright 2023 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
     6  
     7  import (
     8  	"context"
     9  	"math/big"
    10  	"math/rand"
    11  	"testing"
    12  
    13  	"github.com/ethereum/go-ethereum/common"
    14  	erc20mock "github.com/ethersphere/bee/v2/pkg/settlement/swap/erc20/mock"
    15  	"github.com/ethersphere/bee/v2/pkg/statestore/mock"
    16  	"github.com/ethersphere/bee/v2/pkg/swarm"
    17  	transactionmock "github.com/ethersphere/bee/v2/pkg/transaction/mock"
    18  	"github.com/ethersphere/bee/v2/pkg/util/testutil"
    19  	"github.com/google/go-cmp/cmp"
    20  )
    21  
    22  func createRedistribution(t *testing.T, erc20Opts []erc20mock.Option, txOpts []transactionmock.Option) *RedistributionState {
    23  	t.Helper()
    24  	if erc20Opts == nil {
    25  		erc20Opts = []erc20mock.Option{
    26  			erc20mock.WithBalanceOfFunc(func(ctx context.Context, address common.Address) (*big.Int, error) {
    27  				return big.NewInt(1000), nil
    28  			}),
    29  		}
    30  	}
    31  	if txOpts == nil {
    32  		txOpts = []transactionmock.Option{
    33  			transactionmock.WithTransactionFeeFunc(func(ctx context.Context, txHash common.Hash) (*big.Int, error) {
    34  				return big.NewInt(1000), nil
    35  			}),
    36  		}
    37  	}
    38  	log := testutil.NewLogger(t)
    39  	state, err := NewRedistributionState(log, common.Address{}, mock.NewStateStore(), erc20mock.New(erc20Opts...), transactionmock.New(txOpts...))
    40  	if err != nil {
    41  		t.Fatal("failed to connect")
    42  	}
    43  	return state
    44  }
    45  
    46  func TestState(t *testing.T) {
    47  	t.Parallel()
    48  	input := Status{
    49  		Phase:           commit,
    50  		IsFrozen:        true,
    51  		IsFullySynced:   true,
    52  		Round:           2,
    53  		LastWonRound:    2,
    54  		LastPlayedRound: 2,
    55  		LastFrozenRound: 2,
    56  		Block:           2,
    57  	}
    58  	want := Status{
    59  		Phase:           commit,
    60  		IsFrozen:        true,
    61  		IsFullySynced:   true,
    62  		Round:           2,
    63  		LastWonRound:    2,
    64  		LastPlayedRound: 2,
    65  		LastFrozenRound: 2,
    66  		Block:           2,
    67  		Fees:            big.NewInt(0),
    68  		Reward:          big.NewInt(0),
    69  		RoundData:       make(map[uint64]RoundData),
    70  	}
    71  	state := createRedistribution(t, nil, nil)
    72  	state.SetCurrentBlock(input.Block)
    73  	state.SetCurrentEvent(input.Phase, input.Round)
    74  	state.SetFullySynced(input.IsFullySynced)
    75  	state.SetLastWonRound(input.LastWonRound)
    76  	state.SetFrozen(input.IsFrozen, input.LastFrozenRound)
    77  	state.SetLastPlayedRound(input.LastPlayedRound)
    78  	got, err := state.Status()
    79  	if err != nil {
    80  		t.Fatal("failed to get state")
    81  	}
    82  
    83  	opt := []cmp.Option{
    84  		cmp.AllowUnexported(big.Int{}),
    85  		cmp.AllowUnexported(Status{}),
    86  	}
    87  	if diff := cmp.Diff(want, *got, opt...); diff != "" {
    88  		t.Errorf("result mismatch (-want +have):\n%s", diff)
    89  	}
    90  
    91  }
    92  
    93  func TestStateRoundData(t *testing.T) {
    94  	t.Parallel()
    95  
    96  	t.Run("sample data", func(t *testing.T) {
    97  		t.Parallel()
    98  
    99  		state := createRedistribution(t, nil, nil)
   100  
   101  		_, exists := state.SampleData(1)
   102  		if exists {
   103  			t.Error("should not exists")
   104  		}
   105  
   106  		savedSample := SampleData{
   107  			ReserveSampleHash: swarm.RandAddress(t),
   108  			StorageRadius:     3,
   109  		}
   110  		state.SetSampleData(1, savedSample, 0)
   111  
   112  		sample, exists := state.SampleData(1)
   113  		if !exists {
   114  			t.Error("should exist")
   115  		}
   116  		if diff := cmp.Diff(savedSample, sample); diff != "" {
   117  			t.Errorf("sample mismatch (-want +have):\n%s", diff)
   118  		}
   119  	})
   120  
   121  	t.Run("commit key", func(t *testing.T) {
   122  		t.Parallel()
   123  
   124  		state := createRedistribution(t, nil, nil)
   125  
   126  		_, exists := state.CommitKey(1)
   127  		if exists {
   128  			t.Error("should not exists")
   129  		}
   130  
   131  		savedKey := testutil.RandBytes(t, swarm.HashSize)
   132  		state.SetCommitKey(1, savedKey)
   133  
   134  		key, exists := state.CommitKey(1)
   135  		if !exists {
   136  			t.Error("should exist")
   137  		}
   138  		if diff := cmp.Diff(savedKey, key); diff != "" {
   139  			t.Errorf("key mismatch (-want +have):\n%s", diff)
   140  		}
   141  	})
   142  
   143  	t.Run("has revealed", func(t *testing.T) {
   144  		t.Parallel()
   145  
   146  		state := createRedistribution(t, nil, nil)
   147  
   148  		if state.HasRevealed(1) {
   149  			t.Error("should not be revealed")
   150  		}
   151  
   152  		state.SetHasRevealed(1)
   153  
   154  		if !state.HasRevealed(1) {
   155  			t.Error("should be revealed")
   156  		}
   157  	})
   158  
   159  }
   160  
   161  func TestPurgeRoundData(t *testing.T) {
   162  	t.Parallel()
   163  
   164  	state := createRedistribution(t, nil, nil)
   165  
   166  	// helper function which populates data at specified round
   167  	populateDataAtRound := func(round uint64) {
   168  		savedSample := SampleData{
   169  			ReserveSampleHash: swarm.RandAddress(t),
   170  			StorageRadius:     3,
   171  		}
   172  		commitKey := testutil.RandBytes(t, swarm.HashSize)
   173  
   174  		state.SetSampleData(round, savedSample, 0)
   175  		state.SetCommitKey(round, commitKey)
   176  		state.SetHasRevealed(round)
   177  	}
   178  
   179  	// asserts if there is, or there isn't, data at specified round
   180  	assertHasDataAtRound := func(round uint64, shouldHaveData bool) {
   181  		check := func(exists bool) {
   182  			if shouldHaveData && !exists {
   183  				t.Error("should have data")
   184  			} else if !shouldHaveData && exists {
   185  				t.Error("should not have data")
   186  			}
   187  		}
   188  
   189  		_, exists1 := state.SampleData(round)
   190  		_, exists2 := state.CommitKey(round)
   191  		exists3 := state.HasRevealed(round)
   192  
   193  		check(exists1)
   194  		check(exists2)
   195  		check(exists3)
   196  	}
   197  
   198  	const roundsCount = 100
   199  	hasRoundData := make([]bool, roundsCount)
   200  
   201  	// Populate data at random rounds
   202  	for i := uint64(0); i < roundsCount; i++ {
   203  		v := rand.Int()%2 == 0
   204  		hasRoundData[i] = v
   205  		if v {
   206  			populateDataAtRound(i)
   207  		}
   208  		assertHasDataAtRound(i, v)
   209  	}
   210  
   211  	// Run purge successively and assert that all data is purged up to
   212  	// currentRound - purgeDataOlderThenXRounds
   213  	for i := uint64(0); i < roundsCount; i++ {
   214  		state.SetCurrentEvent(0, i)
   215  		state.purgeStaleRoundData()
   216  
   217  		if i <= purgeStaleDataThreshold {
   218  			assertHasDataAtRound(i, hasRoundData[i])
   219  		} else {
   220  			for j := uint64(0); j < i-purgeStaleDataThreshold; j++ {
   221  				assertHasDataAtRound(j, false)
   222  			}
   223  		}
   224  	}
   225  
   226  	// Purge remaining data in single go
   227  	round := uint64(roundsCount + purgeStaleDataThreshold)
   228  	state.SetCurrentEvent(0, round)
   229  	state.purgeStaleRoundData()
   230  
   231  	// One more time assert that everything was purged
   232  	for i := uint64(0); i < roundsCount; i++ {
   233  		assertHasDataAtRound(i, false)
   234  	}
   235  }
   236  
   237  // TestReward test reward calculations. It also checks whether reward is incremented after second win.
   238  func TestReward(t *testing.T) {
   239  	t.Parallel()
   240  	ctx := context.Background()
   241  	// first win reward calculation
   242  	initialBalance := big.NewInt(3000)
   243  	state := createRedistribution(t, []erc20mock.Option{
   244  		erc20mock.WithBalanceOfFunc(func(ctx context.Context, address common.Address) (*big.Int, error) {
   245  			return initialBalance, nil
   246  		}),
   247  	}, nil)
   248  	err := state.SetBalance(ctx)
   249  	if err != nil {
   250  		t.Fatal("failed to set balance")
   251  	}
   252  	balanceAfterFirstWin := big.NewInt(4000)
   253  	state.erc20Service = erc20mock.New([]erc20mock.Option{
   254  		erc20mock.WithBalanceOfFunc(func(ctx context.Context, address common.Address) (*big.Int, error) {
   255  			return big.NewInt(4000), nil
   256  		}),
   257  	}...)
   258  
   259  	err = state.CalculateWinnerReward(ctx)
   260  	if err != nil {
   261  		t.Fatal("failed to calculate reward")
   262  	}
   263  	firstWinResult, err := state.Status()
   264  	if err != nil {
   265  		t.Fatal("failed to get status")
   266  	}
   267  	expectedReward := balanceAfterFirstWin.Sub(balanceAfterFirstWin, initialBalance)
   268  	if firstWinResult.Reward.Cmp(expectedReward) != 0 {
   269  		t.Fatalf("expect reward %d got %d", expectedReward, firstWinResult.Reward)
   270  	}
   271  
   272  	// Second win reward calculation. The reward should add up
   273  	err = state.SetBalance(ctx)
   274  	if err != nil {
   275  		t.Fatal("failed to set balance")
   276  	}
   277  	// Set latest balance
   278  	newCurrentBalance := state.currentBalance
   279  	balanceAfterSecondWin := big.NewInt(7000)
   280  	state.erc20Service = erc20mock.New([]erc20mock.Option{
   281  		erc20mock.WithBalanceOfFunc(func(ctx context.Context, address common.Address) (*big.Int, error) {
   282  			return big.NewInt(7000), nil
   283  		}),
   284  	}...)
   285  
   286  	err = state.CalculateWinnerReward(ctx)
   287  	if err != nil {
   288  		t.Fatal("failed to calculate reward")
   289  	}
   290  	secondWinResult, err := state.Status()
   291  	if err != nil {
   292  		t.Fatal("failed to get status")
   293  	}
   294  	expectedSecondReward := firstWinResult.Reward.Add(firstWinResult.Reward, balanceAfterSecondWin.Sub(balanceAfterSecondWin, newCurrentBalance))
   295  	if secondWinResult.Reward.Cmp(expectedSecondReward) != 0 {
   296  		t.Fatalf("expect reward %d got %d", expectedSecondReward, secondWinResult.Reward)
   297  	}
   298  }
   299  
   300  // TestFee check if fees increments when called multiple times
   301  func TestFee(t *testing.T) {
   302  	t.Parallel()
   303  	firstFee := big.NewInt(10)
   304  	state := createRedistribution(t, nil, []transactionmock.Option{
   305  		transactionmock.WithTransactionFeeFunc(func(ctx context.Context, txHash common.Hash) (*big.Int, error) {
   306  			return firstFee, nil
   307  		}),
   308  	})
   309  	ctx := context.Background()
   310  	state.AddFee(ctx, common.Hash{})
   311  	gotFirstResult, err := state.Status()
   312  	if err != nil {
   313  		t.Fatal("failed to get status")
   314  	}
   315  	if gotFirstResult.Fees.Cmp(firstFee) != 0 {
   316  		t.Fatalf("expected fee %d got %d", firstFee, gotFirstResult.Fees)
   317  	}
   318  	secondFee := big.NewInt(15)
   319  	state.txService = transactionmock.New([]transactionmock.Option{
   320  		transactionmock.WithTransactionFeeFunc(func(ctx context.Context, txHash common.Hash) (*big.Int, error) {
   321  			return secondFee, nil
   322  		}),
   323  	}...)
   324  
   325  	state.AddFee(ctx, common.Hash{})
   326  	gotSecondResult, err := state.Status()
   327  	if err != nil {
   328  		t.Fatal("failed to get status")
   329  	}
   330  	expectedResult := secondFee.Add(secondFee, firstFee)
   331  	if gotSecondResult.Fees.Cmp(expectedResult) != 0 {
   332  		t.Fatalf("expected fee %d got %d", expectedResult, gotSecondResult.Fees)
   333  	}
   334  }