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 }