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  }