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 }