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 }