github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/consensus/hotstuff/votecollector/statemachine.go (about)

     1  package votecollector
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"sync"
     7  
     8  	"github.com/rs/zerolog"
     9  	"go.uber.org/atomic"
    10  
    11  	"github.com/onflow/flow-go/consensus/hotstuff"
    12  	"github.com/onflow/flow-go/consensus/hotstuff/model"
    13  	"github.com/onflow/flow-go/consensus/hotstuff/voteaggregator"
    14  )
    15  
    16  var (
    17  	ErrDifferentCollectorState = errors.New("different state")
    18  )
    19  
    20  // VerifyingVoteProcessorFactory generates hotstuff.VerifyingVoteCollector instances
    21  type VerifyingVoteProcessorFactory = func(log zerolog.Logger, proposal *model.Proposal) (hotstuff.VerifyingVoteProcessor, error)
    22  
    23  // VoteCollector implements a state machine for transition between different states of vote collector
    24  type VoteCollector struct {
    25  	sync.Mutex
    26  	log                      zerolog.Logger
    27  	workers                  hotstuff.Workers
    28  	notifier                 hotstuff.VoteAggregationConsumer
    29  	createVerifyingProcessor VerifyingVoteProcessorFactory
    30  
    31  	votesCache     VotesCache
    32  	votesProcessor atomic.Value
    33  }
    34  
    35  var _ hotstuff.VoteCollector = (*VoteCollector)(nil)
    36  
    37  func (m *VoteCollector) atomicLoadProcessor() hotstuff.VoteProcessor {
    38  	return m.votesProcessor.Load().(*atomicValueWrapper).processor
    39  }
    40  
    41  // atomic.Value doesn't allow storing interfaces as atomic values,
    42  // it requires that stored type is always the same, so we need a wrapper that will mitigate this restriction
    43  // https://github.com/golang/go/issues/22550
    44  type atomicValueWrapper struct {
    45  	processor hotstuff.VoteProcessor
    46  }
    47  
    48  func NewStateMachineFactory(
    49  	log zerolog.Logger,
    50  	notifier hotstuff.VoteAggregationConsumer,
    51  	verifyingVoteProcessorFactory VerifyingVoteProcessorFactory,
    52  ) voteaggregator.NewCollectorFactoryMethod {
    53  	return func(view uint64, workers hotstuff.Workers) (hotstuff.VoteCollector, error) {
    54  		return NewStateMachine(view, log, workers, notifier, verifyingVoteProcessorFactory), nil
    55  	}
    56  }
    57  
    58  func NewStateMachine(
    59  	view uint64,
    60  	log zerolog.Logger,
    61  	workers hotstuff.Workers,
    62  	notifier hotstuff.VoteAggregationConsumer,
    63  	verifyingVoteProcessorFactory VerifyingVoteProcessorFactory,
    64  ) *VoteCollector {
    65  	log = log.With().
    66  		Str("component", "hotstuff.vote_collector").
    67  		Uint64("view", view).
    68  		Logger()
    69  	sm := &VoteCollector{
    70  		log:                      log,
    71  		workers:                  workers,
    72  		notifier:                 notifier,
    73  		createVerifyingProcessor: verifyingVoteProcessorFactory,
    74  		votesCache:               *NewVotesCache(view),
    75  	}
    76  
    77  	// without a block, we don't process votes (only cache them)
    78  	sm.votesProcessor.Store(&atomicValueWrapper{
    79  		processor: NewNoopCollector(hotstuff.VoteCollectorStatusCaching),
    80  	})
    81  	return sm
    82  }
    83  
    84  // AddVote adds a vote to current vote collector
    85  // All expected errors are handled via callbacks to notifier.
    86  // Under normal execution only exceptions are propagated to caller.
    87  func (m *VoteCollector) AddVote(vote *model.Vote) error {
    88  	// Cache vote
    89  	err := m.votesCache.AddVote(vote)
    90  	if err != nil {
    91  		if errors.Is(err, RepeatedVoteErr) {
    92  			return nil
    93  		}
    94  		if doubleVoteErr, isDoubleVoteErr := model.AsDoubleVoteError(err); isDoubleVoteErr {
    95  			m.notifier.OnDoubleVotingDetected(doubleVoteErr.FirstVote, doubleVoteErr.ConflictingVote)
    96  			return nil
    97  		}
    98  		return fmt.Errorf("internal error adding vote %v to cache for block %v: %w",
    99  			vote.ID(), vote.BlockID, err)
   100  	}
   101  
   102  	err = m.processVote(vote)
   103  	if err != nil {
   104  		if errors.Is(err, VoteForIncompatibleBlockError) {
   105  			// For honest nodes, there should be only a single proposal per view and all votes should
   106  			// be for this proposal. However, byzantine nodes might deviate from this happy path:
   107  			// * A malicious leader might create multiple (individually valid) conflicting proposals for the
   108  			//   same view. Honest replicas will send correct votes for whatever proposal they see first.
   109  			//   We only accept the first valid block and reject any other conflicting blocks that show up later.
   110  			// * Alternatively, malicious replicas might send votes with the expected view, but for blocks that
   111  			//   don't exist.
   112  			// In either case, receiving votes for the same view but for different block IDs is a symptom
   113  			// of malicious consensus participants.  Hence, we log it here as a warning:
   114  			m.log.Warn().
   115  				Err(err).
   116  				Msg("received vote for incompatible block")
   117  
   118  			return nil
   119  		}
   120  		return fmt.Errorf("internal error processing vote %v for block %v: %w",
   121  			vote.ID(), vote.BlockID, err)
   122  	}
   123  	return nil
   124  }
   125  
   126  // processVote uses compare-and-repeat pattern to process vote with underlying vote processor
   127  func (m *VoteCollector) processVote(vote *model.Vote) error {
   128  	for {
   129  		processor := m.atomicLoadProcessor()
   130  		currentState := processor.Status()
   131  		err := processor.Process(vote)
   132  		if err != nil {
   133  			if invalidVoteErr, ok := model.AsInvalidVoteError(err); ok {
   134  				m.notifier.OnInvalidVoteDetected(*invalidVoteErr)
   135  				return nil
   136  			}
   137  			// ATTENTION: due to how our logic is designed this situation is only possible
   138  			// where we receive the same vote twice, this is not a case of double voting.
   139  			// This scenario is possible if leader submits his vote additionally to the vote in proposal.
   140  			if model.IsDuplicatedSignerError(err) {
   141  				m.log.Debug().Msgf("duplicated signer %x", vote.SignerID)
   142  				return nil
   143  			}
   144  			return err
   145  		}
   146  
   147  		if currentState != m.Status() {
   148  			continue
   149  		}
   150  
   151  		m.notifier.OnVoteProcessed(vote)
   152  		return nil
   153  	}
   154  }
   155  
   156  // Status returns the status of underlying vote processor
   157  func (m *VoteCollector) Status() hotstuff.VoteCollectorStatus {
   158  	return m.atomicLoadProcessor().Status()
   159  }
   160  
   161  // View returns view associated with this collector
   162  func (m *VoteCollector) View() uint64 {
   163  	return m.votesCache.View()
   164  }
   165  
   166  // ProcessBlock performs validation of block signature and processes block with respected collector.
   167  // In case we have received double proposal, we will stop attempting to build a QC for this view,
   168  // because we don't want to build on any proposal from an equivocating primary. Note: slashing challenges
   169  // for proposal equivocation are triggered by hotstuff.Forks, so we don't have to do anything else here.
   170  //
   171  // The internal state change is implemented as an atomic compare-and-swap, i.e.
   172  // the state transition is only executed if VoteCollector's internal state is
   173  // equal to `expectedValue`. The implementation only allows the transitions
   174  //
   175  //	CachingVotes   -> VerifyingVotes
   176  //	CachingVotes   -> Invalid
   177  //	VerifyingVotes -> Invalid
   178  func (m *VoteCollector) ProcessBlock(proposal *model.Proposal) error {
   179  
   180  	if proposal.Block.View != m.View() {
   181  		return fmt.Errorf("this VoteCollector requires a proposal for view %d but received block %v with view %d",
   182  			m.votesCache.View(), proposal.Block.BlockID, proposal.Block.View)
   183  	}
   184  
   185  	for {
   186  		proc := m.atomicLoadProcessor()
   187  
   188  		switch proc.Status() {
   189  		// first valid block for this view: commence state transition from caching to verifying
   190  		case hotstuff.VoteCollectorStatusCaching:
   191  			err := m.caching2Verifying(proposal)
   192  			if errors.Is(err, ErrDifferentCollectorState) {
   193  				continue // concurrent state update by other thread => restart our logic
   194  			}
   195  
   196  			if err != nil {
   197  				return fmt.Errorf("internal error updating VoteProcessor's status from %s to %s for block %v: %w",
   198  					proc.Status().String(), hotstuff.VoteCollectorStatusVerifying.String(), proposal.Block.BlockID, err)
   199  			}
   200  
   201  			m.log.Info().
   202  				Hex("block_id", proposal.Block.BlockID[:]).
   203  				Msg("vote collector status changed from caching to verifying")
   204  
   205  			m.processCachedVotes(proposal.Block)
   206  
   207  		// We already received a valid block for this view. Check whether the proposer is
   208  		// equivocating and terminate vote processing in this case. Note: proposal equivocation
   209  		// is handled by hotstuff.Forks, so we don't have to do anything else here.
   210  		case hotstuff.VoteCollectorStatusVerifying:
   211  			verifyingProc, ok := proc.(hotstuff.VerifyingVoteProcessor)
   212  			if !ok {
   213  				return fmt.Errorf("while processing block %v, found that VoteProcessor reports status %s but has an incompatible implementation type %T",
   214  					proposal.Block.BlockID, proc.Status(), verifyingProc)
   215  			}
   216  			if verifyingProc.Block().BlockID != proposal.Block.BlockID {
   217  				m.terminateVoteProcessing()
   218  			}
   219  
   220  		// Vote processing for this view has already been terminated. Note: proposal equivocation
   221  		// is handled by hotstuff.Forks, so we don't have anything to do here.
   222  		case hotstuff.VoteCollectorStatusInvalid: /* no op */
   223  
   224  		default:
   225  			return fmt.Errorf("while processing block %v, found that VoteProcessor reported unknown status %s", proposal.Block.BlockID, proc.Status())
   226  		}
   227  
   228  		return nil
   229  	}
   230  }
   231  
   232  // RegisterVoteConsumer registers a VoteConsumer. Upon registration, the collector
   233  // feeds all cached votes into the consumer in the order they arrived.
   234  // CAUTION, VoteConsumer implementations must be
   235  //   - NON-BLOCKING and consume the votes without noteworthy delay, and
   236  //   - CONCURRENCY SAFE
   237  func (m *VoteCollector) RegisterVoteConsumer(consumer hotstuff.VoteConsumer) {
   238  	m.votesCache.RegisterVoteConsumer(consumer)
   239  }
   240  
   241  // caching2Verifying ensures that the VoteProcessor is currently in state `VoteCollectorStatusCaching`
   242  // and replaces it by a newly-created VerifyingVoteProcessor.
   243  // Error returns:
   244  // * ErrDifferentCollectorState if the VoteCollector's state is _not_ `CachingVotes`
   245  // * all other errors are unexpected and potential symptoms of internal bugs or state corruption (fatal)
   246  func (m *VoteCollector) caching2Verifying(proposal *model.Proposal) error {
   247  	blockID := proposal.Block.BlockID
   248  	newProc, err := m.createVerifyingProcessor(m.log, proposal)
   249  	if err != nil {
   250  		return fmt.Errorf("failed to create VerifyingVoteProcessor for block %v: %w", blockID, err)
   251  	}
   252  	newProcWrapper := &atomicValueWrapper{processor: newProc}
   253  
   254  	m.Lock()
   255  	defer m.Unlock()
   256  	proc := m.atomicLoadProcessor()
   257  	if proc.Status() != hotstuff.VoteCollectorStatusCaching {
   258  		return fmt.Errorf("processors's current state is %s: %w", proc.Status().String(), ErrDifferentCollectorState)
   259  	}
   260  	m.votesProcessor.Store(newProcWrapper)
   261  	return nil
   262  }
   263  
   264  func (m *VoteCollector) terminateVoteProcessing() {
   265  	if m.Status() == hotstuff.VoteCollectorStatusInvalid {
   266  		return
   267  	}
   268  	newProcWrapper := &atomicValueWrapper{
   269  		processor: NewNoopCollector(hotstuff.VoteCollectorStatusInvalid),
   270  	}
   271  
   272  	m.Lock()
   273  	defer m.Unlock()
   274  	m.votesProcessor.Store(newProcWrapper)
   275  }
   276  
   277  // processCachedVotes feeds all cached votes into the VoteProcessor
   278  func (m *VoteCollector) processCachedVotes(block *model.Block) {
   279  	cachedVotes := m.votesCache.All()
   280  	m.log.Info().Msgf("processing %d cached votes", len(cachedVotes))
   281  	for _, vote := range cachedVotes {
   282  		if vote.BlockID != block.BlockID {
   283  			continue
   284  		}
   285  
   286  		blockVote := vote
   287  		voteProcessingTask := func() {
   288  			err := m.processVote(blockVote)
   289  			if err != nil {
   290  				m.log.Fatal().Err(err).Msg("internal error processing cached vote")
   291  			}
   292  		}
   293  		m.workers.Submit(voteProcessingTask)
   294  	}
   295  }