github.com/iotexproject/iotex-core@v1.14.1-rc1/consensus/scheme/rolldpos/roundcalculator.go (about)

     1  // Copyright (c) 2019 IoTeX Foundation
     2  // This source code is provided 'as is' and no warranties are given as to title or non-infringement, merchantability
     3  // or fitness for purpose and, to the extent permitted by law, all liability for your use of the code is disclaimed.
     4  // This source code is governed by Apache License 2.0 that can be found in the LICENSE file.
     5  
     6  package rolldpos
     7  
     8  import (
     9  	"time"
    10  
    11  	"github.com/pkg/errors"
    12  
    13  	"github.com/iotexproject/iotex-core/action/protocol/rolldpos"
    14  	"github.com/iotexproject/iotex-core/endorsement"
    15  )
    16  
    17  var errInvalidCurrentTime = errors.New("invalid current time")
    18  
    19  type roundCalculator struct {
    20  	chain                ChainManager
    21  	timeBasedRotation    bool
    22  	rp                   *rolldpos.Protocol
    23  	delegatesByEpochFunc NodesSelectionByEpochFunc
    24  	proposersByEpochFunc NodesSelectionByEpochFunc
    25  	beringHeight         uint64
    26  }
    27  
    28  // UpdateRound updates previous roundCtx
    29  func (c *roundCalculator) UpdateRound(round *roundCtx, height uint64, blockInterval time.Duration, now time.Time, toleratedOvertime time.Duration) (*roundCtx, error) {
    30  	epochNum := round.EpochNum()
    31  	epochStartHeight := round.EpochStartHeight()
    32  	delegates := round.Delegates()
    33  	proposers := round.Proposers()
    34  	switch {
    35  	case height < round.Height():
    36  		return nil, errors.New("cannot update to a lower height")
    37  	case height == round.Height():
    38  		if now.Before(round.StartTime()) {
    39  			return round, nil
    40  		}
    41  	default:
    42  		if height >= round.NextEpochStartHeight() {
    43  			// update the epoch
    44  			epochNum = c.rp.GetEpochNum(height)
    45  			epochStartHeight = c.rp.GetEpochHeight(epochNum)
    46  			var err error
    47  			if delegates, err = c.Delegates(height); err != nil {
    48  				return nil, err
    49  			}
    50  			if proposers, err = c.Proposers(height); err != nil {
    51  				return nil, err
    52  			}
    53  		}
    54  	}
    55  	roundNum, roundStartTime, err := c.roundInfo(height, blockInterval, now, toleratedOvertime)
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  	proposer, err := c.calculateProposer(height, roundNum, proposers)
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  	var status status
    64  	var blockInLock []byte
    65  	var proofOfLock []*endorsement.Endorsement
    66  	if height == round.Height() {
    67  		err = round.eManager.Cleanup(roundStartTime)
    68  		if err != nil {
    69  			return nil, err
    70  		}
    71  		status = round.status
    72  		blockInLock = round.blockInLock
    73  		proofOfLock = round.proofOfLock
    74  	} else {
    75  		err = round.eManager.Cleanup(time.Time{})
    76  		if err != nil {
    77  			return nil, err
    78  		}
    79  	}
    80  	return &roundCtx{
    81  		epochNum:             epochNum,
    82  		epochStartHeight:     epochStartHeight,
    83  		nextEpochStartHeight: c.rp.GetEpochHeight(epochNum + 1),
    84  		delegates:            delegates,
    85  		proposers:            proposers,
    86  
    87  		height:             height,
    88  		roundNum:           roundNum,
    89  		proposer:           proposer,
    90  		roundStartTime:     roundStartTime,
    91  		nextRoundStartTime: roundStartTime.Add(blockInterval),
    92  		eManager:           round.eManager,
    93  		status:             status,
    94  		blockInLock:        blockInLock,
    95  		proofOfLock:        proofOfLock,
    96  	}, nil
    97  }
    98  
    99  // Proposer returns the block producer of the round
   100  func (c *roundCalculator) Proposer(height uint64, blockInterval time.Duration, roundStartTime time.Time) string {
   101  	round, err := c.newRound(height, blockInterval, roundStartTime, nil, 0)
   102  	if err != nil {
   103  		return ""
   104  	}
   105  
   106  	return round.Proposer()
   107  }
   108  
   109  func (c *roundCalculator) IsDelegate(addr string, height uint64) bool {
   110  	delegates, err := c.Delegates(height)
   111  	if err != nil {
   112  		return false
   113  	}
   114  	for _, d := range delegates {
   115  		if addr == d {
   116  			return true
   117  		}
   118  	}
   119  
   120  	return false
   121  }
   122  
   123  // RoundInfo returns information of round by the given height and current time
   124  func (c *roundCalculator) RoundInfo(
   125  	height uint64,
   126  	blockInterval time.Duration,
   127  	now time.Time,
   128  ) (roundNum uint32, roundStartTime time.Time, err error) {
   129  	return c.roundInfo(height, blockInterval, now, 0)
   130  }
   131  
   132  func (c *roundCalculator) roundInfo(
   133  	height uint64,
   134  	blockInterval time.Duration,
   135  	now time.Time,
   136  	toleratedOvertime time.Duration,
   137  ) (roundNum uint32, roundStartTime time.Time, err error) {
   138  	var lastBlockTime time.Time
   139  	if lastBlockTime, err = c.chain.BlockProposeTime(0); err != nil {
   140  		return
   141  	}
   142  	if height > 1 {
   143  		if height >= c.beringHeight {
   144  			var lastBlkProposeTime time.Time
   145  			if lastBlkProposeTime, err = c.chain.BlockProposeTime(height - 1); err != nil {
   146  				return
   147  			}
   148  			lastBlockTime = lastBlockTime.Add(lastBlkProposeTime.Sub(lastBlockTime) / blockInterval * blockInterval)
   149  		} else {
   150  			var lastBlkCommitTime time.Time
   151  			if lastBlkCommitTime, err = c.chain.BlockCommitTime(height - 1); err != nil {
   152  				return
   153  			}
   154  			lastBlockTime = lastBlockTime.Add(lastBlkCommitTime.Sub(lastBlockTime) / blockInterval * blockInterval)
   155  		}
   156  	}
   157  	if !lastBlockTime.Before(now) {
   158  		// TODO: if this is the case, it is possible that the system time is far behind the time of other nodes.
   159  		// better error handling may be needed on the caller side
   160  		err = errors.Wrapf(
   161  			errInvalidCurrentTime,
   162  			"last block time %s is after than current time %s",
   163  			lastBlockTime,
   164  			now,
   165  		)
   166  		return
   167  	}
   168  	duration := now.Sub(lastBlockTime)
   169  	if duration > blockInterval {
   170  		roundNum = uint32(duration / blockInterval)
   171  		if toleratedOvertime == 0 || duration%blockInterval < toleratedOvertime {
   172  			roundNum--
   173  		}
   174  	}
   175  	roundStartTime = lastBlockTime.Add(time.Duration(roundNum+1) * blockInterval)
   176  
   177  	return roundNum, roundStartTime, nil
   178  }
   179  
   180  // Delegates returns list of delegates at given height
   181  func (c *roundCalculator) Delegates(height uint64) ([]string, error) {
   182  	epochNum := c.rp.GetEpochNum(height)
   183  	return c.delegatesByEpochFunc(epochNum)
   184  }
   185  
   186  // Proposers returns list of candidate proposers at given height
   187  func (c *roundCalculator) Proposers(height uint64) ([]string, error) {
   188  	epochNum := c.rp.GetEpochNum(height)
   189  	return c.proposersByEpochFunc(epochNum)
   190  }
   191  
   192  // NewRoundWithToleration starts new round with tolerated over time
   193  func (c *roundCalculator) NewRoundWithToleration(
   194  	height uint64,
   195  	blockInterval time.Duration,
   196  	now time.Time,
   197  	eManager *endorsementManager,
   198  	toleratedOvertime time.Duration,
   199  ) (round *roundCtx, err error) {
   200  	return c.newRound(height, blockInterval, now, eManager, toleratedOvertime)
   201  }
   202  
   203  // NewRound starts new round and returns roundCtx
   204  func (c *roundCalculator) NewRound(
   205  	height uint64,
   206  	blockInterval time.Duration,
   207  	now time.Time,
   208  	eManager *endorsementManager,
   209  ) (round *roundCtx, err error) {
   210  	return c.newRound(height, blockInterval, now, eManager, 0)
   211  }
   212  
   213  func (c *roundCalculator) newRound(
   214  	height uint64,
   215  	blockInterval time.Duration,
   216  	now time.Time,
   217  	eManager *endorsementManager,
   218  	toleratedOvertime time.Duration,
   219  ) (round *roundCtx, err error) {
   220  	epochNum := uint64(0)
   221  	epochStartHeight := uint64(0)
   222  	var delegates, proposers []string
   223  	var roundNum uint32
   224  	var proposer string
   225  	var roundStartTime time.Time
   226  	if height != 0 {
   227  		epochNum = c.rp.GetEpochNum(height)
   228  		epochStartHeight = c.rp.GetEpochHeight(epochNum)
   229  		if delegates, err = c.Delegates(height); err != nil {
   230  			return
   231  		}
   232  		if proposers, err = c.Proposers(height); err != nil {
   233  			return
   234  		}
   235  		if roundNum, roundStartTime, err = c.roundInfo(height, blockInterval, now, toleratedOvertime); err != nil {
   236  			return
   237  		}
   238  		if proposer, err = c.calculateProposer(height, roundNum, proposers); err != nil {
   239  			return
   240  		}
   241  	}
   242  	if eManager == nil {
   243  		if eManager, err = newEndorsementManager(nil, nil); err != nil {
   244  			return nil, err
   245  		}
   246  	}
   247  	round = &roundCtx{
   248  		epochNum:             epochNum,
   249  		epochStartHeight:     epochStartHeight,
   250  		nextEpochStartHeight: c.rp.GetEpochHeight(epochNum + 1),
   251  		delegates:            delegates,
   252  		proposers:            proposers,
   253  
   254  		height:             height,
   255  		roundNum:           roundNum,
   256  		proposer:           proposer,
   257  		eManager:           eManager,
   258  		roundStartTime:     roundStartTime,
   259  		nextRoundStartTime: roundStartTime.Add(blockInterval),
   260  		status:             _open,
   261  	}
   262  	eManager.SetIsMarjorityFunc(round.EndorsedByMajority)
   263  
   264  	return round, nil
   265  }
   266  
   267  // calculateProposer calulates proposer according to height and round number
   268  func (c *roundCalculator) calculateProposer(
   269  	height uint64,
   270  	round uint32,
   271  	proposers []string,
   272  ) (proposer string, err error) {
   273  	// TODO use number of proposers
   274  	numProposers := c.rp.NumDelegates()
   275  	if numProposers != uint64(len(proposers)) {
   276  		err = errors.New("invalid proposer list")
   277  		return
   278  	}
   279  	idx := height
   280  	if c.timeBasedRotation {
   281  		idx += uint64(round)
   282  	}
   283  	proposer = proposers[idx%numProposers]
   284  	return
   285  }