github.com/koko1123/flow-go-1@v0.29.6/consensus/hotstuff/pacemaker/pacemaker.go (about) 1 package pacemaker 2 3 import ( 4 "fmt" 5 "time" 6 7 "go.uber.org/atomic" 8 9 "github.com/koko1123/flow-go-1/consensus/hotstuff" 10 "github.com/koko1123/flow-go-1/consensus/hotstuff/model" 11 "github.com/koko1123/flow-go-1/consensus/hotstuff/pacemaker/timeout" 12 "github.com/koko1123/flow-go-1/model/flow" 13 ) 14 15 // NitroPaceMaker implements the hotstuff.PaceMaker 16 // Its an aggressive pacemaker with exponential increase on timeout as well as 17 // exponential decrease on progress. Progress is defined as entering view V 18 // for which the replica knows a QC with V = QC.view + 1 19 type NitroPaceMaker struct { 20 currentView uint64 21 timeoutControl *timeout.Controller 22 notifier hotstuff.Consumer 23 started *atomic.Bool 24 } 25 26 // New creates a new NitroPaceMaker instance 27 // startView is the view for the pacemaker to start from 28 // timeoutController controls the timeout trigger. 29 // notifier provides callbacks for pacemaker events. 30 func New(startView uint64, timeoutController *timeout.Controller, notifier hotstuff.Consumer) (*NitroPaceMaker, error) { 31 if startView < 1 { 32 return nil, model.NewConfigurationErrorf("Please start PaceMaker with view > 0. (View 0 is reserved for genesis block, which has no proposer)") 33 } 34 pm := NitroPaceMaker{ 35 currentView: startView, 36 timeoutControl: timeoutController, 37 notifier: notifier, 38 started: atomic.NewBool(false), 39 } 40 return &pm, nil 41 } 42 43 // gotoView updates the current view to newView. Currently, the calling code 44 // ensures that the view number is STRICTLY monotonously increasing. The method 45 // gotoView panics as a last resort if FlowPaceMaker is modified to violate this condition. 46 // Hence, gotoView will _always_ return a NewViewEvent for an _increased_ view number. 47 func (p *NitroPaceMaker) gotoView(newView uint64) *model.NewViewEvent { 48 if newView <= p.currentView { 49 // This should never happen: in the current implementation, it is trivially apparent that 50 // newView is _always_ larger than currentView. This check is to protect the code from 51 // future modifications that violate the necessary condition for 52 // STRICTLY monotonously increasing view numbers. 53 panic(fmt.Sprintf("cannot move from view %d to %d: currentView must be strictly monotonously increasing", p.currentView, newView)) 54 } 55 p.currentView = newView 56 timerInfo := p.timeoutControl.StartTimeout(model.ReplicaTimeout, newView) 57 p.notifier.OnStartingTimeout(timerInfo) 58 return &model.NewViewEvent{View: p.currentView} 59 } 60 61 // CurView returns the current view 62 func (p *NitroPaceMaker) CurView() uint64 { 63 return p.currentView 64 } 65 66 // TimeoutChannel returns the timeout channel for current active timeout. 67 // Note the returned timeout channel returns only one timeout, which is the current 68 // timeout. 69 // To get the timeout for the next timeout, you need to call TimeoutChannel() again. 70 func (p *NitroPaceMaker) TimeoutChannel() <-chan time.Time { 71 return p.timeoutControl.Channel() 72 } 73 74 // UpdateCurViewWithQC notifies the pacemaker with a new QC, which might allow pacemaker to 75 // fast forward its view. 76 func (p *NitroPaceMaker) UpdateCurViewWithQC(qc *flow.QuorumCertificate) (*model.NewViewEvent, bool) { 77 if qc.View < p.currentView { 78 return nil, false 79 } 80 // qc.view = p.currentView + k for k ≥ 0 81 // 2/3 of replicas have already voted for round p.currentView + k, hence proceeded past currentView 82 // => 2/3 of replicas are at least in view qc.view + 1. 83 // => replica can skip ahead to view qc.view + 1 84 p.timeoutControl.OnProgressBeforeTimeout() 85 86 newView := qc.View + 1 87 p.notifier.OnQcTriggeredViewChange(qc, newView) 88 return p.gotoView(newView), true 89 } 90 91 // UpdateCurViewWithBlock indicates the pacermaker that the block for the current view has received. 92 // and isLeaderForNextView indicates whether or not this replica is the primary for the NEXT view. 93 func (p *NitroPaceMaker) UpdateCurViewWithBlock(block *model.Block, isLeaderForNextView bool) (*model.NewViewEvent, bool) { 94 // use block's QC to fast-forward if possible 95 newViewOnQc, newViewOccurredOnQc := p.UpdateCurViewWithQC(block.QC) 96 if block.View != p.currentView { 97 return newViewOnQc, newViewOccurredOnQc 98 } 99 // block is for current view 100 101 if p.timeoutControl.TimerInfo().Mode != model.ReplicaTimeout { 102 // i.e. we are already on timeout.VoteCollectionTimeout. 103 // This edge case can occur as follows: 104 // * we previously already have processed a block for the current view 105 // and started the vote collection phase 106 // In this case, we do NOT want to RE-start the vote collection timer 107 // if we get a second block for the current View. 108 return nil, false 109 } 110 newViewOnBlock, newViewOccurredOnBlock := p.actOnBlockForCurView(block, isLeaderForNextView) 111 if !newViewOccurredOnBlock { // if processing current block didn't lead to NewView event, 112 // the initial processing of the block's QC still might have changes the view: 113 return newViewOnQc, newViewOccurredOnQc 114 } 115 // processing current block created NewView event, which is always newer than any potential newView event from processing the block's QC 116 return newViewOnBlock, newViewOccurredOnBlock 117 } 118 119 func (p *NitroPaceMaker) actOnBlockForCurView(block *model.Block, isLeaderForNextView bool) (*model.NewViewEvent, bool) { 120 if isLeaderForNextView { 121 timerInfo := p.timeoutControl.StartTimeout(model.VoteCollectionTimeout, p.currentView) 122 p.notifier.OnStartingTimeout(timerInfo) 123 return nil, false 124 } 125 if block.QC.View+1 == p.currentView { 126 // only decrease timeout if block has been build on a quorum from the previous view; 127 // otherwise, the committee is still not synchronized (as the qc is from a view _prior_ to the previous one) 128 p.timeoutControl.OnProgressBeforeTimeout() 129 } 130 return p.gotoView(p.currentView + 1), true 131 } 132 133 // OnTimeout notifies the pacemaker that the timeout event has looped through the event loop. 134 // It always trigger a view change, and the new view will be returned as NewViewEvent 135 func (p *NitroPaceMaker) OnTimeout() *model.NewViewEvent { 136 p.emitTimeoutNotifications(p.timeoutControl.TimerInfo()) 137 p.timeoutControl.OnTimeout() 138 return p.gotoView(p.currentView + 1) 139 } 140 141 func (p *NitroPaceMaker) emitTimeoutNotifications(timeout *model.TimerInfo) { 142 p.notifier.OnReachedTimeout(timeout) 143 } 144 145 // Start starts the pacemaker 146 func (p *NitroPaceMaker) Start() { 147 if p.started.Swap(true) { 148 return 149 } 150 timerInfo := p.timeoutControl.StartTimeout(model.ReplicaTimeout, p.currentView) 151 p.notifier.OnStartingTimeout(timerInfo) 152 } 153 154 // BlockRateDelay returns the delay for broadcasting its own proposals. 155 func (p *NitroPaceMaker) BlockRateDelay() time.Duration { 156 return p.timeoutControl.BlockRateDelay() 157 }