github.com/iotexproject/iotex-core@v1.14.1-rc1/consensus/scheme/rolldpos/rolldpos.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  	"context"
    10  	"time"
    11  
    12  	"github.com/facebookgo/clock"
    13  	"github.com/iotexproject/go-fsm"
    14  	"github.com/iotexproject/go-pkgs/crypto"
    15  	"github.com/iotexproject/iotex-proto/golang/iotextypes"
    16  	"github.com/pkg/errors"
    17  	"go.uber.org/zap"
    18  
    19  	"github.com/iotexproject/iotex-core/action/protocol/rolldpos"
    20  	"github.com/iotexproject/iotex-core/blockchain"
    21  	"github.com/iotexproject/iotex-core/blockchain/block"
    22  	"github.com/iotexproject/iotex-core/blockchain/genesis"
    23  	"github.com/iotexproject/iotex-core/consensus/consensusfsm"
    24  	"github.com/iotexproject/iotex-core/consensus/scheme"
    25  	"github.com/iotexproject/iotex-core/db"
    26  	"github.com/iotexproject/iotex-core/endorsement"
    27  	"github.com/iotexproject/iotex-core/pkg/log"
    28  )
    29  
    30  var (
    31  	// ErrNewRollDPoS indicates the error of constructing RollDPoS
    32  	ErrNewRollDPoS = errors.New("error when constructing RollDPoS")
    33  	// ErrZeroDelegate indicates seeing 0 delegates in the network
    34  	ErrZeroDelegate = errors.New("zero delegates in the network")
    35  	// ErrNotEnoughCandidates indicates there are not enough candidates from the candidate pool
    36  	ErrNotEnoughCandidates = errors.New("Candidate pool does not have enough candidates")
    37  )
    38  
    39  type (
    40  	// Config is the config struct for RollDPoS consensus package
    41  	Config struct {
    42  		FSM               consensusfsm.ConsensusTiming `yaml:"fsm"`
    43  		ToleratedOvertime time.Duration                `yaml:"toleratedOvertime"`
    44  		Delay             time.Duration                `yaml:"delay"`
    45  		ConsensusDBPath   string                       `yaml:"consensusDBPath"`
    46  	}
    47  
    48  	// ChainManager defines the blockchain interface
    49  	ChainManager interface {
    50  		// BlockProposeTime return propose time by height
    51  		BlockProposeTime(uint64) (time.Time, error)
    52  		// BlockCommitTime return commit time by height
    53  		BlockCommitTime(uint64) (time.Time, error)
    54  		// MintNewBlock creates a new block with given actions
    55  		// Note: the coinbase transfer will be added to the given transfers when minting a new block
    56  		MintNewBlock(timestamp time.Time) (*block.Block, error)
    57  		// CommitBlock validates and appends a block to the chain
    58  		CommitBlock(blk *block.Block) error
    59  		// ValidateBlock validates a new block before adding it to the blockchain
    60  		ValidateBlock(blk *block.Block) error
    61  		// TipHeight returns tip block's height
    62  		TipHeight() uint64
    63  		// ChainAddress returns chain address on parent chain, the root chain return empty.
    64  		ChainAddress() string
    65  	}
    66  
    67  	chainManager struct {
    68  		bc blockchain.Blockchain
    69  	}
    70  )
    71  
    72  // DefaultConfig is the default config
    73  var DefaultConfig = Config{
    74  	FSM: consensusfsm.ConsensusTiming{
    75  		UnmatchedEventTTL:            3 * time.Second,
    76  		UnmatchedEventInterval:       100 * time.Millisecond,
    77  		AcceptBlockTTL:               4 * time.Second,
    78  		AcceptProposalEndorsementTTL: 2 * time.Second,
    79  		AcceptLockEndorsementTTL:     2 * time.Second,
    80  		CommitTTL:                    2 * time.Second,
    81  		EventChanSize:                10000,
    82  	},
    83  	ToleratedOvertime: 2 * time.Second,
    84  	Delay:             5 * time.Second,
    85  	ConsensusDBPath:   "/var/data/consensus.db",
    86  }
    87  
    88  // NewChainManager creates a chain manager
    89  func NewChainManager(bc blockchain.Blockchain) ChainManager {
    90  	return &chainManager{
    91  		bc: bc,
    92  	}
    93  }
    94  
    95  // BlockProposeTime return propose time by height
    96  func (cm *chainManager) BlockProposeTime(height uint64) (time.Time, error) {
    97  	if height == 0 {
    98  		return time.Unix(cm.bc.Genesis().Timestamp, 0), nil
    99  	}
   100  	header, err := cm.bc.BlockHeaderByHeight(height)
   101  	if err != nil {
   102  		return time.Time{}, errors.Wrapf(
   103  			err, "error when getting the block at height: %d",
   104  			height,
   105  		)
   106  	}
   107  	return header.Timestamp(), nil
   108  }
   109  
   110  // BlockCommitTime return commit time by height
   111  func (cm *chainManager) BlockCommitTime(height uint64) (time.Time, error) {
   112  	footer, err := cm.bc.BlockFooterByHeight(height)
   113  	if err != nil {
   114  		return time.Time{}, errors.Wrapf(
   115  			err, "error when getting the block at height: %d",
   116  			height,
   117  		)
   118  	}
   119  	return footer.CommitTime(), nil
   120  }
   121  
   122  // MintNewBlock creates a new block with given actions
   123  func (cm *chainManager) MintNewBlock(timestamp time.Time) (*block.Block, error) {
   124  	return cm.bc.MintNewBlock(timestamp)
   125  }
   126  
   127  // CommitBlock validates and appends a block to the chain
   128  func (cm *chainManager) CommitBlock(blk *block.Block) error {
   129  	return cm.bc.CommitBlock(blk)
   130  }
   131  
   132  // ValidateBlock validates a new block before adding it to the blockchain
   133  func (cm *chainManager) ValidateBlock(blk *block.Block) error {
   134  	return cm.bc.ValidateBlock(blk)
   135  }
   136  
   137  // TipHeight returns tip block's height
   138  func (cm *chainManager) TipHeight() uint64 {
   139  	return cm.bc.TipHeight()
   140  }
   141  
   142  // ChainAddress returns chain address on parent chain, the root chain return empty.
   143  func (cm *chainManager) ChainAddress() string {
   144  	return cm.bc.ChainAddress()
   145  }
   146  
   147  // RollDPoS is Roll-DPoS consensus main entrance
   148  type RollDPoS struct {
   149  	cfsm       *consensusfsm.ConsensusFSM
   150  	ctx        RDPoSCtx
   151  	startDelay time.Duration
   152  	ready      chan interface{}
   153  }
   154  
   155  // Start starts RollDPoS consensus
   156  func (r *RollDPoS) Start(ctx context.Context) error {
   157  	if err := r.ctx.Start(ctx); err != nil {
   158  		return errors.Wrap(err, "error when starting the roll dpos context")
   159  	}
   160  	if err := r.cfsm.Start(ctx); err != nil {
   161  		return errors.Wrap(err, "error when starting the consensus FSM")
   162  	}
   163  	if _, err := r.cfsm.BackToPrepare(r.startDelay); err != nil {
   164  		return err
   165  	}
   166  	close(r.ready)
   167  	return nil
   168  }
   169  
   170  // Stop stops RollDPoS consensus
   171  func (r *RollDPoS) Stop(ctx context.Context) error {
   172  	if err := r.cfsm.Stop(ctx); err != nil {
   173  		return errors.Wrap(err, "error when stopping the consensus FSM")
   174  	}
   175  	return errors.Wrap(r.ctx.Stop(ctx), "error when stopping the roll dpos context")
   176  }
   177  
   178  // HandleConsensusMsg handles incoming consensus message
   179  func (r *RollDPoS) HandleConsensusMsg(msg *iotextypes.ConsensusMessage) error {
   180  	// Do not handle consensus message if the node is not active in consensus
   181  	if !r.ctx.Active() {
   182  		return nil
   183  	}
   184  	<-r.ready
   185  	consensusHeight := r.ctx.Height()
   186  	switch {
   187  	case consensusHeight == 0:
   188  		log.Logger("consensus").Debug("consensus component is not ready yet")
   189  		return nil
   190  	case msg.Height < consensusHeight:
   191  		log.Logger("consensus").Debug(
   192  			"old consensus message",
   193  			zap.Uint64("consensusHeight", consensusHeight),
   194  			zap.Uint64("msgHeight", msg.Height),
   195  		)
   196  		return nil
   197  	case msg.Height > consensusHeight+1:
   198  		log.Logger("consensus").Debug(
   199  			"future consensus message",
   200  			zap.Uint64("consensusHeight", consensusHeight),
   201  			zap.Uint64("msgHeight", msg.Height),
   202  		)
   203  		return nil
   204  	}
   205  	endorsedMessage := &EndorsedConsensusMessage{}
   206  	if err := endorsedMessage.LoadProto(msg, r.ctx.BlockDeserializer()); err != nil {
   207  		return errors.Wrapf(err, "failed to decode endorsed consensus message")
   208  	}
   209  	if !endorsement.VerifyEndorsedDocument(endorsedMessage) {
   210  		return errors.New("failed to verify signature in endorsement")
   211  	}
   212  	en := endorsedMessage.Endorsement()
   213  	switch consensusMessage := endorsedMessage.Document().(type) {
   214  	case *blockProposal:
   215  		if err := r.ctx.CheckBlockProposer(endorsedMessage.Height(), consensusMessage, en); err != nil {
   216  			return errors.Wrap(err, "failed to verify block proposal")
   217  		}
   218  		r.cfsm.ProduceReceiveBlockEvent(endorsedMessage)
   219  		return nil
   220  	case *ConsensusVote:
   221  		if err := r.ctx.CheckVoteEndorser(endorsedMessage.Height(), consensusMessage, en); err != nil {
   222  			return errors.Wrapf(err, "failed to verify vote")
   223  		}
   224  		switch consensusMessage.Topic() {
   225  		case PROPOSAL:
   226  			r.cfsm.ProduceReceiveProposalEndorsementEvent(endorsedMessage)
   227  		case LOCK:
   228  			r.cfsm.ProduceReceiveLockEndorsementEvent(endorsedMessage)
   229  		case COMMIT:
   230  			r.cfsm.ProduceReceivePreCommitEndorsementEvent(endorsedMessage)
   231  		}
   232  		return nil
   233  	// TODO: response block by hash, requestBlock.BlockHash
   234  	default:
   235  		return errors.Errorf("Invalid consensus message type %+v", msg)
   236  	}
   237  }
   238  
   239  // Calibrate called on receive a new block not via consensus
   240  func (r *RollDPoS) Calibrate(height uint64) {
   241  	r.cfsm.Calibrate(height)
   242  }
   243  
   244  // ValidateBlockFooter validates the signatures in the block footer
   245  func (r *RollDPoS) ValidateBlockFooter(blk *block.Block) error {
   246  	height := blk.Height()
   247  	round, err := r.ctx.RoundCalculator().NewRound(height, r.ctx.BlockInterval(height), blk.Timestamp(), nil)
   248  	if err != nil {
   249  		return err
   250  	}
   251  	if !round.IsDelegate(blk.ProducerAddress()) {
   252  		return errors.Errorf(
   253  			"block proposer %s is not a valid delegate",
   254  			blk.ProducerAddress(),
   255  		)
   256  	}
   257  	if err := round.AddBlock(blk); err != nil {
   258  		return err
   259  	}
   260  	blkHash := blk.HashBlock()
   261  	for _, en := range blk.Endorsements() {
   262  		if err := round.AddVoteEndorsement(
   263  			NewConsensusVote(blkHash[:], COMMIT),
   264  			en,
   265  		); err != nil {
   266  			return err
   267  		}
   268  	}
   269  	if !round.EndorsedByMajority(blkHash[:], []ConsensusVoteTopic{COMMIT}) {
   270  		return ErrInsufficientEndorsements
   271  	}
   272  
   273  	return nil
   274  }
   275  
   276  // Metrics returns RollDPoS consensus metrics
   277  func (r *RollDPoS) Metrics() (scheme.ConsensusMetrics, error) {
   278  	var metrics scheme.ConsensusMetrics
   279  	height := r.ctx.Chain().TipHeight()
   280  	round, err := r.ctx.RoundCalculator().NewRound(height+1, r.ctx.BlockInterval(height), r.ctx.Clock().Now(), nil)
   281  	if err != nil {
   282  		return metrics, errors.Wrap(err, "error when calculating round")
   283  	}
   284  
   285  	return scheme.ConsensusMetrics{
   286  		LatestEpoch:         round.EpochNum(),
   287  		LatestHeight:        height,
   288  		LatestDelegates:     round.Delegates(),
   289  		LatestBlockProducer: round.proposer,
   290  	}, nil
   291  }
   292  
   293  // NumPendingEvts returns the number of pending events
   294  func (r *RollDPoS) NumPendingEvts() int {
   295  	return r.cfsm.NumPendingEvents()
   296  }
   297  
   298  // CurrentState returns the current state
   299  func (r *RollDPoS) CurrentState() fsm.State {
   300  	return r.cfsm.CurrentState()
   301  }
   302  
   303  // Activate activates or pauses the roll-DPoS consensus. When it is deactivated, the node will finish the current
   304  // consensus round if it is doing the work and then return the the initial state
   305  func (r *RollDPoS) Activate(active bool) {
   306  	r.ctx.Activate(active)
   307  	// reactivate cfsm if the node is reactivated
   308  	if _, err := r.cfsm.BackToPrepare(0); err != nil {
   309  		log.L().Panic("Failed to reactivate cfsm", zap.Error(err))
   310  	}
   311  }
   312  
   313  // Active is true if the roll-DPoS consensus is active, or false if it is stand-by
   314  func (r *RollDPoS) Active() bool {
   315  	return r.ctx.Active() || r.cfsm.CurrentState() != consensusfsm.InitState
   316  }
   317  
   318  type (
   319  	// BuilderConfig returns the configuration of the builder
   320  	BuilderConfig struct {
   321  		Chain              blockchain.Config
   322  		Consensus          Config
   323  		Scheme             string
   324  		DardanellesUpgrade consensusfsm.DardanellesUpgrade
   325  		DB                 db.Config
   326  		Genesis            genesis.Genesis
   327  		SystemActive       bool
   328  	}
   329  
   330  	// Builder is the builder for rollDPoS
   331  	Builder struct {
   332  		cfg BuilderConfig
   333  		// TODO: we should use keystore in the future
   334  		encodedAddr       string
   335  		priKey            crypto.PrivateKey
   336  		chain             ChainManager
   337  		blockDeserializer *block.Deserializer
   338  		broadcastHandler  scheme.Broadcast
   339  		clock             clock.Clock
   340  		// TODO: explorer dependency deleted at #1085, need to add api params
   341  		rp                   *rolldpos.Protocol
   342  		delegatesByEpochFunc NodesSelectionByEpochFunc
   343  		proposersByEpochFunc NodesSelectionByEpochFunc
   344  	}
   345  )
   346  
   347  // NewRollDPoSBuilder instantiates a Builder instance
   348  func NewRollDPoSBuilder() *Builder {
   349  	return &Builder{}
   350  }
   351  
   352  // SetConfig sets config
   353  func (b *Builder) SetConfig(cfg BuilderConfig) *Builder {
   354  	b.cfg = cfg
   355  	return b
   356  }
   357  
   358  // SetAddr sets the address and key pair for signature
   359  func (b *Builder) SetAddr(encodedAddr string) *Builder {
   360  	b.encodedAddr = encodedAddr
   361  	return b
   362  }
   363  
   364  // SetPriKey sets the private key
   365  func (b *Builder) SetPriKey(priKey crypto.PrivateKey) *Builder {
   366  	b.priKey = priKey
   367  	return b
   368  }
   369  
   370  // SetChainManager sets the blockchain APIs
   371  func (b *Builder) SetChainManager(chain ChainManager) *Builder {
   372  	b.chain = chain
   373  	return b
   374  }
   375  
   376  // SetBlockDeserializer set block deserializer
   377  func (b *Builder) SetBlockDeserializer(deserializer *block.Deserializer) *Builder {
   378  	b.blockDeserializer = deserializer
   379  	return b
   380  }
   381  
   382  // SetBroadcast sets the broadcast callback
   383  func (b *Builder) SetBroadcast(broadcastHandler scheme.Broadcast) *Builder {
   384  	b.broadcastHandler = broadcastHandler
   385  	return b
   386  }
   387  
   388  // SetClock sets the clock
   389  func (b *Builder) SetClock(clock clock.Clock) *Builder {
   390  	b.clock = clock
   391  	return b
   392  }
   393  
   394  // SetDelegatesByEpochFunc sets delegatesByEpochFunc
   395  func (b *Builder) SetDelegatesByEpochFunc(
   396  	delegatesByEpochFunc NodesSelectionByEpochFunc,
   397  ) *Builder {
   398  	b.delegatesByEpochFunc = delegatesByEpochFunc
   399  	return b
   400  }
   401  
   402  // SetProposersByEpochFunc sets proposersByEpochFunc
   403  func (b *Builder) SetProposersByEpochFunc(
   404  	proposersByEpochFunc NodesSelectionByEpochFunc,
   405  ) *Builder {
   406  	b.proposersByEpochFunc = proposersByEpochFunc
   407  	return b
   408  }
   409  
   410  // RegisterProtocol sets the rolldpos protocol
   411  func (b *Builder) RegisterProtocol(rp *rolldpos.Protocol) *Builder {
   412  	b.rp = rp
   413  	return b
   414  }
   415  
   416  // Build builds a RollDPoS consensus module
   417  func (b *Builder) Build() (*RollDPoS, error) {
   418  	if b.chain == nil {
   419  		return nil, errors.Wrap(ErrNewRollDPoS, "blockchain APIs is nil")
   420  	}
   421  	if b.broadcastHandler == nil {
   422  		return nil, errors.Wrap(ErrNewRollDPoS, "broadcast callback is nil")
   423  	}
   424  	if b.clock == nil {
   425  		b.clock = clock.New()
   426  	}
   427  	b.cfg.DB.DbPath = b.cfg.Consensus.ConsensusDBPath
   428  	ctx, err := NewRollDPoSCtx(
   429  		consensusfsm.NewConsensusConfig(b.cfg.Consensus.FSM, b.cfg.DardanellesUpgrade, b.cfg.Genesis, b.cfg.Consensus.Delay),
   430  		b.cfg.DB,
   431  		b.cfg.SystemActive,
   432  		b.cfg.Consensus.ToleratedOvertime,
   433  		b.cfg.Genesis.TimeBasedRotation,
   434  		b.chain,
   435  		b.blockDeserializer,
   436  		b.rp,
   437  		b.broadcastHandler,
   438  		b.delegatesByEpochFunc,
   439  		b.proposersByEpochFunc,
   440  		b.encodedAddr,
   441  		b.priKey,
   442  		b.clock,
   443  		b.cfg.Genesis.BeringBlockHeight,
   444  	)
   445  	if err != nil {
   446  		return nil, errors.Wrap(err, "error when constructing consensus context")
   447  	}
   448  	cfsm, err := consensusfsm.NewConsensusFSM(ctx, b.clock)
   449  	if err != nil {
   450  		return nil, errors.Wrap(err, "error when constructing the consensus FSM")
   451  	}
   452  	return &RollDPoS{
   453  		cfsm:       cfsm,
   454  		ctx:        ctx,
   455  		startDelay: b.cfg.Consensus.Delay,
   456  		ready:      make(chan interface{}),
   457  	}, nil
   458  }