github.com/onflow/flow-go@v0.33.17/consensus/hotstuff/pacemaker/pacemaker.go (about) 1 package pacemaker 2 3 import ( 4 "context" 5 "fmt" 6 "time" 7 8 "github.com/onflow/flow-go/consensus/hotstuff" 9 "github.com/onflow/flow-go/consensus/hotstuff/model" 10 "github.com/onflow/flow-go/consensus/hotstuff/pacemaker/timeout" 11 "github.com/onflow/flow-go/consensus/hotstuff/tracker" 12 "github.com/onflow/flow-go/model/flow" 13 ) 14 15 // ActivePaceMaker implements the hotstuff.PaceMaker 16 // Conceptually, we use the Pacemaker algorithm first proposed in [1] (specifically Jolteon) and described in more detail in [2]. 17 // [1] https://arxiv.org/abs/2106.10362 18 // [2] https://developers.diem.com/papers/diem-consensus-state-machine-replication-in-the-diem-blockchain/2021-08-17.pdf (aka DiemBFT v4) 19 // 20 // To enter a new view `v`, the Pacemaker must observe a valid QC or TC for view `v-1`. 21 // The Pacemaker also controls when a node should locally time out for a given view. 22 // In contrast to the passive Pacemaker (previous implementation), locally timing a view 23 // does not cause a view change. 24 // A local timeout for a view `v` causes a node to: 25 // * never produce a vote for any proposal with view ≤ `v`, after the timeout 26 // * produce and broadcast a timeout object, which can form a part of the TC for the timed out view 27 // 28 // Not concurrency safe. 29 type ActivePaceMaker struct { 30 hotstuff.ProposalDurationProvider 31 32 ctx context.Context 33 timeoutControl *timeout.Controller 34 notifier hotstuff.ParticipantConsumer 35 viewTracker viewTracker 36 started bool 37 } 38 39 var _ hotstuff.PaceMaker = (*ActivePaceMaker)(nil) 40 var _ hotstuff.ProposalDurationProvider = (*ActivePaceMaker)(nil) 41 42 // New creates a new ActivePaceMaker instance 43 // - startView is the view for the pacemaker to start with. 44 // - timeoutController controls the timeout trigger. 45 // - notifier provides callbacks for pacemaker events. 46 // 47 // Expected error conditions: 48 // * model.ConfigurationError if initial LivenessData is invalid 49 func New( 50 timeoutController *timeout.Controller, 51 proposalDurationProvider hotstuff.ProposalDurationProvider, 52 notifier hotstuff.Consumer, 53 persist hotstuff.Persister, 54 recovery ...recoveryInformation, 55 ) (*ActivePaceMaker, error) { 56 vt, err := newViewTracker(persist) 57 if err != nil { 58 return nil, fmt.Errorf("initializing view tracker failed: %w", err) 59 } 60 61 pm := &ActivePaceMaker{ 62 ProposalDurationProvider: proposalDurationProvider, 63 timeoutControl: timeoutController, 64 notifier: notifier, 65 viewTracker: vt, 66 started: false, 67 } 68 for _, recoveryAction := range recovery { 69 err = recoveryAction(pm) 70 if err != nil { 71 return nil, fmt.Errorf("ingesting recovery information failed: %w", err) 72 } 73 } 74 return pm, nil 75 } 76 77 // CurView returns the current view 78 func (p *ActivePaceMaker) CurView() uint64 { return p.viewTracker.CurView() } 79 80 // NewestQC returns QC with the highest view discovered by PaceMaker. 81 func (p *ActivePaceMaker) NewestQC() *flow.QuorumCertificate { return p.viewTracker.NewestQC() } 82 83 // LastViewTC returns TC for last view, this will be nil only if the current view 84 // was entered with a QC. 85 func (p *ActivePaceMaker) LastViewTC() *flow.TimeoutCertificate { return p.viewTracker.LastViewTC() } 86 87 // TimeoutChannel returns the timeout channel for current active timeout. 88 // Note the returned timeout channel returns only one timeout, which is the current 89 // timeout. 90 // To get the timeout for the next timeout, you need to call TimeoutChannel() again. 91 func (p *ActivePaceMaker) TimeoutChannel() <-chan time.Time { return p.timeoutControl.Channel() } 92 93 // ProcessQC notifies the pacemaker with a new QC, which might allow pacemaker to 94 // fast-forward its view. In contrast to `ProcessTC`, this function does _not_ handle `nil` inputs. 95 // No errors are expected, any error should be treated as exception 96 func (p *ActivePaceMaker) ProcessQC(qc *flow.QuorumCertificate) (*model.NewViewEvent, error) { 97 initialView := p.CurView() 98 resultingView, err := p.viewTracker.ProcessQC(qc) 99 if err != nil { 100 return nil, fmt.Errorf("unexpected exception in viewTracker while processing QC for view %d: %w", qc.View, err) 101 } 102 if resultingView <= initialView { 103 return nil, nil 104 } 105 106 // QC triggered view change: 107 p.timeoutControl.OnProgressBeforeTimeout() 108 p.notifier.OnQcTriggeredViewChange(initialView, resultingView, qc) 109 110 p.notifier.OnViewChange(initialView, resultingView) 111 timerInfo := p.timeoutControl.StartTimeout(p.ctx, resultingView) 112 p.notifier.OnStartingTimeout(timerInfo) 113 114 return &model.NewViewEvent{ 115 View: timerInfo.View, 116 StartTime: timerInfo.StartTime, 117 Duration: timerInfo.Duration, 118 }, nil 119 } 120 121 // ProcessTC notifies the Pacemaker of a new timeout certificate, which may allow 122 // Pacemaker to fast-forward its current view. 123 // A nil TC is an expected valid input, so that callers may pass in e.g. `Proposal.LastViewTC`, 124 // which may or may not have a value. 125 // No errors are expected, any error should be treated as exception 126 func (p *ActivePaceMaker) ProcessTC(tc *flow.TimeoutCertificate) (*model.NewViewEvent, error) { 127 initialView := p.CurView() 128 resultingView, err := p.viewTracker.ProcessTC(tc) 129 if err != nil { 130 return nil, fmt.Errorf("unexpected exception in viewTracker while processing TC for view %d: %w", tc.View, err) 131 } 132 if resultingView <= initialView { 133 return nil, nil 134 } 135 136 // TC triggered view change: 137 p.timeoutControl.OnTimeout() 138 p.notifier.OnTcTriggeredViewChange(initialView, resultingView, tc) 139 140 p.notifier.OnViewChange(initialView, resultingView) 141 timerInfo := p.timeoutControl.StartTimeout(p.ctx, resultingView) 142 p.notifier.OnStartingTimeout(timerInfo) 143 144 return &model.NewViewEvent{ 145 View: timerInfo.View, 146 StartTime: timerInfo.StartTime, 147 Duration: timerInfo.Duration, 148 }, nil 149 } 150 151 // Start starts the pacemaker by starting the initial timer for the current view. 152 // Start should only be called once - subsequent calls are a no-op. 153 // CAUTION: ActivePaceMaker is not concurrency safe. The Start method must 154 // be executed by the same goroutine that also calls the other business logic 155 // methods, or concurrency safety has to be implemented externally. 156 func (p *ActivePaceMaker) Start(ctx context.Context) { 157 if p.started { 158 return 159 } 160 p.started = true 161 p.ctx = ctx 162 timerInfo := p.timeoutControl.StartTimeout(ctx, p.CurView()) 163 p.notifier.OnStartingTimeout(timerInfo) 164 } 165 166 /* ------------------------------------ recovery parameters for PaceMaker ------------------------------------ */ 167 168 // recoveryInformation provides optional information to the PaceMaker during its construction 169 // to ingest additional information that was potentially lost during a crash or reboot. 170 // Following the "information-driven" approach, we consider potentially older or redundant 171 // information as consistent with our already-present knowledge, i.e. as a no-op. 172 type recoveryInformation func(p *ActivePaceMaker) error 173 174 // WithQCs informs the PaceMaker about the given QCs. Old and nil QCs are accepted (no-op). 175 func WithQCs(qcs ...*flow.QuorumCertificate) recoveryInformation { 176 // To avoid excessive database writes during initialization, we pre-filter the newest QC 177 // here and only hand that one to the viewTracker. For recovery, we allow the special case 178 // of nil QCs, because the genesis block has no QC. 179 tracker := tracker.NewNewestQCTracker() 180 for _, qc := range qcs { 181 if qc == nil { 182 continue // no-op 183 } 184 tracker.Track(qc) 185 } 186 newestQC := tracker.NewestQC() 187 if newestQC == nil { 188 return func(p *ActivePaceMaker) error { return nil } // no-op 189 } 190 191 return func(p *ActivePaceMaker) error { 192 _, err := p.viewTracker.ProcessQC(newestQC) // panics for nil input 193 return err 194 } 195 } 196 197 // WithTCs informs the PaceMaker about the given TCs. Old and nil TCs are accepted (no-op). 198 func WithTCs(tcs ...*flow.TimeoutCertificate) recoveryInformation { 199 qcTracker := tracker.NewNewestQCTracker() 200 tcTracker := tracker.NewNewestTCTracker() 201 for _, tc := range tcs { 202 if tc == nil { 203 continue // no-op 204 } 205 tcTracker.Track(tc) 206 qcTracker.Track(tc.NewestQC) 207 } 208 newestTC := tcTracker.NewestTC() 209 newestQC := qcTracker.NewestQC() 210 if newestTC == nil { // shortcut if no TCs provided 211 return func(p *ActivePaceMaker) error { return nil } // no-op 212 } 213 214 return func(p *ActivePaceMaker) error { 215 _, err := p.viewTracker.ProcessTC(newestTC) // allows nil inputs 216 if err != nil { 217 return fmt.Errorf("viewTracker failed to process newest TC provided in constructor: %w", err) 218 } 219 _, err = p.viewTracker.ProcessQC(newestQC) // should never be nil, because a valid TC always contain a QC 220 if err != nil { 221 return fmt.Errorf("viewTracker failed to process newest QC extracted from the TCs provided in constructor: %w", err) 222 } 223 return nil 224 } 225 }