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  }