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  }