github.com/ethersphere/bee/v2@v2.2.0/pkg/storageincentives/agent.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 6 7 import ( 8 "context" 9 "crypto/rand" 10 "errors" 11 "fmt" 12 "io" 13 "math/big" 14 "sync" 15 "time" 16 17 "github.com/ethereum/go-ethereum/common" 18 "github.com/ethereum/go-ethereum/core/types" 19 "github.com/ethersphere/bee/v2/pkg/crypto" 20 "github.com/ethersphere/bee/v2/pkg/log" 21 "github.com/ethersphere/bee/v2/pkg/postage" 22 "github.com/ethersphere/bee/v2/pkg/postage/postagecontract" 23 "github.com/ethersphere/bee/v2/pkg/settlement/swap/erc20" 24 "github.com/ethersphere/bee/v2/pkg/storage" 25 "github.com/ethersphere/bee/v2/pkg/storageincentives/redistribution" 26 "github.com/ethersphere/bee/v2/pkg/storageincentives/staking" 27 "github.com/ethersphere/bee/v2/pkg/storer" 28 "github.com/ethersphere/bee/v2/pkg/swarm" 29 "github.com/ethersphere/bee/v2/pkg/transaction" 30 ) 31 32 const loggerName = "storageincentives" 33 34 const ( 35 DefaultBlocksPerRound = 152 36 DefaultBlocksPerPhase = DefaultBlocksPerRound / 4 37 38 // min # of transactions our wallet should be able to cover 39 minTxCountToCover = 15 40 41 // average tx gas used by transactions issued from agent 42 avgTxGas = 250_000 43 ) 44 45 type ChainBackend interface { 46 BlockNumber(context.Context) (uint64, error) 47 HeaderByNumber(context.Context, *big.Int) (*types.Header, error) 48 BalanceAt(ctx context.Context, address common.Address, block *big.Int) (*big.Int, error) 49 SuggestGasPrice(ctx context.Context) (*big.Int, error) 50 } 51 52 type Health interface { 53 IsHealthy() bool 54 } 55 56 type Agent struct { 57 logger log.Logger 58 metrics metrics 59 backend ChainBackend 60 blocksPerRound uint64 61 contract redistribution.Contract 62 batchExpirer postagecontract.PostageBatchExpirer 63 redistributionStatuser staking.RedistributionStatuser 64 store storer.Reserve 65 fullSyncedFunc func() bool 66 overlay swarm.Address 67 quit chan struct{} 68 wg sync.WaitGroup 69 state *RedistributionState 70 chainStateGetter postage.ChainStateGetter 71 commitLock sync.Mutex 72 health Health 73 } 74 75 func New(overlay swarm.Address, 76 ethAddress common.Address, 77 backend ChainBackend, 78 contract redistribution.Contract, 79 batchExpirer postagecontract.PostageBatchExpirer, 80 redistributionStatuser staking.RedistributionStatuser, 81 store storer.Reserve, 82 fullSyncedFunc func() bool, 83 blockTime time.Duration, 84 blocksPerRound, 85 blocksPerPhase uint64, 86 stateStore storage.StateStorer, 87 chainStateGetter postage.ChainStateGetter, 88 erc20Service erc20.Service, 89 tranService transaction.Service, 90 health Health, 91 logger log.Logger, 92 ) (*Agent, error) { 93 a := &Agent{ 94 overlay: overlay, 95 metrics: newMetrics(), 96 backend: backend, 97 logger: logger.WithName(loggerName).Register(), 98 contract: contract, 99 batchExpirer: batchExpirer, 100 store: store, 101 fullSyncedFunc: fullSyncedFunc, 102 blocksPerRound: blocksPerRound, 103 quit: make(chan struct{}), 104 redistributionStatuser: redistributionStatuser, 105 health: health, 106 chainStateGetter: chainStateGetter, 107 } 108 109 state, err := NewRedistributionState(logger, ethAddress, stateStore, erc20Service, tranService) 110 if err != nil { 111 return nil, err 112 } 113 114 a.state = state 115 116 a.wg.Add(1) 117 go a.start(blockTime, a.blocksPerRound, blocksPerPhase) 118 119 return a, nil 120 } 121 122 // start polls the current block number, calculates, and publishes only once the current phase. 123 // Each round is blocksPerRound long and is divided into three blocksPerPhase long phases: commit, reveal, claim. 124 // The sample phase is triggered upon entering the claim phase and may run until the end of the commit phase. 125 // If our neighborhood is selected to participate, a sample is created during the sample phase. In the commit phase, 126 // the sample is submitted, and in the reveal phase, the obfuscation key from the commit phase is submitted. 127 // Next, in the claim phase, we check if we've won, and the cycle repeats. The cycle must occur in the length of one round. 128 func (a *Agent) start(blockTime time.Duration, blocksPerRound, blocksPerPhase uint64) { 129 defer a.wg.Done() 130 131 phaseEvents := newEvents() 132 defer phaseEvents.Close() 133 134 logErr := func(phase PhaseType, round uint64, err error) { 135 if err != nil { 136 a.logger.Error(err, "phase failed", "phase", phase, "round", round) 137 } 138 } 139 140 phaseEvents.On(commit, func(ctx context.Context) { 141 phaseEvents.Cancel(claim) 142 143 round, _ := a.state.currentRoundAndPhase() 144 err := a.handleCommit(ctx, round) 145 logErr(commit, round, err) 146 }) 147 148 phaseEvents.On(reveal, func(ctx context.Context) { 149 phaseEvents.Cancel(commit, sample) 150 round, _ := a.state.currentRoundAndPhase() 151 logErr(reveal, round, a.handleReveal(ctx, round)) 152 }) 153 154 phaseEvents.On(claim, func(ctx context.Context) { 155 phaseEvents.Cancel(reveal) 156 phaseEvents.Publish(sample) 157 158 round, _ := a.state.currentRoundAndPhase() 159 logErr(claim, round, a.handleClaim(ctx, round)) 160 }) 161 162 phaseEvents.On(sample, func(ctx context.Context) { 163 round, _ := a.state.currentRoundAndPhase() 164 isPhasePlayed, err := a.handleSample(ctx, round) 165 logErr(sample, round, err) 166 167 // Sample handled could potentially take long time, therefore it could overlap with commit 168 // phase of next round. When that case happens commit event needs to be triggered once more 169 // in order to handle commit phase with delay. 170 currentRound, currentPhase := a.state.currentRoundAndPhase() 171 if isPhasePlayed && 172 currentPhase == commit && 173 currentRound-1 == round { 174 phaseEvents.Publish(commit) 175 } 176 }) 177 178 var ( 179 prevPhase PhaseType = -1 180 currentPhase PhaseType 181 ) 182 183 phaseCheck := func(ctx context.Context) { 184 ctx, cancel := context.WithTimeout(ctx, blockTime*time.Duration(blocksPerRound)) 185 defer cancel() 186 187 a.metrics.BackendCalls.Inc() 188 block, err := a.backend.BlockNumber(ctx) 189 if err != nil { 190 a.metrics.BackendErrors.Inc() 191 a.logger.Error(err, "getting block number") 192 return 193 } 194 195 a.state.SetCurrentBlock(block) 196 197 round := block / blocksPerRound 198 199 a.metrics.Round.Set(float64(round)) 200 201 p := block % blocksPerRound 202 if p < blocksPerPhase { 203 currentPhase = commit // [0, 37] 204 } else if p >= blocksPerPhase && p < 2*blocksPerPhase { // [38, 75] 205 currentPhase = reveal 206 } else if p >= 2*blocksPerPhase { 207 currentPhase = claim // [76, 151] 208 } 209 210 // write the current phase only once 211 if currentPhase == prevPhase { 212 return 213 } 214 215 prevPhase = currentPhase 216 a.metrics.CurrentPhase.Set(float64(currentPhase)) 217 218 a.logger.Info("entered new phase", "phase", currentPhase.String(), "round", round, "block", block) 219 220 a.state.SetCurrentEvent(currentPhase, round) 221 a.state.SetFullySynced(a.fullSyncedFunc()) 222 a.state.SetHealthy(a.health.IsHealthy()) 223 go a.state.purgeStaleRoundData() 224 225 isFrozen, err := a.redistributionStatuser.IsOverlayFrozen(ctx, block) 226 if err != nil { 227 a.logger.Error(err, "error checking if stake is frozen") 228 } else { 229 a.state.SetFrozen(isFrozen, round) 230 } 231 232 phaseEvents.Publish(currentPhase) 233 } 234 235 ctx, cancel := context.WithCancel(context.Background()) 236 go func() { 237 <-a.quit 238 cancel() 239 }() 240 241 // manually invoke phaseCheck initially in order to set initial data asap 242 phaseCheck(ctx) 243 244 phaseCheckInterval := blockTime 245 // optimization, we do not need to check the phase change at every new block 246 if blocksPerPhase > 10 { 247 phaseCheckInterval = blockTime * 5 248 } 249 250 for { 251 select { 252 case <-ctx.Done(): 253 return 254 case <-time.After(phaseCheckInterval): 255 phaseCheck(ctx) 256 } 257 } 258 } 259 260 func (a *Agent) handleCommit(ctx context.Context, round uint64) error { 261 // commit event handler has to be guarded with lock to avoid 262 // race conditions when handler is triggered again from sample phase 263 a.commitLock.Lock() 264 defer a.commitLock.Unlock() 265 266 if _, exists := a.state.CommitKey(round); exists { 267 // already committed on this round, phase is skipped 268 return nil 269 } 270 271 // the sample has to come from previous round to be able to commit it 272 sample, exists := a.state.SampleData(round - 1) 273 if !exists { 274 // In absence of sample, phase is skipped 275 return nil 276 } 277 278 err := a.commit(ctx, sample, round) 279 if err != nil { 280 return err 281 } 282 283 a.state.SetLastPlayedRound(round) 284 285 return nil 286 } 287 288 func (a *Agent) handleReveal(ctx context.Context, round uint64) error { 289 // reveal requires the commitKey from the same round 290 commitKey, exists := a.state.CommitKey(round) 291 if !exists { 292 // In absence of commitKey, phase is skipped 293 return nil 294 } 295 296 // reveal requires sample from previous round 297 sample, exists := a.state.SampleData(round - 1) 298 if !exists { 299 // Sample must have been saved so far 300 return fmt.Errorf("sample not found in reveal phase") 301 } 302 303 a.metrics.RevealPhase.Inc() 304 305 rsh := sample.ReserveSampleHash.Bytes() 306 txHash, err := a.contract.Reveal(ctx, sample.StorageRadius, rsh, commitKey) 307 if err != nil { 308 a.metrics.ErrReveal.Inc() 309 return err 310 } 311 a.state.AddFee(ctx, txHash) 312 313 a.state.SetHasRevealed(round) 314 315 return nil 316 } 317 318 func (a *Agent) handleClaim(ctx context.Context, round uint64) error { 319 hasRevealed := a.state.HasRevealed(round) 320 if !hasRevealed { 321 // When there was no reveal in same round, phase is skipped 322 return nil 323 } 324 325 a.metrics.ClaimPhase.Inc() 326 327 isWinner, err := a.contract.IsWinner(ctx) 328 if err != nil { 329 a.metrics.ErrWinner.Inc() 330 return err 331 } 332 333 if !isWinner { 334 a.logger.Info("not a winner") 335 // When there is nothing to claim (node is not a winner), phase is played 336 return nil 337 } 338 339 a.state.SetLastWonRound(round) 340 a.metrics.Winner.Inc() 341 342 // In case when there are too many expired batches, Claim trx could runs out of gas. 343 // To prevent this, node should first expire batches before Claiming a reward. 344 err = a.batchExpirer.ExpireBatches(ctx) 345 if err != nil { 346 a.logger.Info("expire batches failed", "err", err) 347 // Even when error happens, proceed with claim handler 348 // because this should not prevent node from claiming a reward 349 } 350 351 errBalance := a.state.SetBalance(ctx) 352 if errBalance != nil { 353 a.logger.Info("could not set balance", "err", err) 354 } 355 356 sampleData, exists := a.state.SampleData(round - 1) 357 if !exists { 358 return fmt.Errorf("sample not found") 359 } 360 361 anchor2, err := a.contract.ReserveSalt(ctx) 362 if err != nil { 363 a.logger.Info("failed getting anchor after second reveal", "err", err) 364 } 365 366 proofs, err := makeInclusionProofs(sampleData.ReserveSampleItems, sampleData.Anchor1, anchor2) 367 if err != nil { 368 return fmt.Errorf("making inclusion proofs: %w", err) 369 } 370 371 txHash, err := a.contract.Claim(ctx, proofs) 372 if err != nil { 373 a.metrics.ErrClaim.Inc() 374 return fmt.Errorf("claiming win: %w", err) 375 } 376 377 a.logger.Info("claimed win") 378 379 if errBalance == nil { 380 errReward := a.state.CalculateWinnerReward(ctx) 381 if errReward != nil { 382 a.logger.Info("calculate winner reward", "err", err) 383 } 384 } 385 386 a.state.AddFee(ctx, txHash) 387 388 return nil 389 } 390 391 func (a *Agent) handleSample(ctx context.Context, round uint64) (bool, error) { 392 storageRadius := a.store.StorageRadius() 393 394 if a.state.IsFrozen() { 395 a.logger.Info("skipping round because node is frozen") 396 return false, nil 397 } 398 399 isPlaying, err := a.contract.IsPlaying(ctx, storageRadius) 400 if err != nil { 401 a.metrics.ErrCheckIsPlaying.Inc() 402 return false, err 403 } 404 if !isPlaying { 405 a.logger.Info("not playing in this round") 406 return false, nil 407 } 408 a.state.SetLastSelectedRound(round + 1) 409 a.metrics.NeighborhoodSelected.Inc() 410 a.logger.Info("neighbourhood chosen", "round", round) 411 412 if !a.state.IsFullySynced() { 413 a.logger.Info("skipping round because node is not fully synced") 414 return false, nil 415 } 416 417 if !a.state.IsHealthy() { 418 a.logger.Info("skipping round because node is unhealhy", "round", round) 419 return false, nil 420 } 421 422 _, hasFunds, err := a.HasEnoughFundsToPlay(ctx) 423 if err != nil { 424 return false, fmt.Errorf("has enough funds to play: %w", err) 425 } else if !hasFunds { 426 a.logger.Info("insufficient funds to play in next round", "round", round) 427 a.metrics.InsufficientFundsToPlay.Inc() 428 return false, nil 429 } 430 431 now := time.Now() 432 sample, err := a.makeSample(ctx, storageRadius) 433 if err != nil { 434 return false, err 435 } 436 dur := time.Since(now) 437 a.metrics.SampleDuration.Set(dur.Seconds()) 438 439 a.logger.Info("produced sample", "hash", sample.ReserveSampleHash, "radius", sample.StorageRadius, "round", round) 440 441 a.state.SetSampleData(round, sample, dur) 442 443 return true, nil 444 } 445 446 func (a *Agent) makeSample(ctx context.Context, storageRadius uint8) (SampleData, error) { 447 salt, err := a.contract.ReserveSalt(ctx) 448 if err != nil { 449 return SampleData{}, err 450 } 451 452 timeLimiter, err := a.getPreviousRoundTime(ctx) 453 if err != nil { 454 return SampleData{}, err 455 } 456 457 rSample, err := a.store.ReserveSample(ctx, salt, storageRadius, uint64(timeLimiter), a.minBatchBalance()) 458 if err != nil { 459 return SampleData{}, err 460 } 461 462 sampleHash, err := sampleHash(rSample.Items) 463 if err != nil { 464 return SampleData{}, err 465 } 466 467 sample := SampleData{ 468 Anchor1: salt, 469 ReserveSampleItems: rSample.Items, 470 ReserveSampleHash: sampleHash, 471 StorageRadius: storageRadius, 472 } 473 474 return sample, nil 475 } 476 477 func (a *Agent) minBatchBalance() *big.Int { 478 cs := a.chainStateGetter.GetChainState() 479 nextRoundBlockNumber := ((a.state.currentBlock() / a.blocksPerRound) + 2) * a.blocksPerRound 480 difference := nextRoundBlockNumber - cs.Block 481 minBalance := new(big.Int).Add(cs.TotalAmount, new(big.Int).Mul(cs.CurrentPrice, big.NewInt(int64(difference)))) 482 483 return minBalance 484 } 485 486 func (a *Agent) getPreviousRoundTime(ctx context.Context) (time.Duration, error) { 487 previousRoundBlockNumber := ((a.state.currentBlock() / a.blocksPerRound) - 1) * a.blocksPerRound 488 489 a.metrics.BackendCalls.Inc() 490 timeLimiterBlock, err := a.backend.HeaderByNumber(ctx, new(big.Int).SetUint64(previousRoundBlockNumber)) 491 if err != nil { 492 a.metrics.BackendErrors.Inc() 493 return 0, err 494 } 495 496 return time.Duration(timeLimiterBlock.Time) * time.Second / time.Nanosecond, nil 497 } 498 499 func (a *Agent) commit(ctx context.Context, sample SampleData, round uint64) error { 500 a.metrics.CommitPhase.Inc() 501 502 key := make([]byte, swarm.HashSize) 503 if _, err := io.ReadFull(rand.Reader, key); err != nil { 504 return err 505 } 506 507 rsh := sample.ReserveSampleHash.Bytes() 508 obfuscatedHash, err := a.wrapCommit(sample.StorageRadius, rsh, key) 509 if err != nil { 510 return err 511 } 512 513 txHash, err := a.contract.Commit(ctx, obfuscatedHash, round) 514 if err != nil { 515 a.metrics.ErrCommit.Inc() 516 return err 517 } 518 a.state.AddFee(ctx, txHash) 519 520 a.state.SetCommitKey(round, key) 521 522 return nil 523 } 524 525 func (a *Agent) Close() error { 526 close(a.quit) 527 528 stopped := make(chan struct{}) 529 go func() { 530 a.wg.Wait() 531 close(stopped) 532 }() 533 534 select { 535 case <-stopped: 536 return nil 537 case <-time.After(5 * time.Second): 538 return errors.New("stopping incentives with ongoing worker goroutine") 539 } 540 } 541 542 func (a *Agent) wrapCommit(storageRadius uint8, sample []byte, key []byte) ([]byte, error) { 543 storageRadiusByte := []byte{storageRadius} 544 545 data := append(a.overlay.Bytes(), storageRadiusByte...) 546 data = append(data, sample...) 547 data = append(data, key...) 548 549 return crypto.LegacyKeccak256(data) 550 } 551 552 // Status returns the node status 553 func (a *Agent) Status() (*Status, error) { 554 return a.state.Status() 555 } 556 557 type SampleWithProofs struct { 558 Hash swarm.Address `json:"hash"` 559 Proofs redistribution.ChunkInclusionProofs `json:"proofs"` 560 Duration time.Duration `json:"duration"` 561 } 562 563 // SampleWithProofs is called only by rchash API 564 func (a *Agent) SampleWithProofs( 565 ctx context.Context, 566 anchor1 []byte, 567 anchor2 []byte, 568 storageRadius uint8, 569 ) (SampleWithProofs, error) { 570 sampleStartTime := time.Now() 571 572 timeLimiter, err := a.getPreviousRoundTime(ctx) 573 if err != nil { 574 return SampleWithProofs{}, err 575 } 576 577 rSample, err := a.store.ReserveSample(ctx, anchor1, storageRadius, uint64(timeLimiter), a.minBatchBalance()) 578 if err != nil { 579 return SampleWithProofs{}, err 580 } 581 582 hash, err := sampleHash(rSample.Items) 583 if err != nil { 584 return SampleWithProofs{}, fmt.Errorf("sample hash: %w", err) 585 } 586 587 proofs, err := makeInclusionProofs(rSample.Items, anchor1, anchor2) 588 if err != nil { 589 return SampleWithProofs{}, fmt.Errorf("make proofs: %w", err) 590 } 591 592 return SampleWithProofs{ 593 Hash: hash, 594 Proofs: proofs, 595 Duration: time.Since(sampleStartTime), 596 }, nil 597 } 598 599 func (a *Agent) HasEnoughFundsToPlay(ctx context.Context) (*big.Int, bool, error) { 600 balance, err := a.backend.BalanceAt(ctx, a.state.ethAddress, nil) 601 if err != nil { 602 return nil, false, err 603 } 604 605 price, err := a.backend.SuggestGasPrice(ctx) 606 if err != nil { 607 return nil, false, err 608 } 609 610 avgTxFee := new(big.Int).Mul(big.NewInt(avgTxGas), price) 611 minBalance := new(big.Int).Mul(avgTxFee, big.NewInt(minTxCountToCover)) 612 613 return minBalance, balance.Cmp(minBalance) >= 1, nil 614 }